一尘不染

值接收器与指针接收器

go

我不清楚在哪种情况下我想使用值接收器而不是总是使用指针接收器。

回顾一下文档:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

文档也说“对于类型,如基本类型,切片和小结构,值接收机是非常便宜,所以,除非所述方法的语义需要一个指针,一个值是接收机有效和明确的。”

他们文档说的第一点是值接收器“非常便宜”,但问题是它是否比指针接收器便宜。所以我做了一个小的基准测试(gist 上的代码),它向我展示了,即使对于只有一个字符串字段的结构,指针接收器也更快。这些是结果:

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(编辑:请注意,第二点在较新的 go 版本中无效,请参阅评论。)

第二点,文档说价值接收者“高效且清晰”,这更取决于品味,不是吗?就个人而言,我更喜欢通过在任何地方使用相同的东西来保持一致性。什么意义上的效率?性能方面似乎指针几乎总是更有效。很少有具有一个 int 属性的测试运行显示 Value 接收器的最小优势(范围为 0.01-0.1 ns/op)

有人能告诉我一个值接收器显然比指针接收器更有意义的情况吗?还是我在基准测试中做错了什么?我是否忽略了其他因素?


阅读 149

收藏
2021-11-07

共1个答案

一尘不染

请注意,常见问题解答确实提到了一致性

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

本主题所述

关于接收者的指针与值的规则是值方法可以在指针和值上调用,但指针方法只能在指针上调用

正如Sart 评论的那样,这是不正确的

值接收器和指针接收器方法都可以在正确类型的指针或非指针上调用。

无论调用什么方法,在方法主体内,接收者的标识符在使用值接收器时引用一个副本值,当使用指针接收器时引用一个指针:example

现在:

有人能告诉我一个值接收器显然比指针接收器更有意义的情况吗?

代码评审意见可以帮助:

  • 如果接收者是 map、func 或 chan,则不要使用指向它的指针。
  • 如果接收者是一个切片并且该方法不重新切片或重新分配切片,则不要使用指向它的指针。
  • 如果方法需要改变接收者,接收者必须是一个指针。
  • 如果接收者是一个包含一个sync.Mutex或类似同步字段的结构体,则接收者必须是一个指针以避免复制。
  • 如果接收器是一个大的结构体或数组,指针接收器的效率更高。大是多大?假设它相当于将其所有元素作为参数传递给方法。如果感觉太大,那么对于接收器来说也太大了。
  • 函数或方法,无论是并发的还是从该方法调用时,都可以改变接收者吗?值类型在调用方法时创建接收器的副本,因此外部更新不会应用于此接收器。如果更改必须在原始接收器中可见,则接收器必须是指针。
  • 如果接收器是一个结构体、数组或切片,并且它的任何元素都是一个指向可能正在发生变化的东西的指针,那么更喜欢指针接收器,因为它会让读者更清楚地了解意图。
  • 如果接收器是一个小数组或结构,它自然是一个值类型(例如,类似time.Time类型的东西),没有可变字段和指针,或者只是一个简单的基本类型,如 int 或 string,值接收器使感觉
    一个值接收者可以减少可以产生的垃圾量;如果将值传递给值方法,则可以使用堆栈上副本而不是在堆上分配。(编译器试图巧妙地避免这种分配,但它不可能总是成功。)不要在没有首先分析的情况下选择值接收器类型。
  • 最后,如有疑问,请使用指针接收器。

粗体部分可以在以下位置找到net/http/server.go#Write()

// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}

注意:在评论中指出了关于接口方法的警告:

遵循接收器类型应该一致的建议,如果您有一个指针接收器,那么您的(p *type) String() string方法应该使用指针接收器。

但这并没有实现Stringer接口,除非您的 API 的调用者也使用指向您的类型的指针,这可能是您的 API 的可用性问题。

我不知道一致性在这里是否胜过可用性。


指出:

您可以混合和匹配带有值接收器的方法和带有指针接收器的方法,并将它们与包含值和指针的变量一起使用,而不必担心哪个是哪个。
两者都可以使用,并且语法相同。

然而,如果需要带有指针接收器的方法来满足接口,那么只有一个指针可以分配给接口——一个值将是无效的。

通过接口调用值接收器方法总是会创建值的额外副本

接口值基本上是指针,而您的值接收器方法需要值;因此,每次调用都需要 Go 创建值的新副本,用它调用您的方法,然后将值扔掉。
只要您使用值接收器方法并通过接口值调用它们,就无法避免这种情况;这是 Go 的基本要求。

的概念不可寻址值,这是可寻址的值相反。仔细的技术版本在地址运算符的 Go 规范中,但挥手总结版本是大多数匿名值是不可寻址的(一个很大的例外是复合文字

2021-11-07