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) 
fmt.Println(slice1[2]) 

看似数组和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本身也就是一个数组,只是增加了长度和容量属性。
slice.png
由上图可以看到,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 已经完全不同了,其拥有了不同的 arraylen 还有 cap 。因此在第 11 行打印的地址是新的,并且扩容了 2 倍。
第5行 再看 arr 的地址,其实并没有变化,因为在第10行新创建的 slice 并没有返回给 main 中的 arr ,因此 arr 还是原来的 arr
第6行 出现了第0个元素的值在change函数中进行修改的值10,那是因为在第9行的arr1的数组是引用类型,在这里对arr[0]的修改当然会反应到main中的arr了,但是一定要记住,lencap不是引用类型,仅仅是简单的赋值操作,所以如果在change函数中不返回新创建的slicemain中的arr是永远不可能获得lencapappend函数明显就是返回了这样的slice
因为,在涉及到函数中对slice进行操作的话,尽可能地返回slice,以免造成意想不到的bug。

另外值得说明的是,
如果append后依然没有超过arr1的容量,那么将会在原来的数组上进行append,具体来讲,就是上图中左边的slice进行append 20后,第4个位置的4将会被替换成20,也会影响到右边的slice,因为他们之间是有交集的。无论这是发生在main中,还是change函数中,都一样。

当你学会了这些,你就能熟练使用slice不用担心出bug啦


thomaston
82 声望8 粉丝

everything is ok