疑问

今天我在工作时遇到了一个有趣的问题:

为什么在许多内置函数和库中,将切片的指针作为参数很常见,切片不是总是按引用传递吗?

例如,在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)
}