值接收器与指针接收器

对于我来说,在哪种情况下我想使用值接收器而不是总是使用指针接收器是非常不清楚的。

从文件中总结一下:

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

文档还说:“对于基本类型、切片和小结构等类型,值接收器非常便宜,所以除非方法的语义需要指针,否则值接收器是有效和清晰的。”

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

// 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属性的测试运行显示值接收器的最小优势(范围为0.01-0.1 ns/op)

谁能告诉我一个例子,一个值接收器明显比一个指针接收器更有意义?还是我在基准测试中做错了什么?我是否忽略了其他因素?

56642 次浏览

注意FAQ中提到了一致性 .

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

如上所述在这篇文章中:

对于接收者来说,关于指针和值的规则是值方法可以 可以在指针和值上调用,但只能调用指针方法 在指针< / p >

这是不正确的,如评论 by Sart Simha

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

不管对什么方法调用,在方法体中,当使用值接收器时,接收器的标识符指的是逐拷贝值,当使用指针接收器时,接收器的标识符指的是指针:例子

现在:

有人能告诉我一个例子,一个值接收器明显比一个指针接收器更有意义吗?

代码评审注释可以帮助:

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

粗体部分是在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) {
...
}

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

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

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

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


指出:

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

然而,如果需要带有指针接收器的方法来满足接口,那么只能将指针赋值给接口——值将无效。

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

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

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

补充@VonC伟大的,翔实的答案。

我很惊讶,当项目变大,老开发者离开,新开发者出现时,没有人真正提到维护成本。围棋当然是一门年轻的语言。

一般来说,我尽量避免使用指针,但它们确实有自己的位置和优点。

我在以下情况下使用指针:

  • 处理大型数据集
  • 有一个结构维护状态,例如TokenCache,
  • <李> < ul >
  • 我确保所有字段都是私有的,交互只能通过定义的方法接收器
  • 李< / ul > < / > <李> < ul >
  • 我没有将这个函数传递给任何goroutine
  • 李< / ul > < / >

例句:

type TokenCache struct {
cache map[string]map[string]bool
}


func (c *TokenCache) Add(contract string, token string, authorized bool) {
tokens := c.cache[contract]
if tokens == nil {
tokens = make(map[string]bool)
}


tokens[token] = authorized
c.cache[contract] = tokens
}

我避免使用指针的原因:

  • 指针不是并行安全的(GoLang的全部意义)
  • once指针接收器,always指针接收器(用于Struct的所有方法以保持一致性)
  • 互斥锁肯定比“价值复制成本”更昂贵、更慢、更难维护。
  • 说到“价值复制成本”,这真的是个问题吗?过早的优化是万恶之源,你可以在以后添加指针
  • 它直接,有意识地迫使我设计小结构
  • 指针可以通过设计意图清晰、I/O明显的纯函数来避免
  • 我相信,使用指针的垃圾收集更加困难
  • 更容易讨论封装和责任
  • 保持简单,傻瓜(是的,指针可能很棘手,因为你永远不知道下一个项目的开发)
  • 单元测试就像穿过粉红色的花园(只有斯洛伐克语的表达?),意味着简单
  • 如有条件,没有空值(可以在需要指针的地方传递NIL)

我的经验法则是,写尽可能多的封装方法,比如:

package rsa


// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
return []byte("secret text"), nil
}


cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock)

更新:

这个问题激励我进一步研究这个话题,并写了一篇关于它的博客文章

这是一个语义学问题。假设你写了一个函数,以两个数字作为参数。你不想突然发现这些数字中的任何一个被调用函数改变了。如果你把它们作为指针传递,那是可能的。很多东西都应该像数字一样。比如点,2D矢量,日期,矩形,圆等等。这些东西没有同一性。两个圆在同一位置,具有相同的半径,不应相互区分。它们是值类型。

但是像数据库连接或文件句柄、GUI中的按钮这样的东西,身份是很重要的。在这些情况下,你需要一个指向对象的指针。

当某些东西本质上是值类型时,例如矩形或点,最好能够不使用指针传递它们。为什么?因为这意味着你肯定会避免改变对象。它向代码的读者阐明语义和意图。很明显,接收对象的函数不能也不会改变对象。