前言
如果我们需要传递一个 slice,一定要传递它的指针,这是因为 go 中只有值传递,如果不这么做会带来意想不到的副作用
SliceHeader
如果你学过 Rust 就会发现 Go 中 slice 的内存布局和 Rust 的 Vec
比较类似,它的结构如下:
type SliceHeader struct {
Data uintptr // 指向底层数组
Len int // 长度
Cap int // 容量
}
当我们向函数传递 slice 时,出于性能考虑,不可能复制它的底层数组,而是复制 SliceHeader
。问题就在这里,给 slice 增加新的元素有可能导致重新分配内存(这取决于当前 Cap
的大小),所以 Data
可能会变化,而已经传过去的 slice 会访问原来的指针。注意,这并不是未定义行为,因为仍有对象在引用之前那块内存,所以没有被释放。
验证
我们写个简单的例子,做法是先将 slice 传递给一个新的 goroutine,但等到给原来的 slice 添加新元素之后才打印它的值。如果之前的推理正确,那么程序的输出必须符合以下说法的其中之一:
- 如果
添加的元素数量 + Len <= Cap
,即不需要重新分配内存,那么延迟打印的数组同样改变 - 如果
添加的元素数量 + Len > Cap
,即重新分配内存了,那么延迟打印的数组不变
package main
import (
"fmt"
"reflect"
"unsafe"
)
// 输出 slice 的 SliceHeader 信息
func printSliceInfo(arr *[]int, from string) {
u := (*reflect.SliceHeader)(unsafe.Pointer(arr))
fmt.Printf("%s: {\n\tData: 0x%x\n\tLen: %d\n\tCap: %d\n}\n", from, u.Data, u.Len, u.Cap)
}
// 延迟输出
func printLater(arr []int, done chan<- bool, after <-chan bool) {
<-after // 阻塞,等待添加新元素完成
printSliceInfo(&arr, "Later")
fmt.Println(arr)
done <- true
}
func main() {
done := make(chan bool)
after := make(chan bool)
arr := make([]int, 2, 3) // 初始化一个长度为 2,容量为 3 的 slice
arr[0] = 1
arr[1] = 2
printSliceInfo(&arr, "original")
fmt.Println(arr)
go printLater(arr, done, after)
arr = append(arr, 3)
printSliceInfo(&arr, "changed")
fmt.Println(arr)
after <- true
<-done
}
输出如下:
origin: {
Data: 0xc00000a480
Len: 2
Cap: 5
}
[1 2]
changed: {
Data: 0xc00000a480
Len: 3
Cap: 5
}
[1 2 3]
Later: {
Data: 0xc00000a480
Len: 2
Cap: 5
}
[1 2]
结果让人匪夷所思,虽然添加了一个元素,但是总长没有超过 Cap
, 没有重新分配内存,所以 Data
指针没变,而延迟输出的数组却看不到被添加的值。
原因在于,复制过去的 SliceHeader
中的 Len
依然为 2
,没有更新!,那么修改就很简单了
func printLater(arr []int, done chan<- bool) {
dur := time.NewTimer(100 * time.Millisecond)
<-dur.C
printSliceInfo(&arr, "Later")
//fmt.Println(arr)
u := (*reflect.SliceHeader)(unsafe.Pointer(&arr))
fmt.Println(*(*[3]int)(unsafe.Pointer(u.Data)))
done <- true
}
拿到裸指针 Data
,直接输出长度为 3
的数组就可以了。(或者 u.Len = 3
)
很显然,如果传递的是 *[]int
,也就是传递 SliceHeader
的指针,我们就无需关心 slice 的内部结构了。
PS
其实不需要通过
SliceHeader
来拿到 slice 的信息,这里使用只是出于展示的目的。&arr[0]
就是Data
,剩下的可以通过len()
和cap()
获得注意!reslicing 可能会有相似的副作用,因为引用的是同一个数组,修改新的 slice 可能会导致原来的发生变化
版本控制
Version | Action | Time |
---|---|---|
1.0 | Init | 2022-01-04 |