1.数组是什么,slice是什么
在 golang
中,我们可以像C语言一样创建一个数组,也可以创建一个动态数组 (slice
)
数组:
arr := [2]int{1, 2}
arr[0]=3
arr[1]=4
fmt.Println(arr) // output:[3,4]
此时我们创建了一个包含2个元素的数组,[]中只能是常量,因为数组在创建的时候必须是确定的。
slice:slice
这个对象在 golang
中是一个比较特殊的存在,从不同的角度观察,有时像引用类型,有时又不像,具体是什么情况呢?下面会说到。
slice1 := make([]int, 2, 4)
slice1 = append(slice1, 6)
fmt.Println(slice1) // output:[2,4,6]
fmt.Println(slice1[2]) // output:6
看似数组和slice的区别不大,实际上很不一样。
数组是不能使用append的,slice是可以通过append动态增加长度。
2.slice与数组的关系
slice的数据结构是这样的
type slice struct {
array unsafe.Pointer
len int
cap int
}
这是一个典型的结构体,其中第一个字段就是数组,类型是unsafe.Pointer。在此题外话一下,简单介绍下unsafe.Pointer。
此类型和C语言中常用的void*有点像,可以通过unsafe.Pointer和其它任意类型的指针相互转换,因为在golang中不同的类型之间是不能随意转换的,必须要有中间的unsafe.Pointer作为过渡,例如
var a int = 1
var b *uint64 = (*uint64)((unsafe.Pointer)(&a))
否则就会报错,类型转换失败。好了,到目前为止我们知道了这个array的类型其实就是一个指针类型,和C语言其实类似。
3.从数组中获取slice
回到slice本身,我们看到其包含一个array,是个引用类型,所以实际上,slice本身也就是一个数组,只是增加了长度和容量属性。
由上图可以看到,array是基准的数组,在这个数组上取2个slice,左边的slice从第1和开始到第2个结束,容量是3(4表示原始数组的第6位),因为左边的slice只有1个空余的位置可以插入元素,再多一个就会超过容量3,所以此时该slice不会再引用这个数组,而是重新开辟一个新的数组,数组的长度是4,容量会是2倍,即cap=6。同理,右边的slice也一样。
右边的slice是一个基于改数组的从4到5,长度是2,容量是3的slice(6表示原始数组的第6位)
4.理解append,如何避免掉坑
让人迷惑的操作?
slice虽然是个引用类型,但是如果像这样传参
1:main() {
2: arr := []int{1,2,3,4,5,6}
3: fmt.Printf("%p\n",arr)
4: change(arr)
5: fmt.Printf("%p\n",arr)
6: fmt.Printf("%p,arr:%v,len:%v,cap:%v\n",&arr[0],arr,len(arr),cap(arr))
7: }
8: func change(arr1 []int) {
9: arr1[0]=10
10: arr1=append(arr1,20)
11: fmt.Printf("%p,arr1:%v,len:%v,cap:%v\n",&arr1[0],arr1,len(arr1),cap(arr1))
12: return
13:}
上面的输出将会是:
0xc00001a150
0xc00005a060,arr1:[10 2 3 4 5 6 20],len:7,cap:12
0xc00001a150
0xc00001a150,arr:[10 2 3 4 5 6],len:6,cap:6
在解释之前,我们要说明的是每个 slice
用 %p
打印地址的时候,实际上打印的是其中的 array
地址,这在 go
的内部对 slice
是有特殊处理的。
解释一下这个过程:
第3行 打印出arr这个引用的地址
第9行 的 arr1
这个 slice
的地址依然和 arr
是一样的,因为 arr
把其 array
引用和 len
, cap
一并赋值给 arr1
,所以第 9 行对索引为 0 的元素修改是有效的。
第10行 对 arr1
进行了 append
操作,由于此时的 cap
已经用完了,所以会另外创建一个全新的 slice
,其绑定的数组也是新创建的,因此此时返回的 arr1
已经完全不同了,其拥有了不同的 array
和 len
还有 cap
。因此在第 11 行打印的地址是新的,并且扩容了 2 倍。
第5行 再看 arr
的地址,其实并没有变化,因为在第10行新创建的 slice
并没有返回给 main
中的 arr
,因此 arr
还是原来的 arr
。
第6行 出现了第0个元素的值在change函数中进行修改的值10,那是因为在第9行的arr1
的数组是引用类型,在这里对arr[0]
的修改当然会反应到main
中的arr
了,但是一定要记住,len
和cap
不是引用类型,仅仅是简单的赋值操作,所以如果在change
函数中不返回新创建的slice
,main
中的arr
是永远不可能获得len
和cap
。append
函数明显就是返回了这样的slice
。
因为,在涉及到函数中对slice
进行操作的话,尽可能地返回slice
,以免造成意想不到的bug。
另外值得说明的是,
如果append
后依然没有超过arr1
的容量,那么将会在原来的数组上进行append
,具体来讲,就是上图中左边的slice
进行append 20
后,第4个位置的4将会被替换成20,也会影响到右边的slice
,因为他们之间是有交集的。无论这是发生在main
中,还是change
函数中,都一样。
当你学会了这些,你就能熟练使用slice不用担心出bug啦
有疑问加站长微信联系(非本文作者)