Go 语言里的 slice 切片
变量的 pass-by-value
Go 语言里的切片 slice,主要是针对数组类型不够灵活的问题。比如,a [50]int
和 b [80]int
是不同的类型,但同一个 slice s []int
可以指向这些不同的类型,所以在参数设计时更灵活;比如 var a [50]int
,在传值的时候,在 64 位 CPU 的架构上整个数组的 400 个字节都会被拷贝,通常这是没有必要的。
slice 结构,有三个成员变量,包括指向底层数组某个偏移位置的指针 array
、切片元素的个数 len
、切片可以存放的元素个数的上限 cap
(都是小写字母开头,所以看看就好了 :D):
type slice struct {
array unsafe.Pointer
len int
cap int
}
<< Effective Go >> 里有一句话是:..., the slice itself (the run-time data structure holding the pointer, length, and capacity) is passed by value.
所以每次 slice 在传递的时候都是传值的,从定义的结构可以看到,会拷贝 24 个字节。
slice 指向的内容
var a [50]int
var s []int
s = a[20:35] // 指向了数组的一个区间,范围包括第 20、21、……、34 个元素
// 检查 len 和 cap
fmt.Printf("sizeof(s) = %d, len:%d, cap:%d\n", unsafe.Sizeof(s), len(s), cap(s))
// output:
// sizeof(s) = 24, len:15, cap:30
slice 的传递
var a [50]int
var s1, s2 []int
s1 = a[20:35]
s2 = s1
考虑两个 slice,s0 赋值给 s1。这时,它们指向了相同的底层数组,并且有相同的 len 和 cap。可以这样理解,目前 s1 和 s2 看底层数组的角度是一样的。
通过 s1 进行的操作,对 s2 是否可见呢?这要取决于这个操作是否在 s2 的角度范围内,比如:
- 通过 s1 作用在第 0、1、len-1 个元素上,那么对于 s2 是可见的;
- 给 s1 追加一个元素,那对 s2 是不可见的,因为在 s2 的 len 范围之外了;
- s1 指向了(比如追加若干元素后,或者显式地更新指向)新的底层数组,那对 s2 肯定是不可见的,因为 s2 看到的仍然是旧的底层数组;
- ……
所以,caller 在向 callee 在传递 slice 时,一定要考虑 callee 的实现里对 slice 操作,如果有影响,可能需要 callee 把更新过的 slice 返回给 caller。比如,append
:
s1 = append(s1, 1)
s1 = append(s1, 2)
s1 = append(s1, 3)
如果底层数组还有空间,那么 apped 返回后的 s1 相比原来只更新了 len;如果底层数组不够用了,也就是 len(s1)
达到 cap(s1)
了,那么 go 运行时会重新分配一个更大的底层数组、拷贝原有数据、并返回,这样 s1 的 array、len、cap 都会更新。当然,对于旧的底层数组,如果不再被使用到,就会交给 gc 处理。
slice 的深拷贝
以上的传值过程,可以理解成是浅拷贝。类似于 C 语言里的指针或者字符串的传递,拷贝的是地址,而不是地址所指向的一片内存的数据。
对于深拷贝,一种办法是 copy
,即分配新的内存,并拷贝数据进去:
s1 := []int{1, 2, 3, 4, 5}
s2 := make([]int, len(s1))
copy(s2, s1)
还有一种是 append
,把 s1 里的内容,往一个空的 slice 里追加,返回新分配的结果:
s1 := []int{1, 2, 3, 4, 5}
s2 = append([]int(nil), s1...)