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

Posted

技术标签:

【中文标题】在 Go 函数中返回局部数组的一部分是不是安全?【英文标题】:Is returning a slice of a local array in a Go function safe?在 Go 函数中返回局部数组的一部分是否安全? 【发布时间】:2017-07-20 16:35:25 【问题描述】:

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

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

【问题讨论】:

【参考方案1】:

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

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

切片表达式: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 被视为要在堆栈上分配的小数组。详情见What is considered "small" object in Go regarding stack allocation?

【讨论】:

这是一个非常详细的答案。让我感到困惑的是我假设数组将在堆栈上分配并在返回时释放,就像在 C 和 C++ 中一样。如果它是在堆上分配的,则使用此代码模式将一无所获。除了使用递归函数来使用堆栈并分配一次且仅一次确切需要的大小之外,还有其他解决方案吗? @chmike 查看已编辑的答案。您可以选择将指向数组的指针作为参数传递。 感谢您的建议。但这不符合我的目标。函数 foo 正在生成一些数据,这些数据将在我知道永远不会超过 100 的切片中返回。问题是我事先不知道产生的数据量,一般会远少于100个。大约10个左右。我假设数组是在堆栈上分配的。这个假设是错误的。小阵列是。没有那么大的数组。 @chmike 您可以返回您传递的数组的一部分,它指定 _useful 部分(您填充的部分),这不是问题。编辑以使其清楚。这也被认为是一个小数组,它将在堆栈上分配。详情见What is considered “small” object in Go regarding stack allocation? @bigdatamann 这是foo()函数的分析结果:表示分析器检测到(输入)参数tmp被返回值引用,所以要考虑这个在分析foo()的调用时。【参考方案2】:

数据没有被复制。该数组将用作切片的底层数组。

您似乎在担心数组的生命周期,但编译器和垃圾收集器会为您解决这个问题。就像返回指向“局部变量”的指针一样安全。

【讨论】:

我假设数组是在堆栈上分配的。这仅适用于小型阵列。上面的大数组是在堆上分配的,所以实际上,一个切片会携带它,并且会浪费大量内存空间,除非我将相关数据复制到我自己分配的切片中。但是该函数将分配两个对象,这是希望避免的。因此,首先使用该数组是一个糟糕的选择。【参考方案3】:

没有任何问题发生。

Go 不会进行复制,但编译器会执行 escape analysis 并分配在堆上函数外部可见的变量。

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

【讨论】:

以上是关于在 Go 函数中返回局部数组的一部分是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

golang/go语言闭包(closure)

golang/go语言闭包(closure)

go强大的垃圾回收机制。

Go语言之函数详解

python中使用闭包及修改外部函数的局部变量

Go的变量作用域