1

我很难理解指针、切片和接口在 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切片添加了一个元素,但仅此而已。输入list负载继续为nil。我想我应该能够将引用传递给我的切片并附加一些东西。我显然错过了如何让它工作的东西。

4

4 回答 4

3

(代码在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)
}
于 2013-09-11T02:36:29.453 回答
1

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

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 按值传递参数,如文档所述

在函数调用中,函数值和参数按通常的顺序计算。在它们被评估之后,调用的参数按值传递给函数,被调用的函数开始执行。

实际上,这意味着上例中的 、 和 中的每一个ab指向c不同的 int 变量,其中ab是初始c值的副本。

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

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的指针,允许它们各自的逻辑改变原始值。请注意,此处记录的行为仍然有效:a并且b仍然是原始值的副本,但作为increment函数参数提供的原始值是c.

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

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

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

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

于 2013-09-11T02:48:38.983 回答
0

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

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

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

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

// pseudocode
type map *SomeInternalMapStructure

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

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

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

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

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

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

于 2013-09-12T10:31:15.037 回答
0

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

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

于 2013-09-11T04:00:32.053 回答