[toc]
slice 的 length 和 capacity
1 | s := make([]int, 3, 6) |
在 make 函数里面,capacity 是可选的参数。上面这段代码我们创建了一个 length 是 3,capacity 是 6 的 slice,那么底层的数据结构是这样的:
slice 的底层实际上指向了一个数组。当然,由于我们的 length 是 3,所以这样设置 s[4] = 0 会 panic 的。需要使用 append 才能添加新元素。
1 | panic: runtime error: index out of range [4] with length 3 |
当 appned 超过 cap 大小的时候,slice 会自动帮我们扩容,在元素数量小于 1024 的时候每次会扩大一倍,当超过了 1024 个元素每次扩大 25%。
有时候我们会使用 :操作符从另一个 slice 上面创建一个新切片:
1 | s1 := make([]int, 3, 6) |
实际上这两个 slice 还是指向了底层同样的数组,构如下:
由于指向了同一个数组,那么当我们改变第一个槽位的时候,比如 s1[1]=2,实际上两个 slice 的数据都会发生改变:
但是当我们使用 append 的时候情况会有所不同:
1 | s2 = append(s2, 3) |
s1 的 len 并没有被改变,所以看到的还是3元素。
还有一件比较有趣的细节是,如果再接着 append s1 那么第四个元素会被覆盖掉:
1 | s1 = append(s1, 4) |
再继续 append s2 直到 s2 发生扩容,这个时候会发现 s2 实际上和 s1 指向的不是同一个数组了:
1 | s2 = append(s2, 5, 6, 7) |
除了上面这种情况,还有一种情况 append 会产生意想不到的效果:
1 | s1 := []int{1, 2, 3} |
如果 print 它们应该是这样:
1 | s1=[1 2 10], s2=[2], s3=[2 10] |
slice 初始化
slice 的初始化有很多种方式:
1 | func main() { |
输出:
1 | 1: empty=true nil=true |
前两种方式会创建一个 nil 的 slice,后两种会进行初始化,并且这些 slice 的大小都为 0 。
对于 var s []string 这种方式来说,好处就是不用做任何的内存分配。比如下面场景可能可以节省一次内存分配:
1 | func f() []string { |
对于 s := []string{} 这种方式来说,它比较适合初始化一个已知元素的 slice:
1 | s := []string{"foo", "bar", "baz"} |
如果没有这个需求其实用 var s []string 比较好,反正在使用的适合都是通过 append 添加元素, var s []string 还能节省一次内存分配。
如果我们初始化了一个空的 slice, 那么最好是使用 len(xxx) == 0来判断 slice 是不是空的,如果使用 nil 来判断可能会永远非空的情况,因为对于 s := []string{} 和 s = make([]string, 0) 这两种初始化都是非 nil 的。
对于 []string(nil) 这种初始化的方式,使用场景很少,一种比较方便地使用场景是用它来进行 slice 的 copy:
1 | src := []int{0, 1, 2} |
对于 make 来说,它可以初始化 slice 的 length 和 capacity,如果我们能确定 slice 里面会存放多少元素,从性能的角度考虑最好使用 make 初始化好,因为对于一个空的 slice append 元素进去每次达到阈值都需要进行扩容,下面是填充 100 万元素的 benchmark:
1 | BenchmarkConvert_EmptySlice-4 22 49739882 ns/op |
可以看到,如果我们提前填充好 slice 的容量大小,性能是空 slice 的四倍,因为少了扩容时元素复制以及重新申请新数组的开销。
copy slice
1 | src := []int{0, 1, 2} |
使用 copy 函数 copy slice 的时候需要注意,上面这种情况实际上会 copy 失败,因为对 slice 来说是由 length 来控制可用数据,copy 并没有复制这个字段,要想 copy 我们可以这么做:
1 | src := []int{0, 1, 2} |
除此之外也可以用上面提到的:
1 | src := []int{0, 1, 2} |
slice capacity内存释放问题
1 | type Foo struct { |
上面这个例子中使用 printAlloc 函数来打印内存占用:
1 | func printAlloc() { |
上面 foos 初始化了 1000 个容量的 slice ,里面 Foo struct 每个都持有 1M 内存的 slice,然后通过 keepFirstTwoElementsOnly 返回持有前两个元素的 Foo 切片,我们的想法是手动执行 GC 之后其他的 998 个 Foo 会被 GC 销毁,但是输出结果如下:
1 | 387 KB |
实际上并没有,原因就是实际上 keepFirstTwoElementsOnly 返回的 slice 底层持有的数组是和 foos 持有的同一个:
所以我们真的要只返回 slice 的前2个元素的话应该这样做:
1 | func keepFirstTwoElementsOnly(foos []Foo) []Foo { |
不过上面这种方法会初始化一个新的 slice,然后将两个元素 copy 过去。不想进行多余的分配可以这么做:
1 | func keepFirstTwoElementsOnly(foos []Foo) []Foo { |