(译文)为什么在许多BuiltIn函数和库中,会出现将slice的指针作为参数传递给函数,难道slice不总是通过引用传递?

324 阅读4分钟

例如,在Kubernetes的api-machinery的实现中,我们可以看到带有以下签名的函数:

func Convert_Slice_string_To_string(input *[]string, out *string, s conversion.Scope) error

以及,在优先队列的示例中,我们可以再次找到类似的内容:

func (pq *PriorityQueue) Pop() interface{}

难道slice切片本来就不是一个指向底层数据的指针?

我们来利用Go-Playground进行一些调查,以测试所报告代码的行为。

在整个说明中,我将使用相同的示例:一个函数,该函数初始化切片,将其作为参数传递给第二个函数,
对其进行修改,然后打印该切片以验证内容。

通常认为切片是通过引用传递的,实际上,即使切片被初始化为[a,a],以下示例也将打印[b,b]和[b,b],因为在执行过程中对其进行了修改 字面量函数的变化,并且变化对main函数可见。

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 //下标为0的元素指针
}

通过值将切片传递给函数,所有字段都会被复制一份,只有数据能够通过切片的指针副本被修改和外部获取。

但是,请记住,如果指针被覆盖或修改(复制,赋值或追加),则在函数外部将看不到任何更改,此外,初始的长度或容量也不会更改。

那么,问题的答案很简单,但是隐藏在slice本身的实现中:

当函数要修改切片的结构,大小或在内存中的位置时,指向切片的指针是必不可少的,并且每次更改都应对调用
该函数的用户可见。

当我们将切片作为参数传递给函数时,切片的值通过引用传递(因为我们传递了指针的副本),但是描述切片本身的所有元数据只是副本。

我们可以在函数中修改切片的数据,但是,如果由于某种原因导致指向数据的指针发生更改,或者更改了切片元数据,则外部函数可能看不到该更改,或者根本看不到该更改。

例如,如果在函数内重新分配切片,则使用新的内存地址,即使值相同,切片也会指向新内存地址,因此,值的任何修改都不会被看到,因为切片指向两个不同的位置(切片副本中的指针被覆盖)。

因此,在同一示例中,强制再次分配分片,

func main() {
    slice:= []string{"a","a"}
 
    func(slice []string){
        slice= append(slice, "a")
        slice[0]="b"
        slice[1]="b" 
        fmt.Print(slice) // [b b a]
    }(slice)
    fmt.Print(slice) // [a a] 
}

将打印[b,b,a]和[a,a]。

将slice= append(slice,"a") 移动到下面两行之后,我们可以注意到结果是不一样的,因为切片在操作值之后重新分配地址,并且指针仍指向初始内存地址。 您可以使用以下代码进行检查:

func main() {
    slice:= []string{"a","a"}
 
    func(slice []string){
        slice[0]="b"
        slice[1]="b"
        slice= append(slice, "a") // 这一行原来在slice[0]="b"上面
        fmt.Print(slice)    // [b,b,a]
    }(slice)
    fmt.Print(slice)  //[b,b]
}

这里会打印[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]和[](一个空数组,因为它不是初始化)。

func main() {
    slice:= make([]string, 2, 3) 
    func(slice []string){        
        slice= append(slice, "a","a")        
        slice[0]="b"
        slice[1]="b"
        fmt.Print(slice)
    }(slice)
    fmt.Print(slice) 
}
在数百行或数千行中间发现这种错误可能非常困难。
因此,请记住,如果您只想修改元素的值而不是数字的数量或位置,则可以按值传递切片,否则会不时出现怪异
的错误。

现在您已经准备好理解以下代码片段的结果,在 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)
}

原文来自

medium.com/swlh/golang…