go slice
go 学习笔记一 —— slice 和 append
go 里面很重要的两个概念是 slice 和 array。
一般来说,slice 较 array 更常被使用,搞清楚这两种的区别,以及一些基本操作很有必要。
[TOC]
slice initialization
1 | // slice initialization |
第一种 make 方法,可以指定底层数组的 len 和 cap,并用 slice 类型的默认值进行初始化。
第二种方法,是比较简洁的初始化方法,可以指定初始值,且其 len 和 cap 比较直观。
虽然本文不对 array 进行过多讨论,但其初始化方法与 slice 进行对比以便理解,还是很有必要的。
array initialization
array 是 slice 的底层数据结构,可以和 c 里面的数组类比理解。
1 | // array initialization |
array 初始化与 slice 最大的区别就是指定了长度。第三种方法本质上长度还是固定的,只不过是根据后面的初始化列表推断的。
一种简单的判别 array 和 slice 的方法就是看初始化的时候有没有固定长度。
append
append 是 操作 slice 最常用的操作,也是本文的重点。
normal usage
1 | // normal usage |
append 扩容
1 | var a []int |
首先, append 是会改变 slice 底层数组, 当底层数组容量不够 append 后的长度时,会触发扩容机制: 申请更大的一个数组,并执行拷贝。
1 | // grow grows the slice s so that it can hold extra more values, allocating |
数组容量扩充的代码如上所示。
简单来说,原数组容量为0则最终容量等于扩充后的长度;小于1024的时候成倍增长,不小于1024时以25%的速率上升到不小于扩充后长度。
append 的具体实现
首先,得提一下 go 是没有范型支持,在这个前提上为什么 append 可以传入任何类型的 slice 并正确运行呢?
1 | func append(slice []Type, elems ...Type) []Type |
append 的接口定义如上所示,那么 Type 是什么为什么可以接受所有类型呢?是 interface{} 嘛?
其实答案也很简单,并不是。因为 go 没有范型支持,所以 append 是由编译器进行支持的。
其次,想弄明白 append 具体做了什么, 要提到一个小问题:回答下面程序的输出结果。
1 | a := make([]int, 5, 10) |
如果你最后搞明白了这个程序的结果,那你就完全明白 slice 和 append 了。
首先, slice 相对于 C 里面的指针,其多了两个功能:当前数组的长度和最大容量。
第一步,用 slice a 初始化 b,此时 b 的 len、 cap 和指针都和 a 相同。
第二步,对 slice a 进行 append,底层数组的第5-7位(从0计数)被填充对应的值。此时 slice b 三个数值没有任何变化,即还是指向了和 slice a 一样的空间。
第三步,对 slice b 进行 append,底层数组的第5-6位被填充对应的值。
此时 slice a 和 b 的底层数组的指针还是一样的,即指向了同一片空间。所以输出结果乍一看有点反常理,其实理解了 slice 的机制也很容易想明白。
1 | // answer |
以下是从 blog 中找到了 go 1.13 的 具体实现,可以与上面的进行印证。
1 | func Append(s Value, x ...Value) Value { |
2021.03.11 补充
前段时间,同事问了一个问题:
1 | a := []int{1} |
答案为什么是
1 | len: 2, cap: 2 |
这个问题我们研究了好久,也没有得出最后的结论。
而如果分开 append, 则会出现预期的 cap 为 8 的结果。
查阅了 go 的相关源码并没有发现相关的细节。留个坑以后有空了汇编读一下,看看能不能找到具体的原因。