1. 切片究竟是什么
Go 语言数组是一个固定长度的、容纳同构类型元素的连续序列。因此 Go 数组类型具有两个属性:元素类型和数组长度。但凡这两个属性相同的数组类型是等价的。比如下面变量 a、b、c 对应的数组类型是三个不同的数组类型:
var a [8]intvar b [8]bytevar c [9]int
切片之于数组就像是文件描述符之于文件。在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色;而切片则走向“前台”,为底层的存储(数组)打开了一个访问的“窗口”。

下面是切片在 Go 运行时(runtime)层面的内部表示:
//$GOROOT/src/runtime/slice.gotype slice struct {array unsafe.Pointerlen intcap int}
在运行时中,每个切片变量都是一个 runtime.slice 结构体的实例。我们用下面语句创建一个 slice 实例:
s := make([]byte, 5)

我们还可以创建对已存在数组进行操作的切片,这种语法 u[low:max] 称为数组的切片化:
u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}s := u[3:7]

我们可以为一个已存在数组建立多个操作数组的切片。

切片可以提供比指针更为强大的功能,比如下标访问、边界溢出校验、动态扩容等。
2. 切片的高级特性:动态扩容
切片类型是“部分”满足零值可用理念的,即零值切片也可以通过 append 预定义函数进行元素赋值操作:
var s []byte // s被赋予零值nils = append(s, 1)
新数组建立后,append 会把旧数组中的数据 copy 到新数组中,之后新数组便成为了 slice 的底层数组,旧数组会被垃圾回收掉。
// slice_append.govar s []int // s被赋予零值nils = append(s, 11)fmt.Println(len(s), cap(s)) //1 1s = append(s, 12)fmt.Println(len(s), cap(s)) //2 2s = append(s, 13)fmt.Println(len(s), cap(s)) //3 4s = append(s, 14)fmt.Println(len(s), cap(s)) //4 4s = append(s, 15)fmt.Println(len(s), cap(s)) //5 8
append 操作有些时候会给 Gopher 带来一些困惑,比如通过语法 u[low:max] 形式进行数组切片化而创建的切片,一旦切片 cap 触碰到数组的上界,再对切片进行 append,切片就会和原数组的解除“绑定”:
// slice_unbind_orig_array.gopackage mainimport "fmt"func main() {u := []int{11, 12, 13, 14, 15}fmt.Println("array:", u) // [11, 12, 13, 14, 15]s := u[1:3]fmt.Printf("slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // [12, 13]s = append(s, 24)fmt.Println("after append 24, array:", u)fmt.Printf("after append 24, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)s = append(s, 25)fmt.Println("after append 25, array:", u)fmt.Printf("after append 25, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)s = append(s, 26)fmt.Println("after append 26, array:", u)fmt.Printf("after append 26, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)s[0] = 22fmt.Println("after reassign 1st elem of slice, array:", u)fmt.Printf("after reassign 1st elem of slice, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)}
运行这段代码,我们得到如下结果:
$go run slice_unbind_orig_array.goarray: [11 12 13 14 15]slice(len=2, cap=4): [12 13]after append 24, array: [11 12 13 24 15]after append 24, slice(len=3, cap=4): [12 13 24]after append 25, array: [11 12 13 24 25]after append 25, slice(len=4, cap=4): [12 13 24 25]after append 26, array: [11 12 13 24 25]after append 26, slice(len=5, cap=8): [12 13 24 25 26]after reassign 1st elem of slice, array: [11 12 13 24 25]after reassign 1st elem of slice, slice(len=5, cap=8): [22 13 24 25 26]
3. 尽量使用 cap 参数创建 slice
如何减少或避免为过多内存分配和拷贝付出的代价呢?一种有效的方法就是根据 slice 的使用场景在为新创建的 slice 赋初值时使用 cap 参数。
s := make([]T, 0, cap)
下面是一个使用 cap 和不使用 cap 参数的 slice 的基准测试:
// slice_benchmark_test.gopackage mainimport "testing"const sliceSize = 10000func BenchmarkSliceInitWithoutCap(b *testing.B) {for n := 0; n < b.N; n++ {sl := make([]int, 0)for i := 0; i < sliceSize; i++ {sl = append(sl, i)}}}func BenchmarkSliceInitWithCap(b *testing.B) {for n := 0; n < b.N; n++ {sl := make([]int, 0, sliceSize)for i := 0; i < sliceSize; i++ {sl = append(sl, i)}}}
下面是基本测试运行的结果(go 1.12.7,macbook pro i5 8 核,16G 内存):
$go test -benchmem -bench=. slice_benchmark_test.gogoos: darwingoarch: amd64BenchmarkSliceInitWithoutCap-8 50000 36484 ns/op 386297 B/op 20 allocs/opBenchmarkSliceInitWithCap-8 200000 9250 ns/op 81920 B/op 1 allocs/opPASSok command-line-arguments 4.163s
