本文将讲解Go语言中的数组与slice。之前看到网上好多 《深入理解slice》、《深入解析slice》... 的文章,我是比较佩服的,他们从应用、源码、汇编代码等各个角度分析了slice与数组,感叹他们已经领先自己好多了,于是把他们的文章结合源码自己学习了一遍。 佩服之余,也在想我有必要再写一篇吗?本着记录自己学习成果、便于新手入门的原则,我还是自己写一篇。尽量不写汇编,免得吓跑大家~
本文不但会讲解slice的结构、存储,还会将slice常见的api分析一下。
1 数组
数组是一块连续的内存。其定义了类型跟长度。因为其长度固定,不易扩展,所以在Go语言中,我们很少直接使用数组。个人理解,数组更多地是作为slice的底层存储来使用。
2 slice
slice可以理解为其他语言中的动态数组。
2.1 定义与数据结构
slice 结构是这样的:
type slice struct {
array unsafe.Pointer // 看就是这个数组
len int // 元素长度
cap int // 容量
}
其实定义中的array 就是一个数组,这个数组可以被多个slice共享。多个slice共享数组可能会引起一些问题,我们在下面会讲。
slice的定义有如下几种方式:
// 方式一 make
slice1 := make([]int, 3, 3)
// 方式二 空slice
var slice2 []int
// 方式三 利用array定义
arr := [3]int{1,2,3}
slice3 := arr[0:3]
无论哪种,其底层使用的函数 都是 makeslice
插播一条,如果找某一句go代码是对应源码中的什么函数?我一般先编译,然后反汇编。
// 编译
go build --gcflags "-N -l" -o main4 main.go
// 反汇编
go tool objdump -s "main\.main" main4
makeslice 主要做了两件事情:
(1)根据类型跟cap 计算内存容量,分配内存;
(2)利用上面分配的内存、len、cap 创建slice。
2.2 append
2.2.1 append之后,新生成的slice跟原来的slice不是一个了
slice最常见的操作就是append。append是向一个slice插入某个元素。执行完append之后会生成一个新的slice。也就是说append之后的slice跟之前的slice肯定不是一个。
看个例子:
func TestSliceAppend_sliceAddrChange(t *testing.T) {
arr := [3]int{1,2,3}
slice := arr[0:2]
newSlice := append(slice, 50)
fmt.Printf("arrPoint = %p, slicePointer = %p, newSlicePoint = %p \n", &arr, &slice, &newSlice)
fmt.Println("=============================================")
}
结果是
// 利用array新生成的slice, 以及利用slice append操作生成的newSlice 地址都是不一样的。
arrPoint = 0xc42006caa0, slicePointer = 0xc42006cac0, newSlicePoint = 0xc42006cae0
2.2.2 append之后,底层数组变了吗?
那slice底层的数组会变吗?先看下面的例子
func TestSliceAppend_ArrayAddr(t *testing.T) {
arr := [3]int{1,2,3}
slice := arr[0:2]
newSlice := append(slice, 50)
fmt.Printf("arrPoint = %p, slicePointer = %p, newSlicePoint = %p \n", &arr, &slice, &newSlice)
fmt.Printf("arrPoint = %p, slice array Pointer = %p, newSlice array Point = %p \n", &arr[0], &slice[0], &newSlice[0])
fmt.Println("=============================================")
}
结果是
arrPoint = 0xc42000ac00, slicePointer = 0xc42000ac20, newSlicePoint = 0xc42000ac40
arrPoint = 0xc42000ac00, slice array Pointer = 0xc42000ac00, newSlice array Point = 0xc42000ac00
我们看到,底层数组的地址是一样的。说明两个slice 共用了底层的array。
这是一定的吗?再看个例子:
func TestSliceAppend_newArray(t *testing.T) {
fmt.Println("array 容量还够,则复用")
arr := [3]int{1,2,3}
slice := arr[0:3]
newSlice := append(slice, 50)
fmt.Printf("arrPoint = %p, slicePointer = %p, newSlicePoint = %p \n", &arr[0], &slice[0], &newSlice[0])
fmt.Println("=============================================")
}
结论是这样的
arrPoint = 0xc42000ac00, slice array Pointer = 0xc42000ac00, newSlice array Point = 0xc42000ac00
我们发现第一个slice的首元素地址跟数组首元素地址相同,但是newSlice就不同了。为什么呢?
这是因为原来底层数组的容量已经不够了,这是会新分配一段新内存,这样就跟原来的内存不一样的地址了
2.2.3 append如果底层数组的扩容,一般会扩容多大?
其实反汇编2.2.2 中发生底层数组扩容的代码,会发现发生扩容时调用的底层函数是growslice。
那么我们就直接总结一下结论:(如果对源码感兴趣,可以看一下 go/src/runtime/slice.go#L76 growslice 的定义)
当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。[里面会涉及到内存对齐的问题,如果涉及内存对齐,就不会这些倍数了]
2.3 slice作为参数
本小结主要是讲一点,slice不管是以其自身还是以其指针作为参数 传递给函数,都会进行值拷贝。所以我们建议使用指针。为什么呢?节省内存啊。如果一个slice大小1G,如果用其自身传递,那么就得拷贝1G;而用指针则只需要拷贝一个指针就OK了。
2.3.1 使用slice自身作为参数
使用slice自身作为参数的例子:
func TestParamValueOrPoint(t *testing.T) {
arrayA := [2]int{100, 200}
sliceA := arrayA[:]
fmt.Printf("sliceA : %p , %v\n", &sliceA, sliceA)
testSlice(sliceA)
fmt.Println("---------------------------")
}
func testSlice(x []int) {
fmt.Printf("func Slice : %p , %v\n", &x, x)
}
结果是:
sliceA : 0xc42000abc0 , [100 200]
func Slice : 0xc42000ac00 , [100 200]
我们发现进入testSlice函数之后获取的参数地址,已经不是之前的slice地址了。
2.3.2 使用slice的指针作为参数
例子:
func testSlicePoint(x *[]int) {
fmt.Printf("func Slice : %p , %v\n", x, *x)
(*x)[1] += 100
}
func testArrayPoint(x *[2]int) {
fmt.Printf("func Array : %p , %v\n", x, *x)
(*x)[1] += 100
}
func TestParamUsePointer(t *testing.T) {
fmt.Println("验证 Go 中 数组跟slice都是用其指针作为参数传递")
arrayA := [2]int{100, 200}
testArrayPoint(&arrayA) // 1.传数组指针
sliceB := arrayA[:]
testSlicePoint(&sliceB) // 2.传切片
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("sliceB : %p , %v\n", &sliceB, sliceB)
}
结果:
func Array : 0xc420016f80 , [100 200]
func Slice : 0xc42000ac40 , [100 300]
arrayA : 0xc420016f80 , [100 400]
sliceB : 0xc42000ac40 , [100 400]
我们发现,此时是拷贝的地址,所以可以继续使用原来的slice。
2.4 for range
单独列出这部分来,想表达的是如果采用for range ,会对原slice的值进行复制,这样改变复制之后的值,对原slice是不会产生影响的。
举个例子:
func TestRange_noEffect(t *testing.T) {
b := []int{1, 2}
// 场景一 这个例子是为了测试for range. 说明for range 每次会将slice的内容copy给v, 所以v变化,也不会影响b
fmt.Println("场景一 无影响")
for _, v := range b {
v++
//fmt.Println(k, v)
}
for k, v := range b {
fmt.Println(k, v)
}
fmt.Println("=============================")
}
结果是
场景一 无影响
0 1
1 2
如果想产生影响呢?
func TestRange_haveEffect(t *testing.T) {
b := []int{1, 2}
// 场景二 要想产生实际影响,要这样做
fmt.Println("场景二 产生影响")
for k, v := range b {
b[k] = v + 1
}
for k, v := range b {
fmt.Println(k, v)
}
fmt.Println("=============================")
}
结果会是
场景二 产生影响
0 2
1 3
2.5 copy
copy的作用是将源slice copy给目的slice,类型必须一致。其底层只是内存的copy。
对应的函数是 slicecopy 。
来个例子吧,也没啥好讲的
func TestSliceCopy(t *testing.T) {
fmt.Println("slicecopy 方法最终的复制结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就全部完成了。")
array := []int{10, 20, 30, 40}
slice := make([]int, 6)
n := copy(slice, array)
fmt.Println(n,slice)
fmt.Println("=============================================")
fmt.Println("即使array2有7个元素,但是slice2只接收了6个。")
array2 := []int{10, 20, 30, 40, 50,60,70}
slice2 := make([]int, 6)
n2 := copy(slice2, array2)
fmt.Println(n2,slice2)
fmt.Println("=============================================")
}
3 总结
本文将自己在学习Go 数组跟slice的一些好文章进行了总结。从slice的定义、初始化、常用api这几块分析了一下,并举了一些典型的例子。有些地方提到了一点 go源码的函数。希望这篇文章对你有所帮助~
4 参考文献
golang中的slice https://www.jianshu.com/p/3273e9e32951
golang中的数组 https://www.jianshu.com/p/863ffd730cef
深度解密Go语言之Slice [https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA]
深入解析 Go 中 Slice 底层实现 (https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA)
https://halfrost.com/go_slice/
深入理解 Go Slice https://book.eddycjy.com/golang/slice/slice.html
数组和切片 https://draveness.me/golang/datastructure/golang-array-and-slice.html
go源码
5 其他
本文是《循序渐进go语言》的第八篇-《Go-数组与slice》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~