【问题标题】:Passing custom slice types by reference通过引用传递自定义切片类型
【发布时间】:2013-09-14 21:33:45
【问题描述】:

我很难理解指针、切片和接口在 Go 中是如何交互的。这是我目前编写的代码:

type Loader interface {
  Load(string, string)
}

type Foo struct {
  a, b string
}

type FooList []Foo

func (l FooList) Load(a, b string) {
  l = append(l, Foo{a, b})
  // l contains 1 Foo here
}

func Load(list Loader) {
  list.Load("1", "2")
  // list is still nil here
}

鉴于此设置,然后我尝试执行以下操作:

var list FooList
Load(list)
fmt.Println(list)

但是,此处的列表始终为 nil。我的 FooList.Load 函数确实向 l 切片添加了一个元素,但仅此而已。 Load 中的 list 仍然是 nil。我想我应该能够将引用传递给我的切片并附加一些东西。不过,我显然错过了如何让它工作的东西。

【问题讨论】:

    标签: pointers go slice


    【解决方案1】:

    http://play.golang.org/p/uuRKjtxs9D 中的代码)

    如果您打算对方法进行更改,您可能需要使用指针接收器。

    // We also define a method Load on a FooList pointer receiver.
    func (l *FooList) Load(a, b string) {
        *l = append(*l, Foo{a, b})
    }
    

    不过,这会导致 FooList 值本身不能满足Loader 接口。

    var list FooList
    Load(list)      // You should see a compiler error at this point.
    

    一个指向 FooList 值的指针,但是,将满足Loader 接口。

    var list FooList
    Load(&list)
    

    完整代码如下:

    package main
    
    import "fmt"
    
    /////////////////////////////
    type Loader interface {
      Load(string, string)
    }
    
    func Load(list Loader) {
        list.Load("1", "2")
    }
    /////////////////////////////
    
    
    type Foo struct {
      a, b string
    }
    
    // We define a FooList to be a slice of Foo.
    type FooList []Foo
    
    // We also define a method Load on a FooList pointer receiver.
    func (l *FooList) Load(a, b string) {
        *l = append(*l, Foo{a, b})
    }
    
    // Given that we've defined the method with a pointer receiver, then a plain
    // old FooList won't satisfy the Loader interface... but a FooList pointer will.
    
    func main() {
        var list FooList
        Load(&list)
        fmt.Println(list)
    }
    

    【讨论】:

    • 我发誓这是我开始的地方,但出现了类型错误。然后我在某处读到你不应该将指针传递给切片,因为它们已经是引用,这就是我感到困惑的地方......无论如何,现在一切正常。谢谢!
    【解决方案2】:

    我将简化问题,以便更容易理解。那里正在做的事情与此非常相似,这也不起作用(您可以运行它here):

    type myInt int
    
    func (a myInt) increment() { a = a + 1 }
    func increment(b myInt)    { b.increment() }
    
    func main() {
        var c myInt = 42
        increment(c)
        fmt.Println(c) // => 42
    }
    

    这不起作用的原因是因为Go通过值传递参数,如documentation describes

    在函数调用中,函数值和参数以通常的方式计算 命令。求值后,调用的参数按值传递 到函数,被调用的函数开始执行。

    实际上,这意味着上面示例中的每个abc 都指向不同的int 变量,其中ab 是初始c 的副本价值。

    要修复它,我们必须使用指针,以便我们可以引用相同的内存区域(可运行here):

    type myInt int
    
    func (a *myInt) increment() { *a = *a + 1 }
    func increment(b *myInt)    { b.increment() }
    
    func main() {
        var c myInt = 42
        increment(&c)
        fmt.Println(c) // => 43
    }
    

    现在ab 都是包含变量c地址 的指针,允许它们各自的逻辑更改原始值。请注意,此处记录的行为仍然有效:ab 仍然是原始值的副本,但作为参数提供给 increment 函数的原始值是 @ 的 地址 987654339@.

    切片的情况与此没有什么不同。它们是引用,但引用本身是按值作为参数提供的,因此如果更改引用,调用站点将不会观察到更改,因为它们是不同的变量。

    不过,还有一种不同的方法可以让它工作:实现一个类似于标准 append 函数的 API。再次使用更简单的示例,我们可以在不改变原始值且不使用指针的情况下实现increment,而是返回更改后的值:

    func increment(i int) int {  return i+1 }
    

    您可以在标准库中的许多地方看到该技术使用,例如strconv.AppendInt 函数。

    【讨论】:

      【解决方案3】:

      值得保留一个关于如何实现 Go 的数据结构的心智模型。这通常更容易推理此类行为。

      http://research.swtch.com/godata 很好地介绍了高级视图。

      【讨论】:

        【解决方案4】:

        Go 是按值传递的。对于参数和接收器都是如此。如果需要给切片赋值,需要使用指针。

        然后我在某处读到你不应该将指针传递给切片,因为 他们已经是参考

        这并不完全正确,并且缺少故事的一部分。

        当我们说某个东西是“引用类型”时,包括映射类型、通道类型等,我们的意思是它实际上是一个指向内部数据结构的指针。例如,您可以将地图类型视为基本定义为:

        // pseudocode
        type map *SomeInternalMapStructure
        

        所以要修改关联数组的“内容”,不需要赋值给map变量;您可以按值传递映射变量,并且该函数可以更改映射变量指向的关联数组的内容,并且调用者可以看到它。当您意识到它是指向某些内部数据结构的指针时,这是有道理的。如果您想更改 哪个 您希望它指向的内部关联数组,您只会分配给映射变量。

        但是,切片更复杂。它是一个指针(指向一个内部数组),加上长度和容量,两个整数。所以基本上,你可以把它想象成:

        // pseudocode
        type slice struct {
            underlyingArray uintptr
            length int
            capacity int
        }
        

        所以它不“只是”一个指针。它是一个相对于底层数组的指针。但长度和容量是切片类型的“值”部分。

        因此,如果您只需要更改切片的一个元素,那么是的,它就像一个引用类型,因为您可以按值传递切片并让函数更改一个元素,并且它对调用者可见。

        但是,当您 append()(这就是您在问题中所做的)时,情况就不同了。首先,追加影响切片的长度,长度是切片的直接部分之一,而不是指针后面。其次,追加可能会产生一个不同的底层数组(如果原来的底层数组容量不够,就分配一个新的);因此切片的数组指针部分也可能被更改。因此有必要改变切片值。 (这就是为什么append()会返回一些东西。)从这个意义上说,它不能被视为引用类型,因为我们不仅仅是“改变它指向的东西”;我们直接更改切片。

        【讨论】:

          猜你喜欢
          • 2022-01-20
          • 2019-05-29
          • 2018-01-02
          • 1970-01-01
          • 2017-06-21
          • 1970-01-01
          • 2021-10-26
          • 2017-02-10
          • 1970-01-01
          相关资源
          最近更新 更多