传递自定义切片类型的引用

huangapple go评论79阅读模式
英文:

Passing custom slice types by reference

问题

我很难理解指针、切片和接口在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 在这里包含了一个 Foo
}

func Load(list Loader) {
  list.Load("1", "2")
  // 在这里 list 仍然是 nil
}

在这个设置中,我尝试做以下操作:

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

然而,list 总是为 nil。我的 FooList.Load 函数确实向 l 切片添加了一个元素,但只到此为止。Load 函数中的 list 仍然是 nil。我认为我应该能够传递切片的引用并让它添加元素。但显然我在如何使其工作方面遗漏了一些东西。

英文:

I'm having trouble wrapping my head around how pointers, slices, and interfaces interact in Go. This is what I currently have coded up:

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
}

Given this setup, I then try to do the following:

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

However, list is always nil here. My FooList.Load function does add an element to the l slice, but that's as far as it gets. The list in Load continues to be nil. I think I should be able to just pass the reference to my slice around and have things append to it. I'm obviously missing something on how to get it to work though.

答案1

得分: 3

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

如果你打算在方法中进行更改,你可能想要使用指针接收器。

// 我们还在FooList的指针接收器上定义了一个Load方法。
func (l *FooList) Load(a, b string) {
*l = append(*l, Foo{a, b})
}

然而,这会导致FooList值本身无法满足Loader接口。

var list FooList
Load(list) // 在这一点上,你应该会看到编译器错误。

然而,对于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
}

// 我们定义FooList为Foo的切片。
type FooList []Foo

// 我们还在FooList的指针接收器上定义了一个Load方法。
func (l *FooList) Load(a, b string) {
*l = append(*l, Foo{a, b})
}

// 鉴于我们用指针接收器定义了该方法,一个普通的FooList将无法满足Loader接口...但是一个FooList指针可以。

func main() {
var list FooList
Load(&list)
fmt.Println(list)
}

英文:

(Code in http://play.golang.org/p/uuRKjtxs9D)

If you intend your method to make changes, you probably want to use a pointer receiver.

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

This has a consequence, though, that a FooList value won't itself satisfy the Loader interface.

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

A pointer to a FooList value, though, will satisfy the Loader interface.

var list FooList
Load(&list)

Complete code below:

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

得分: 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通过值传递参数,正如文档描述的

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

实际上,这意味着上面示例中的abc各自指向不同的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的_地址_,允许它们的逻辑更改原始值。请注意,文档中的行为仍然适用:ab仍然是原始值的副本,但作为increment函数的参数提供的原始值是c的_地址_。

对于切片的情况与此类似。它们是引用,但引用本身作为参数按值传递,因此如果更改引用,调用点将不会观察到更改,因为它们是不同的变量。

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

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

你可以在标准库的许多地方看到这种技术的应用,比如strconv.AppendInt函数。

英文:

I'm going to simplify the problem so it's easier to understand. What is being done there is very similar to this, which also does not work (you can run it 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
}

The reason why this does not work is because Go passes parameters by value, as the documentation describes:

> In a function call, the function value and arguments are evaluated in the usual
> order. After they are evaluated, the parameters of the call are passed by value
> to the function and the called function begins execution.

In practice, this means that each of a, b, and c in the example above are pointing to different int variables, with a and b being copies of the initial c value.

To fix it, we must use pointers so that we can refer to the same area of memory (runnable 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
}

Now a and b are both pointers that contain the address of variable c, allowing their respective logic to change the original value. Note that the documented behavior still holds here: a and b are still copies of the original value, but the original value provided as a parameter to the increment function is the address of c.

The case for slices is no different than this. They are references, but the reference itself is provided as a parameter by value, so if you change the reference, the call site will not observe the change since they are different variables.

There's also a different way to make it work, though: implementing an API that resembles that of the standard append function. Again using the simpler example, we might implement increment without mutating the original value, and without using a pointer, by returning the changed value instead:

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

You can see that technique used in a number of places in the standard library, such as the strconv.AppendInt function.

答案3

得分: 0

这是一个值得保持的心智模型,用来理解Go语言的数据结构是如何实现的。通常这样做可以更容易地推理出这种行为的原因。

http://research.swtch.com/godata 是一个介绍Go语言高级视图的好资源。

英文:

It's worth keeping a mental model of how Go's data structures are implemented. That usually makes it easier to reason about behaviour like this.

http://research.swtch.com/godata is a good introduction to the high-level view.

答案4

得分: 0

Go是按值传递的。这对于参数和接收器都是正确的。如果你需要对切片值进行赋值,你需要使用指针。

然后我在某个地方读到,不应该传递切片的指针,因为它们已经是引用了。

这并不完全正确,而且缺少了一部分内容。

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

// 伪代码
type map *SomeInternalMapStructure

因此,要修改关联数组的“内容”,你不需要对映射变量进行赋值;你可以通过值传递一个映射变量,并且该函数可以更改由映射变量指向的关联数组的内容,并且这将对调用者可见。当你意识到它是指向某个内部数据结构的指针时,这是有意义的。只有当你想要改变指向的内部关联数组时,才会对映射变量进行赋值。

然而,切片更加复杂。它是一个指针(指向内部数组),加上长度和容量,两个整数。因此,你可以将其简单地看作:

// 伪代码
type slice struct {
    underlyingArray uintptr
    length int
    capacity int
}

因此,它不仅仅是一个指针。它是一个指向底层数组的指针。但长度和容量是切片类型的“值”部分。

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

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

英文:

Go is pass-by-value. This is true for both parameters and receivers. If you need to assign to the slice value, you need to use a pointer.

> Then I read somewhere that you shouldn't pass pointers to slices since
> they are already references

This is not entirely true, and is missing part of the story.

When we say something is a "reference type", including a map type, a channel type, etc., we mean that it is actually a pointer to an internal data structure. For example, you can think of a map type as basically defined as:

// pseudocode
type map *SomeInternalMapStructure

So to modify the "contents" of the associative array, you don't need to assign to a map variable; you can pass a map variable by value and that function can change the contents of the associative array pointed to by the map variable, and it will be visible to the caller. This makes sense when you realize it's a pointer to some internal data structure. You would only assign to a map variable if you want to change which internal associative array you want it to point to.

However, a slice is more complicated. It is a pointer (to an internal array), plus the length and capacity, two integers. So basically, you can think of it as:

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

So it's not "just" a pointer. It is a pointer with respect to the underlying array. But the length and capacity are "value" parts of the slice type.

So if you just need to change an element of the slice, then yes, it acts like a reference type, in that you can pass the slice by value and have the function change an element and it's visible to the caller.

However, when you append() (which is what you're doing in the question), it's different. First, appending affects the length of the slice, and length is one of the direct parts of the slice, not behind a pointer. Second, appending may produce a different underlying array (if the capacity of the original underlying array is not enough, it allocates a new one); thus the array pointer part of the slice might also be changed. Thus it is necessary to change the slice value. (This is why append() returns something.) In this sense, it cannot be regarded as a reference type, because we are not just "changing what it points to"; we are changing the slice directly.

huangapple
  • 本文由 发表于 2013年9月11日 09:39:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/18731376.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定