slice 的传递

前言

如果我们需要传递一个 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