2.1 slice 底层结构

切片即动态数组,可以动态扩容改变数组的容量. golang 的 slice 底层结构如下所示,它是一个结构体,里面包含了指向数组的地址,并通过 len、cap 保存数组的元素数、容量:

type slice struct {
    array unsafe.Pointer // 指向数组的指针
    len   int // 切片中元素的数量
    cap   int // array 数组的总容量
}


2.2 切片 slice 赋值

切片 slice 的赋值操作是改变了 slice 内部结构的值. 所以赋值后改变的是指针 array 指向的地址、len 和 cap 值. 赋值操作的左、右俩个切片的 array 指向的是同一个数组,所以它们的数组中元素的值也会一起发生改变.


2.3 切片的拷贝

考虑到切片 slice 的结构,对于切片直接用 = 拷贝,实际上是浅拷贝,只是改变了指针的指向,并没有改变数组中元素的值. 对于深度拷贝的需求,可以借助 copy 内置函数完成. 两种拷贝的方式如下:

  • 深度拷贝: copy(sliceA, sliceB)

  • 浅拷贝: sliceA = sliceB

var s1 = []int{1, 2}        // 初始化一个切片
var s2 = make([]int, 2) // 初始化一个空的切片,cap为2
copy(s2, s1) // 将s1拷贝给s2
s2[0] = 99 // 改变s2[0]
fmt.Println(s1[0]) // 打印 1 而不是 99

2.3 切片 slice 函数传递

golang 函数的参数传递都是值传递,而 map、channel、slice 都是引用类型,会传递指针值. 但是,切片的结构及扩容机制特殊. 

在切片进行复制时,会将切片的值(指针、cap、len)复制了一份. 在函数内部可以改变原切片的值. 


但是,当涉及到 append 触发扩容时,原来的指针指向的地址会发生变化,之后再对数组值进行更改,原切片将不受影响.

//定义一个函数,给切片添加一个元素
func addOne(s []int) {
    s[0] = 4  // 可以改变原切片值
    s = append(s, 1)  // 扩容后分配了新的地址,原切片将不再受影响
    s[0] = 8
}
var s1 = []int{2} // 初始化一个切片
addOne(s1) // 调用函数添加一个切片
fmt.Println(s1)     // 输出一个值 [4]


2.4 切片 slice 的扩容

当使用 append(slice,data) 时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建一个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组。

其中新数组的长度一般是老数组的俩倍,当然,如果一直是俩倍增加,那也会极大的浪费内存. 所以在老数组长度大于 1024 时候,将每次按照不小于 25% 的涨幅扩容. 

slice 增加长度的源码在 src/runtime/slice.go 的 growslice 函数中.