疑问
今天我在工作时遇到了一个有趣的问题:
为什么在许多内置函数和库中,将切片的指针作为参数很常见,切片不是总是按引用传递吗?
例如,在Kubernetes的api-machinery实现中,我们可以看到一个函数的签名如下:
func Convert_Slice_string_To_string(input *[]string, out *string, s conversion.Scope) error;:
在优先队列的示例中,我们再次发现类似的情况:
func (pq *PriorityQueue) Pop() interface{};
切片不是已经是指向底层数据的指针吗?
让我们通过使用Go-Playground来测试代码的行为。在整个解释过程中,我将使用相同的示例:一个初始化切片的函数,将其作为参数传递给第二个函数,修改它,然后打印切片以验证内容。
[b,b][b,b][a,a]
func main() {
slice:= []string{"a","a"} func(slice []string){
slice[0]="b";
slice[1]="b";
fmt.Print(slice)
}(slice)
fmt.Print(slice)
}
使用指针会得到相同的结果,事实上,下面的代码
func main() {
slice:= []string{"a","a"}
func(slice *[]string){
(*slice)[0]="b";
(*slice)[1]="b";
fmt.Print(*slice)
}(&slice)
fmt.Print(slice)
}
[b,b][b,b]
那么...为什么这些函数有那个签名?
解释
你可以粗略地想象切片的实现方式如下:
type sliceHeader struct {
Length int
Capacity int
ZerothElement *byte
}
将切片按值传递给函数,所有字段都会被复制,只有数据可以从外部进行修改和访问,通过指针的副本。
但是,请记住,如果指针被重写或修改(由于复制,分配或附加),则在函数外部将看不到任何更改,此外,长度或容量的任何更改都不会对初始函数可见。
然后,问题的答案很简单,但隐藏在切片本身的实现中:
当函数将修改切片的结构、大小或内存位置时,切片的指针是不可或缺的,并且每个更改都应该对调用函数的人可见。
当我们将一个切片作为参数传递给函数时,切片的值按引用传递**(因为我们传递指针的副本),但是描述切片本身的所有元数据只是副本。
我们可以在字面函数中修改切片的数据,但是如果指针由于任何原因更改或修改(由于复制、赋值或附加),则对外部函数的更改可能是部分或不可见的。
例如,如果切片再次分配,则使用新的内存位置;即使值相同,切片也指向新位置,因此对于外部来说,对值的任何修改都将不可见,因为切片指向两个不同的位置(切片副本中的指针被覆盖)。
因此,相同的示例,强制切片再次分配,
func main() {
slice:= []string{"a","a"}
func(slice []string){
slice= append(slice, "a")
slice[0]="b";
slice[1]="b";
fmt.Print(slice)
}(slice)
fmt.Print(slice)
}
[b,b,a][a,a]append
func main() {
slice:= []string{"a","a"}
func(slice []string){
slice[0]="b";
slice[1]="b";
slice= append(slice, "a")
fmt.Print(slice)
}(slice)
fmt.Print(slice)
}
[b,b,a][b,b]
这种行为可能会导致难以发现的棘手错误,因为结果取决于初始数组的大小,例如,下面的代码
func main() {
slice:= make([]string, 2, 3)
func(slice []string){
slice= append(slice, "a")
slice[0]="b";
slice[1]="b";
fmt.Print(slice)
}(slice)
fmt.Print(slice)
}
[b,b,a][b,b]
slice=append(slice, "a", "a")[b,b,a,a][]
在数百或数千行的代码中发现此类错误可能会相当困难。因此,请确保记住,如果你仅想修改元素的值而不是它们的数量或位置,则可以通过按值传递切片来实现,否则将会时不时出现奇怪的错误。
现在你已经准备好理解以下代码片段的结果了,请在Golang Playground中检查答案或在评论中写下答案:
func main() {
slice:= make([]string, 1, 3)
func(slice []string){
slice=slice[1:3]
slice[0]="b"
slice[1]="b"
fmt.Print(len(slice))
fmt.Print(slice)
}(slice)
fmt.Print(len(slice))
fmt.Print(slice)
}