开篇
大多数时候我们都忘记了或者压根不知道slice是怎么工作的。大多数时候我们只是把slice当做动态数组来用。通过这篇翻译的文章,重新认识slice,我们可以一定程度上避免掉入slice的陷阱,并且更好的使用它。
参考资料有:
Effective Go
Go Slices: usage and internal
本文重点是代码例子,边动手边学习
什么是数组
Go中的数组(array)是一个固定大小的、单一类型的一个序列。
创建数组需要两个参数:size和type。
Array的size是类型的一部分
x := [5]int{1, 2, 3}
y := [5]int{3, 2, 1}
z := [5]int{1, 2, 3}
fmt.Printf("x == y: %v\n", x == y)
fmt.Printf("x == z: %v\n", x == z)
in := [5]int{10, 20, 30}
fmt.Printf("contents > in:%v\n", in)
上面的x、y、z类型相同,可以比较。element的顺序相同就相等。
a := [4]int{1, 2, 3}
fmt.Printf("x == a: %v\n", x == a)
上面a和x类型不同,因为size一个是4,一个是5。它俩比较,会报编译错误
当然,Go的数组也不能越界访问。
Array的element会初始化为0
in := [5]int{10, 20, 30}
fmt.Printf("contents > in:%v\n", in)
https://golang.org/ref/spec#The_zero_value
Array是value(不是reference)
把数组赋值给另一个数组,或者把数组传递到函数中的一个参数,都会发生值拷贝。
func passArray(y [5]int) {
fmt.Printf("&y:%p\n", &y)
y[0] = 90
fmt.Printf("y: %v\n", y)
}
func main() {
x := [5]int{1, 2, 3}
fmt.Printf("&x:%p\n", &x)
passArray(&x)
fmt.Printf("x: %v\n", x)
}
如果我们想要函数能够修改它的参数的值,我们应该把数组的地址传进去,当然,函数的参数也应该变为数组的指针。
func passArray(y *[5]int) {
fmt.Printf("y:%p\n", y)
y[0] = 90
fmt.Printf("y: %v\n", y)
}
func main() {
x := [5]int{1, 2, 3}
fmt.Printf("&x:%p\n", &x)
passArray(&x)
fmt.Printf("x: %v\n", x)
}
什么是slice
在Go官方博客中,slice的定义是数组中一段的描述符。slice由一个数组的指针、数组段的长度和slice本身的容量三部分组成。
slice的底层存储
先用一段程序来看看slice和array的关系
func passSlice(xx []int) {
fmt.Printf("xx> &xx:%p &xx[0]:%p\n", &xx, &xx[0])
fmt.Printf("xx> len:%d cap:%d\n", len(xx), cap(xx))
}
func main() {
x := []int{1, 2, 3}
fmt.Printf("x> &x:%p &x[0]:%p\n", &x, &x[0])
fmt.Printf("x> len:%d cap:%d\n", len(x), cap(x))
passSlice(x)
}
这段程序我们先创建了一个slice:x。然后打印出来了它的地址、长度和容量。再将它传到了一个函数中,再次打印上述信息。输出如下:
x> &x:0x40a0e0 &x[0]:0x40e020
x> len:3 cap:3
xx> &xx:0x40a0f0 &xx[0]:0x40e020
xx> len:3 cap:3
发现了么?将slice直接传递给一个函数,函数参数是一个新创建的slice,但是slice内部的数据和原始的slice内部的数据还是同一个地址。这说明了x和xx共享了同一个内部数据(也就是同一个数组的同一段存储)
显然,当函数中的xx修改了元素,原来的x中的值也会被修改。这里经常会出现bug,因为多个slice共享同一份数组,可能会相互干扰。
另一个有趣的地方,既然slice会共享底层存储,那么当我们对某一个slice进行append操作,会发生什么?
func sliceAppend(xx []int) {
xx = append(xx, 4)
}
func main() {
x := []int{1, 2, 3}
sliceAppend(x)
fmt.Printf("%v\n", x)
}
如果你认为输出是[1,2,3,4],那就错了,程序输出还是[1,2,3]。到底发生了什么?通过下面的代码看一下地址就一清二楚了。
func sliceAppendAddress(xx []int) {
fmt.Printf("xx before > &[0]:%p len:%d cap:%d\n", &xx[0], len(xx), cap(xx))
xx = append(xx, 4)
fmt.Printf("xx after > &[0]:%p len:%d cap:%d\n", &xx[0], len(xx), cap(xx))
}
func main() {
x := []int{1, 2, 3}
fmt.Printf("x before > &[0]:%p len:%d cap:%d\n", &x[0], len(x), cap(x))
sliceAppendAddress(x)
fmt.Printf("x after > &[0]:%p len:%d cap:%d\n", &x[0], len(x), cap(x))
}
从上面的输出可以很清楚的看到,在append之后,xx的地址、容量都发生了变化,这些变化并没有影响到原来的x。这个例子很好理解,函数中的xx在append的时候容量不够了,发生了reallocate,这时Go会为它重新创建一个底层存储(也就是一个数组)。
如果,容量足够,会发生什么?我们来看下面的例子
func sliceAppend(xx []int) {
fmt.Printf("xx before > len:%d cap:%d\n", len(xx), cap(xx))
xx = append(xx, 4)
fmt.Printf("xx after > len:%d cap:%d\n", len(xx), cap(xx))
}
func main() {
x := make([]int, 0, 5)
x = append(x, 1, 2, 3)
fmt.Printf("x before > len:%d cap:%d\n", len(x), cap(x))
sliceAppend(x)
fmt.Printf("x after > %v\n", x)
}
为什么?从容量来看,这里是不会发生reallocate的,可是为什么原来的x还是没有发生变化呢?实际上,这是因为x受到了自己len的限制。我们只要扩展一下它的长度就行了:
func sliceAppend(xx []int) {
fmt.Printf("xx before > len:%d cap:%d\n", len(xx), cap(xx))
xx = append(xx, 4)
fmt.Printf("xx after > len:%d cap:%d\n", len(xx), cap(xx))
}
func main() {
x := make([]int, 0, 5)
x = append(x, 1, 2, 3)
fmt.Printf("x before > len:%d cap:%d\n", len(x), cap(x))
sliceAppend(x)
x = x[:4]
fmt.Printf("x after > %v\n", x)
fmt.Printf("x after > len:%d cap:%d\n", len(x), cap(x))
}
总结一下从这几个例子我们学到了什么
slice是值传递,也就是函数参数会复制一个slice(数组指针、len、cap)
只要底层存储没有变化,对函数接收的slice的element修改,会影响到原始的slice
如果在函数中发生了reallocate,也就是说底层存储发生了变化,那么receiver和caller的slice不会相互影响
对切片进行切片
先看下面一段程序
s := []int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("s > len:%d cap:%d\n", len(s), cap(s))
ss := s[2:4]
fmt.Printf("ss> len:%d cap:%d\n", len(ss), cap(ss))
对slice进行切片,语法是
第high个元素是不包含在新的切片中的。所以:
len(newSlice) : high-low
cap(newSlice) : cap(s)-low
如果想指定新切片的最大坐标,还可以这样写
newSlice := s[low:high:max]
注意这种写法不适用于string,此时新切片的cap是max-low
slice还有一个常见的操作,就是对slice进行切片(slicing a slice)
s := []int{10, 20, 30, 40, 50, 60, 70}
fmt.Printf(" &s:%p &s[2]:%p\n", &s, &s[2])
ss := s[2:4]
fmt.Printf("&ss:%p &ss[0]:%p\n", &ss, &ss[0])
从上面的结果可以看出,sub-slice和原来的slice是共享的相同的底层存储(数组)。那么显然,对sub-slice的修改,也会影响到原有的slice。
清空slice
将一个slice清空的最佳方法是什么呢?直觉上有两种方法
我们先来看看s=nil
s := []int{1, 2, 3}
fmt.Printf("s> len:%d cap:%d &[0]:%p\n", len(s), cap(s), &s[0])
s = nil
fmt.Printf("s> len:%d cap:%d\n", len(s), cap(s))
s = append(s, 4)
fmt.Printf("s> len:%d cap:%d &[0]:%p\n", len(s), cap(s), &s[0])
s=nil之后,s的指针、len、cap全部清零,append之后开辟了新的存储空间(数组)
现在来看第二种方法s=[:0]
s := []int{1, 2, 3}
fmt.Printf("s> len:%d cap:%d &[0]:%p\n", len(s), cap(s), &s[0])
s = s[:0]
fmt.Printf("s> len:%d cap:%d\n", len(s), cap(s))
s = append(s, 4)
fmt.Printf("s> len:%d cap:%d &[0]:%p\n", len(s), cap(s), &s[0])
fmt.Printf(“s> %v\n”, s)
这种情况下,s只是清空了len和cap,底层存储数组的指针还保留,所以append之后还是原来的地址。
综上可以这么说,s=nil是一种类似release的操作,s=s[:0]只是清空数据。
Slice的吊诡之处
下面分析一下slice的常见问题
太多的reallcation
连续的多次append()可能会造成reallcation
func doX(in []int) (out []int){
for _, v := range in {
fmt.Printf("before> out len:%d cap:%d\n", len(out), cap(out))
out = append(out, v)
fmt.Printf("after > out len:%d cap:%d\n", len(out), cap(out))
}
return out
}
doX([]int{1,2,3,4,5})
--------------------------
before> out len:0 cap:0
after > out len:1 cap:1
before> out len:1 cap:1
after > out len:2 cap:2
before> out len:2 cap:2
after > out len:3 cap:4
before> out len:3 cap:4
after > out len:4 cap:4
before> out len:4 cap:4
after > out len:5 cap:8
观察这里的cap,变化了4次,也就是发生了4次的reallocation。
如果提前进行一些内存分配,就不会有这样的情况了。
func doX(in []int) (out []int){
out = make([]int, 0, len(in))
for _, v := range in {
out = append(out, v)
}
return out
}
doX([]int{1,2,3,4,5})
-------------------------------
before> out len:0 cap:5
after > out len:1 cap:5
before> out len:1 cap:5
after > out len:2 cap:5
before> out len:2 cap:5
after > out len:3 cap:5
before> out len:3 cap:5
after > out len:4 cap:5
before> out len:4 cap:5
after > out len:5 cap:5
可以通过下面的工具来针对这个问题进行静态分析:https://github.com/alexkohler/prealloc
没有释放内存
在re-slicing的时候,并不会复制一份内存,所以整个数组的内存都会因为slicing出来的切片而保留,直到slicing被nil-ed才会释放。
理论上这个并不算『内存泄露』,因为那段内存确实还在使用,只不过只是一小部分。
这个目前没有好用的静态分析工具。
参考资料
https://golang.org/doc/effective_go.html
https://blog.golang.org/go-slices-usage-and-internals
评论