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

Is returning a slice of a local array in a Go function safe?

如果我 return 数组的一部分是函数或方法的局部变量,会发生什么情况? Go 是否将数组数据复制到使用 make() 创建的切片中?容量是否匹配切片大小或数组大小?

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

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

数组不会被复制,但切片表达式的结果将是引用数组的切片。在 Go 中,return 局部变量或其函数或方法中的地址是完全安全的,Go 编译器执行 escape analysis 来确定一个值是否可以逃逸函数,如果可以(或者更确切地说,是否可以无法证明一个值可能不会逃逸),它将它分配在堆上,因此它将在函数 returns.

之后可用

切片表达式:tmp[:end] 表示 tmp[0:end](因为缺少 low 索引默认为零)。由于您没有指定容量,它将默认为 len(tmp) - 0len(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 上试试。

避免堆分配

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

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

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

如果调用函数创建数组(在堆栈上),这不会导致堆 "reallocation" 或 "moving":

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被认为是分配在栈上的小数组。有关详细信息,请参阅 What is considered "small" object in Go regarding stack allocation?

数据未复制。该数组将用作切片的底层数组。

您似乎在担心数组的生命周期,但编译器和垃圾收集器会为您解决这个问题。它与返回指向 "local variables".

的指针一样安全

没有发生任何错误。

Go 不会创建副本,但编译器会执行 escape analysis 并分配将在堆上函数外部可见的变量。

容量将是底层数组的容量。