持续创作,加速成长!这是我参与「掘金日新计划 · 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 表示的
- 字符串本质是个结构体
- 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中字符编码问题(变长编码)
首先需要了解的是:
- go 所有的字符均使用unicode 字符集
- 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 字符集
- 一种统一的字符集
- 囊括了159种文字的144679个字符
- 14万个字符至少需要3个字节表示(2个字节只能表示65536个字符)
- 英文字母均排在前128个,所以在全部的英文字符中,这样就很浪费空间了,在这种情况下,就出现了utf-8 变长编码
1.5 utf-8变长编码
- unicode 的一种变长格式
- 128个US-ASCII 字符自需要 一个字节编码
- 西方常用字符需要两个字节
- 其他字符(中日韩)需要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 字符串的切分
字符串的数字是不能随便切分的,因为可能多个字节代表这一个汉字,如果到了必须切分的情况,又该如何处理呢 切分流程(不唯一)
- 转为rune 数组
- 切片
- 转为string
代码:
s = string([]rune(s)[:3])取字符串的前三个字符
3. slice 数据结构
3.1 切片的本质
在runtime/slice.go 文件中这样的数据结构
切片的本质是对数组的引用
切片的图画表示:
3.2 切片的创建
- 根据数组创建
arr[0:3] or slice[0:3] - 字面量创建(编译时插入创建数组的代码)
slice := []int{1, 2,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 0 main.go:6) CALL runtime.newobject(SB) // 新建了一个结构体的值,把三个变量塞进来 main.go:6) MOVQ 8(SP), 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 切片的访问
- 下标直接访问元素
- range 遍历元素
- len(slice) 查看切片的长度
- cap(slice) 查看数组容量
3.6 切片的追加
- 不扩容时,只用调整len (编译器负责)
类似下面你的两幅图,表示其过程
2. 扩容时,编译转为调用runtime.growslice()
这其中的增加空间有点类似c++ 中的vector, 数组是连续的空间,必须这样增长,不能像上面那样直接增长的
过程: 显示扩容成原先的两倍空间(达到一定大小后,就不是两倍的关系了,下面有叙述),之后再将原先空间中的数据拷贝到这里来,最后删除掉原先的数据
3. 补充
- 如果期望容量大于当前容量的两倍就会使用期望容量
- 如果当前切片的长度小于1024, 将容量翻倍
- 如果当前切片的长度大于1024, 每次增加25%
- 切片扩容时,并发不安全的,注意切片并发要加锁
在runtime.slice.go 中体现上面描述的
3.7 解释并发不安全现象
两个协程读切片的话,如果切片两倍扩容,会废弃原来的数组,会追加新的数组,这时前面的协程还在读老的数组,新追加的数据就会读不到,进而导致不一致问题。