栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Go语言

go 字符串与切片数据结构底层分析

Go语言 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

go 字符串与切片数据结构底层分析

文章目录
    • 1. 字符串切片
      • 1.1 查看字符串大小
      • 1.2 len 中表示的什么长度?
      • 1.3 go中字符编码问题(变长编码)
      • 1.4 Unicode 字符集
      • 1.5 utf-8变长编码
      • 1.6 字符串存储图画表示
    • 2. 字符串访问与切分
      • 2.1 下标访问
      • 2.2 range 访问,正确
      • 2.3 go 底层如何判断汉字占3字节,英文占1字节?
      • 2.4 字符串的切分
    • 3. slice 数据结构
      • 3.1 切片的本质
      • 3.2 切片的创建
      • 3.3 汇编分析
      • 3.4 分析代码
      • 3.5 切片的访问
      • 3.6 切片的追加
      • 3.7 解释并发不安全现象

1. 字符串切片 1.1 查看字符串大小

看看下面的code,打印不同字符串占用的大小

package main
import (
	"fmt"
	"unsafe"
) 
func main() {
	fmt.Println(unsafe.Sizeof("刘德华"))
	fmt.Println(unsafe.Sizeof("刘德华是一个	good man"))
	// 16
	// 16
}

为什么两个不同长度在系统中占用的长度都是一样的呢

在go中是stringStruct 表示的

  1. 字符串本质是个结构体
  2. data指针指向底层Byte 数组
1.2 len 中表示的什么长度?

我们可从上面的源码中查看,结构体是私有化,但是我们可以看反射包中的StringHeader 中的结构

code:

import (
	"fmt"
	"reflect"
	"unsafe"
) 
func main() {
	s := "刘德华"
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Println(sh.Len) // 9, utf-8 编码
}

经过两次类型转化求出的结果,结果就是len 表示的是Byte数组长度(字节数),并不是字符个数

1.3 go中字符编码问题(变长编码)

首先需要了解的是:

  1. go 所有的字符均使用unicode 字符集
  2. go中使用utf-8 编码

看看下面代码

import (
	"fmt"
	"reflect"
	"unsafe"
) 
func main() {
	s := "刘德华"
	s1 := "刘德华good man"
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	sh1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
	fmt.Println(sh.Len)   // 9, utf-8 编码
	fmt.Println(sh1.Len)  // 17, utf-8 编码
}
1.4 Unicode 字符集
  1. 一种统一的字符集
  2. 囊括了159种文字的144679个字符
  3. 14万个字符至少需要3个字节表示(2个字节只能表示65536个字符)
  4. 英文字母均排在前128个,所以在全部的英文字符中,这样就很浪费空间了,在这种情况下,就出现了utf-8 变长编码
1.5 utf-8变长编码
  1. unicode 的一种变长格式
  2. 128个US-ASCII 字符自需要 一个字节编码
  3. 西方常用字符需要两个字节
  4. 其他字符(中日韩)需要3个字节,极少数字符需要4个字节
1.6 字符串存储图画表示

2. 字符串访问与切分 2.1 下标访问
func main() {
	s := "刘德华good man"
	//for i := 0; i < len(s); i++ {
	// fmt.Println(s[i]) // 以脚标访问得到的是底层字节, 三个字节是一个字
	//}
}
2.2 range 访问,正确
package main
import "fmt"
func main() {
	s := "刘德华good man"
	for _, c := range s {
		//fmt.Println(c) // 打印的每一个字
		fmt.Printf("%cn", c) // 自动判断出汉字是3个	字节,英文是1个字节
	}
}
2.3 go 底层如何判断汉字占3字节,英文占1字节?

在runtime/utf8.go 文件中

2.4 字符串的切分

字符串的数字是不能随便切分的,因为可能多个字节代表这一个汉字,如果到了必须切分的情况,又该如何处理呢
切分流程(不唯一)

  1. 转为rune 数组
  2. 切片
  3. 转为string
    代码: s = string([]rune(s)[:3]) 取字符串的前三个字符
3. slice 数据结构 3.1 切片的本质

在runtime/slice.go 文件中这样的数据结构

切片的本质是对数组的引用

切片的图画表示:

3.2 切片的创建
  1. 根据数组创建
    arr[0:3] or slice[0:3]
  2. 字面量创建(编译时插入创建数组的代码)
    slice := []int{1, 2,3 }
  3. make: 运行时创建数组
    slice := make([]int, 10)
3.3 汇编分析

code :

func main() {
	s := []int{1, 2, 3}
	fmt.Println(s)
}

通过命令go build -gcflags -S main.go 查看编译出来的汇编代码

s := []int{1, 2, 3} 的汇编如下
main.go:6) LEAQ type.[3]int(SB), AX // 创建一个数组
main.go:6) MOVQ AX, (SP)
main.go:6) PCDATA $1, $0
main.go:6) CALL runtime.newobject(SB) // 新建了一个结构体的值,把三个变量塞进来
main.go:6) MOVQ 8(SP), AX
main.go:6) MOVQ $1, (AX)
main.go:6) MOVQ $2, 8(AX)
main.go:6) MOVQ $3, 16(AX)

3.4 分析代码
arr := [10]int{0,1,2,3,4,5,6,7,8,9}
slice := arr[1,4]

上面的代码用下面的图文表示,可以是这样

3.5 切片的访问
  1. 下标直接访问元素
  2. range 遍历元素
  3. len(slice) 查看切片的长度
  4. cap(slice) 查看数组容量
3.6 切片的追加
  1. 不扩容时,只用调整len (编译器负责)

类似下面你的两幅图,表示其过程


2. 扩容时,编译转为调用runtime.growslice()
这其中的增加空间有点类似c++ 中的vector, 数组是连续的空间,必须这样增长,不能像上面那样直接增长的
过程: 显示扩容成原先的两倍空间(达到一定大小后,就不是两倍的关系了,下面有叙述),之后再将原先空间中的数据拷贝到这里来,最后删除掉原先的数据

3. 补充

  • 如果期望容量大于当前容量的两倍就会使用期望容量
  • 如果当前切片的长度小于1024, 将容量翻倍
  • 如果当前切片的长度大于1024, 每次增加25%
  • 切片扩容时,并发不安全的,注意切片并发要加锁

在runtime.slice.go 中体现上面描述的

3.7 解释并发不安全现象

两个协程读切片的话,如果切片两倍扩容,会废弃原来的数组,会追加新的数组,这时前面的协程还在读老的数组,新追加的数据就会读不到,进而导致不一致问题。

参考:
后台服务器:https://course.0voice.com/v1/course/intro?courseId=5&agentId=0

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/991214.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号