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

250 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第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("%c\n", 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,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)MOVQ1, (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 解释并发不安全现象

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