繁体   English   中英

值接收器与指针接收器

[英]Value receiver vs. pointer receiver

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

从文档中回顾一下:

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)

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

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

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

正如这个线程中提到的

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

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

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

无论调用什么方法,在方法体内,接收者的标识符在使用值接收器时指的是一个复制值,在使用指针接收器时指的是指针: 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) {
...
}

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

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

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

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


指出:

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

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

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

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

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

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

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

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

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

  • 处理大型数据集
  • 有一个结构维护状态,例如 TokenCache,
    • 我确保所有字段都是私有的,只能通过定义的方法接收器进行交互
    • 我没有将此函数传递给任何 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 if 条件(可以在预期指针的地方传递 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-orientated-golang-4fa62b88c701

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM