在 Go 函数中返回局部数组的一部分是否安全?

chm*_*ike 6 arrays return return-value go slice

如果我返回作为函数或方法局部变量的数组切片会发生什么?Go 是否将数组数据复制到切片中make()?容量是否与切片大小或数组大小匹配?

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

icz*_*cza 6

这在规范:切片表达式中有详细说明。

数组不会被复制,而是切片表达式的结果将是一个引用数组的切片。在 Go 中,从函数或方法返回局部变量或它们的地址是完全安全的,Go 编译器执行转义分析来确定一个值是否可以转义函数,如果是(或者更确切地说,它不能证明一个值可能不会转义),它会在堆上分配它,以便在函数返回后可用。

切片表达式:tmp[:end]意味着tmp[0:end](因为缺失的low索引默认为零)。由于您没有指定容量,它将默认为len(tmp) - 0which is len(tmp)which is 100

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

a[low : high : max]
Run Code Online (Sandbox Code Playgroud)

这将结果切片的容量设置为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
Run Code Online (Sandbox Code Playgroud)

Go Playground上试一试。

避免堆分配

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

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

func foo(tmp *[100]uint64) []uint64 {
    // ...
    return tmp[:end]
}
Run Code Online (Sandbox Code Playgroud)

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

func main() {
    var tmp [100]uint64
    foo(&tmp)
}
Run Code Online (Sandbox Code Playgroud)

运行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
Run Code Online (Sandbox Code Playgroud)

变量tmp不会移动到堆中。

请注意,这[100]uint64被视为要在堆栈上分配的小数组。有关详细信息,请参阅Go 中关于堆栈分配的“小”对象是什么?