一尘不染

在Go函数中返回局部数组的切片安全吗?

go

如果返回作为函数或方法的局部变量的数组的切片会怎样?Go是否将数组数据复制到使用创建的切片中make()?容量将匹配切片大小还是阵列大小?

func foo() []uint64 {
    var tmp [100]uint64
    end := 0
    ...
    for ... {
        ...
        tmp[end] = uint64(...)
        end++
        ...
    }
    ... 
    return tmp[:end]
}

阅读 230

收藏
2020-07-02

共1个答案

一尘不染

这在Spec:Slice
expressions中
有详细说明。

不会复制该数组,但slice表达式的结果将是引用该数组的slice。在Go中,从函数或方法返回局部变量或它们的地址是绝对安全的,Go编译器执行逸出分析以确定值是否可以逸出该函数,以及是否可以逸出(或是否不能证明某个值)可能无法转义),它会在堆上分配它,因此函数返回后将可用。

切片表达式:tmp[:end]意思是tmp[0:end](因为缺少的low索引默认为零)。既然你没有指定容量,则默认为len(tmp) - 0这是len(tmp)这是100

您还可以使用 完整切片表达式 来控制结果切片的容量,该 表达式 的形式为:

a[low : high : max]

将结果切片的容量设置为max - low

更多示例来阐明所得切片的长度和容量:

var a [100]int

s := a[:]
fmt.Println(len(s), cap(s)) // 100 100
s = a[:50]
fmt.Println(len(s), cap(s)) // 50 100
s = a[10:50]
fmt.Println(len(s), cap(s)) // 40 90
s = a[10:]
fmt.Println(len(s), cap(s)) // 90 90

s = a[0:50:70]
fmt.Println(len(s), cap(s)) // 50 70
s = a[10:50:70]
fmt.Println(len(s), cap(s)) // 40 60
s = a[:50:70]
fmt.Println(len(s), cap(s)) // 50 70

Go Playground上尝试一下。

避免堆分配

如果要在堆栈上分配它,则不能返回任何指向它(或其一部分)的值。如果将其分配在堆栈上,将无法保证返回后仍保持可用状态。

一种可能的解决方案是将指向数组的指针作为函数的参数传递(并且您可以返回一个切片,指定该函数填充的 有用 部分),例如:

func foo(tmp *[100]uint64) []uint64 {
    // ...
    return tmp[:end]
}

如果调用者函数在堆栈上创建了数组,则不会导致“重新分配”或“移动”到堆:

func main() {
    var tmp [100]uint64
    foo(&tmp)
}

运行go run -gcflags '-m -l' play.go,结果是:

./play.go:8: leaking param: tmp to result ~r1 level=0
./play.go:5: main &tmp does not escape

该变量tmp不会移到堆。

请注意,这[100]uint64被认为是要在堆栈上分配的小数组。

2020-07-02