[译] Go:方法接收者应该使用 T 还是 *T

1,440 阅读2分钟

这篇文章是我几天前在 Twitter 上提出的建议的延续。

在 Go 中,对于任何类型 T 都存在类型 *T,表示获取 T 类型(T 表示你声明的类型)变量的地址。例如:

type T struct { a int; b bool }
var t T    // t's type is T
var p = &t // p's type is *T

这两种类型,T 和 *T 是不同的,*T 不能替代 T(此规则是递归的,**T 会返回 *T 地址指向的值)。

你可以在任何类型上声明方法;也就是说,你在 package 中声明了一个类型。因此,你可以在这个类型上声明一个方法,他的接收者可以使用 T 或者 *T。或者是说声明接收者的类型为 T 是为了获取接收者值的副本,声明接收者的类型为 *T 是为了获取指向接收者值的指针(Go 中的方法只是函数的语法糖,它将接收者作为第一个形式参数传递)。那么问题就变成了,我们应该选择哪种方式?(如果该方法不改变它的接收者,它是否需要方法这种形式?)

显然,如果你的方法改变了接收者,那应该声明 * T。但是,如果该方法不改变其接收者,是否可以将其声明为 T 呢?

事实证明,这样做的场景非常有限。例如,众所周知,你不应该复制 sync.Mutex 值,因为它会使互斥锁失效。由于互斥锁控制对数据的访问,它们经常被包含在结构中:

package counter

type Val struct {
        mu  sync.Mutex
        val int
}

func (v *Val) Get() int {
        v.mu.Lock()
        defer v.mu.Unlock()
        return v.val
}

func (v *Val) Add(n int) {
        v.mu.Lock()
        defer v.mu.Unlock()
        v.val += n
}

大多数 Go 程序员都知道我们应该使用指针接收者 *Val ,并在其上声明 Get 或 Add 方法。但是,任何嵌入 Val 以利用其零值的类型,也必须把方法的接收者设为指针类型,否则它可能复制其嵌入类型的值。

type Stats struct {
        a, b, c counter.Val
}

func (s Stats) Sum() int {
        return s.a.Get() + s.b.Get() + s.c.Get() // whoops
}

对于切片类型,可能也会发生类似的陷阱,当然也有可能发生意外的数据竞争

简而言之,我认为你更应该在 *T 上声明方法,除非你有充分的理由不这样做。

相关文章

  1. What is the zero value, and why is it useful?
  2. Ice cream makers and data races
  3. Slices from the ground up
  4. The empty struct