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

huangapple go评论88阅读模式
英文:

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

问题

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

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

如果你返回一个函数或方法的局部变量数组的切片,Go语言会将切片指向原始数组的一部分,而不会复制整个数组数据。切片的容量将与切片的大小匹配,即切片的长度和容量都将是end的值。这意味着切片只能访问到原始数组中已经赋值的元素,而不会包含未赋值的元素。

英文:

What happens if I return a slice of an array that is a local variable of a function or method? Does Go copy the array data into a slice create with make()? Will the capacity match the slice size or the array size?

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

答案1

得分: 7

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

数组不会被复制,而是切片表达式的结果将是一个引用该数组的切片。在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 被认为是一个在堆栈上分配的小数组。有关详细信息,请参阅 https://stackoverflow.com/questions/42243197/what-is-considered-small-object-in-go-regarding-stack-allocation/42244087#42244087

英文:

This is detailed in Spec: Slice expressions.

The array will not be copied, but instead the result of the slice expression will be a slice that refers to the array. In Go it is perfectly safe to return local variables or their addresses from functions or methods, the Go compiler performs an escape analysis to determine if a value may escape the function, and if so (or rather if it can't prove that a value may not escape), it allocates it on the heap so it will be available after the function returns.

The slice expression: tmp[:end] means tmp[0:end] (because a missing low index defaults to zero). Since you didn't specify the capacity, it will default to len(tmp) - 0 which is len(tmp) which is 100.

You can also control the result slice's capacity by using a full slice expression, which has the form:

a[low : high : max]

Which sets the resulting slice's capacity to max - low.

More examples to clarify the resulting slice's length and capacity:

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

Try it on the Go Playground.

Avoiding heap allocation

If you want to allocate it on the stack, you can't return any value that points to it (or parts of it). If it would be allocated on the stack, there would be no guarantee that after returning it remains available.

A possible solution to this would be to pass a pointer to an array as an argument to the function (and you may return a slice designating the useful part that the function filled), e.g.:

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

If the caller function creates the array (on the stack), this will not cause a "reallocation" or "moving" to the heap:

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

Running go run -gcflags '-m -l' play.go, the result is:

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

The variable tmp is not moved to heap.

Note that [100]uint64 is considered a small array to be allocated on the stack. For details see https://stackoverflow.com/questions/42243197/what-is-considered-small-object-in-go-regarding-stack-allocation/42244087#42244087

答案2

得分: 2

数据不会被复制。数组将被用作切片的底层数组。

看起来你在担心数组的生命周期,但是编译器和垃圾回收器会为你解决这个问题。这和返回指向“局部变量”的指针一样安全。

英文:

The data is not copied. The array will be used as the underlying array of the slice.

It looks like you're worrying about the life time of the array, but the compiler and garbage collector will figure that out for you. It's as safe as returning pointers to "local variables".

答案3

得分: 2

没有发生任何错误。

Go语言不会进行复制,而是编译器执行逃逸分析,并在堆上分配将在函数外可见的变量。

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

英文:

Nothing wrong happens.

Go does not make a copy but compiler performs escape analysis and allocates the variable that will be visible outside function on heap.

The capacity will be the capacity of underlying array.

huangapple
  • 本文由 发表于 2017年3月1日 19:13:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/42530219.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定