值接收器与指针接收器

Posted

技术标签:

【中文标题】值接收器与指针接收器【英文标题】:Value receiver vs. pointer receiver 【发布时间】:2015-03-02 17:53:45 【问题描述】:

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

回顾一下文档:

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

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

第一点他们说价值接收器“非常便宜”,但问题是它是否比指针接收器便宜。所以我做了一个小基准(code on 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 版本中无效,请参阅 cmets。)

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

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

【问题讨论】:

我用一个字符串字段和两个字段运行了类似的基准测试:字符串和整数字段。我从价值接收器那里得到了更快的结果。 BenchmarkChangePointerReceiver-4 10000000000 0.99 ns/op BenchmarkChangeItValueReceiver-4 10000000000 0.33 ns/op 这是使用 Go 1.8。我想知道自您上次运行基准测试以来是否对编译器进行了优化。有关详细信息,请参阅gist。 你是对的。使用 Go1.9 运行我的原始基准测试,我现在也得到了不同的结果。指针接收器 0.60 ns/op,值接收器 0.38 ns/op 【参考方案1】:

注意the FAQ does mention consistency

接下来是一致性。如果该类型的某些方法必须具有指针接收器,那么其余的也应该具有,因此无论该类型如何使用,方法集都是一致的。详情请参阅section on method sets。

如上所述in this thread:

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

这不是真的,正如commented by Sart Simha

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

无论调用什么方法,在方法体内,接收者的标识符在使用值接收器时指的是一个复制值,在使用指针接收器时指的是指针:example。

现在:

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

Code Review comment 可以提供帮助:

如果接收者是 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 在the comments 中指出了关于接口方法的警告:

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

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

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


指出:

“Method Sets (Pointer vs Value Receiver)”

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

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

“Go interfaces and automatically generated functions”来自Chris Siebenmann(2017 年 6 月)

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

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

“Learning about Go's unaddressable values and slicing”(仍来自 Chris(2018 年 9 月))

不可寻址值的概念,与可寻址值相反。谨慎的技术版本在 Address operators 的 Go 规范中,但挥手总结版本是大多数匿名值是不可寻址的(一个很大的例外是 composite literals)

【讨论】:

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers 实际上不是这样。值接收器和指针接收器方法都可以在正确类型的指针或非指针上调用。无论调用什么方法,在方法体内,接收者的标识符在使用值接收器时指的是一个复制值,在使用指针接收器时指的是指针:参见play.golang.org/p/3WHGaAbURM 有一个很好的解释here“如果x是可寻址的并且&x的方法集包含m,x.m()就是(&x).m()的简写。” 很好的答案,但我强烈不同意这一点:“因为它会使意图更加清晰”,NOPE,一个干净的 API,X 作为参数,Y 作为返回值是一个明确的意图。通过指针传递 Struct 并花时间仔细阅读代码以检查所有属性被修改的内容远非清晰、可维护。 @itsbruce 感谢您的反馈。您能否指出货物崇拜谣言,让我编辑答案并删除它们?这将对其他读者有所帮助。 我相信实现json.Unmarshaler 是另一种无法实际避免使用指针接收器的情况。没有它,没有任何东西可以解组,返回后一切都会丢失。 json.Marshaler 则相反。如果您不在那里使用值接收器,您将无法编组您类型的非指针值,因为json.Marshal() 不会调用该方法。【参考方案2】:

另外添加到@VonC 很棒的,信息丰富的答案。

令我惊讶的是,一旦项目变得更大,老开发人员离开,新开发人员来了,没有人真正提到维护成本。 Go 肯定是一门年轻的语言。

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

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

处理大型数据集 有一个结构维护状态,例如令牌缓存, 我确保所有字段都是 PRIVATE,只能通过定义的方法接收器进行交互 我没有将此函数传递给任何 goroutine

例如:

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 的全部要点) 一次指针接收器,总是指针接收器(用于所有 Struct 的方法以保持一致性) 与“价值复制成本”相比,互斥锁肯定更昂贵、更慢且更难维护 说到“价值复制成本”,这真的是个问题吗?过早的优化是万恶之源,以后随时可以加指针 它直接、有意识地强迫我设计小型结构 可以通过设计意图明确且 I/O 明显的纯函数来避免指针 我相信使用指针更难收集垃圾 更容易争论封装、责任 保持简单,愚蠢(是的,指针可能很棘手,因为您永远不知道下一个项目的开发人员) 单元测试就像在粉色花园中漫步(只有斯洛伐克语的表达?),意味着简单 如果条件不存在 NIL(可以在需要指针的地方传递 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) 

更新:

这个问题启发了我更多地研究这个话题并写了一篇关于它的博客文章https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

【讨论】:

我喜欢你在这里所说的 99% 并且非常同意它。也就是说,我想知道您的示例是否是说明您观点的最佳方式。 TokenCache 本质上不是一个映射(来自@VonC - “如果接收器是映射、func 或 chan,请不要使用指向它的指针”)。由于地图是引用类型,通过使“Add()”成为指针接收器可以获得什么? TokenCache 的任何副本都将引用相同的映射。看看这个 Go 游乐场 - play.golang.com/p/Xda1rsGwvhq 很高兴我们保持一致。好点。实际上,我想我在这个例子中使用了指针,因为我从一个项目中复制了它,而 TokenCache 处理的东西不仅仅是那个地图。如果我在一种方法中使用指针,我会在所有方法中使用它。您是否建议从这个特定的 SO 示例中删除指针? 大声笑,复制/粘贴又来了! ? IMO,您可以保持原样,因为它说明了一个容易陷入的陷阱,或者您可以将地图替换为展示状态和/或大型数据结构的东西。 好吧,我相信他们会阅读 cmets... PS:Rich,您的论点似乎有道理,请在 LinkedIn 上添加我(我的个人资料中的链接)很高兴联系。 @Lukas:一个非常旁注:斯洛伐克语的“穿过玫瑰(不是粉红色!)花园”在英语中有一个非常相似的对应物:“玫瑰床”。所以'单元测试是玫瑰花':-)【参考方案3】:

这是一个语义问题。想象一下,您编写了一个以两个数字作为参数的函数。您不想突然发现这些数字中的任何一个都被调用函数改变了。如果您将它们作为可能的指针传递。很多事情都应该像数字一样。点、2D 向量、日期、矩形、圆形等。这些东西没有身份。不应区分同一位置、同一半径的两个圆。它们是值类型。

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

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

【讨论】:

以上是关于值接收器与指针接收器的主要内容,如果未能解决你的问题,请参考以下文章

go语言-interface的值接收和指针接受的区别

go语言-interface的值接收和指针接受的区别

go语言-interface的值接收和指针接受的区别

Golang入门到项目实战 | golang接口值类型接收者和指针类型接收者

Golang入门到项目实战 | golang接口值类型接收者和指针类型接收者

为啥接口不能用指针接收器实现