4

假设我们有这种结构(有史以来最简单的结构之一):

type some struct{
    I uint32
}

我们希望有一个该类型的变量并在 for 循环中原子地递增(可能在另一个 goroutine 中,但现在情况不同了)。我执行以下操作:

q := some{0}
for i := 0; i < 10; i++ {
        atomic.AddUint32(&q.I,1) // increment [1]
        fmt.Println(q.I)
}

我们得到了我们所期望的,到目前为止一切都很好,但是如果我们为该类型声明一个函数,如下所示:

func (sm some) Add1(){
    atomic.AddUint32(&sm.I,1)
}

并在上面的示例(第 [1] 行)中调用此函数,该值不会增加,我们只会得到零。问题很明显——为什么?

这必须是一些基本的东西,但由于我是新手,所以我没有意识到这一点。

4

2 回答 2

7

Go 编程语言规范

来电

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

接收者sm some按值传递给方法,当您从方法返回时,副本将被丢弃。使用指针接收器。

例如,

package main

import (
    "fmt"
    "sync/atomic"
)

type some struct {
    I uint32
}

func (sm *some) Add1() {
    atomic.AddUint32(&sm.I, 1)
}

func main() {
    var s some
    s.Add1()
    fmt.Println(s)
}

输出:

{1}

常见问题解答 (FAQ)

函数参数什么时候传值?

与 C 家族中的所有语言一样,Go 中的所有内容都是按值传递的。也就是说,一个函数总是得到一个被传递的东西的副本,就好像有一个赋值语句将值分配给参数一样。例如,将 int 值传递给函数会生成 int 的副本,传递指针值会生成指针的副本,但不会复制它指向的数据。

我应该在值或指针上定义方法吗?

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

对于不习惯指针的程序员来说,这两个例子之间的区别可能会让人感到困惑,但情况其实很简单。当在一个类型上定义一个方法时,接收者(在上面的例子中)表现得就像它是方法的一个参数一样。将接收者定义为值还是指针是同一个问题,那么,函数参数应该是值还是指针。有几个考虑。

首先,也是最重要的,该方法是否需要修改接收器?如果是这样,接收者必须是一个指针。(切片和映射充当引用,因此它们的故事有点微妙,但例如要在方法中更改切片的长度,接收者仍然必须是指针。)在上面的示例中,如果 pointerMethod 修改了s,调用者将看到这些更改,但 valueMethod 是使用调用者参数的副本调用的(这是传递值的定义),因此调用者将看不到它所做的更改。

顺便说一句,指针接收器与 Java 中的情况相同,尽管在 Java 中指针隐藏在幕后。Go 的价值接收器是不寻常的。

其次是效率的考虑。如果接收器很大,例如一个大结构,使用指针接收器会便宜得多。

其次是一致性。如果该类型的某些方法必须具有指针接收器,那么其余的也应该具有,因此无论该类型如何使用,方法集都是一致的。有关详细信息,请参阅方法集部分。

对于基本类型、切片和小型结构等类型,值接收器非常便宜,因此除非方法的语义需要指针,否则值接收器是高效且清晰的。

于 2016-03-05T13:02:24.690 回答
2

您的函数需要接收要递增的值的指针,这样您就不会传递结构的副本,并且在下一次迭代时 I 可以递增。

package main

import (
"sync/atomic"
"fmt"
)

type some struct{
    I uint32
}

func main() {
q := &some{0}
for i := 0; i < 10; i++ {
        q.Add1()
        fmt.Println(q.I)
}
}

func (sm *some) Add1(){
    atomic.AddUint32(&sm.I,1)
}
于 2016-03-05T13:08:14.733 回答