在 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) - 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 上试试。
避免堆分配
如果你想在堆栈上分配它,你不能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 并分配将在堆上函数外部可见的变量。
容量将是底层数组的容量。
如果我 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) - 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 上试试。
避免堆分配
如果你想在堆栈上分配它,你不能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 并分配将在堆上函数外部可见的变量。
容量将是底层数组的容量。