值接收器与指针接收器


107

对于我而言,目前尚不清楚,在这种情况下,我想使用值接收器而不是始终使用指针接收器。
回顾一下文档:

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

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


3
我使用单个字符串字段以及两个字段(字符串字段和整数字段)运行了类似的基准测试。从价值接收者那里我得到了更快的结果。BenchmarkChangePointerReceiver-4 10000000000 0.99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0.33 ns / op这正在使用Go 1.8。我想知道自您上次运行基准测试以来是否进行了编译器优化。有关更多详细信息,请参见要点
pbitty

2
你是对的。使用Go1.9运行原始基准测试,我现在也得到了不同的结果。指针接收器0.60 ns / op,值接收器0.38 ns / op
Chrisport

Answers:


117

请注意,FAQ确实提到了一致性

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

本主题所述

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

现在:

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

代码评审意见可以帮助:

  • 如果接收方是地图,函数或频道,请不要使用指向它的指针。
  • 如果接收方是切片,并且该方法没有重新切片或重新分配切片,请不要使用指向它的指针。
  • 如果该方法需要更改接收方,则接收方必须是指针。
  • 如果接收方是包含一个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) {
...
}

16
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
哈特·

3
有一个很好的解释在这里 “如果x是寻址和&X的方法集包含男,XM()是(X).M()的简写。”
TERA


4
很好的答案,但我强烈不同意这一点:“因为它将使意图更加明确”,NOPE,干净的API,X作为参数和Y作为返回值是明确的意图。通过指针传递Struct并花时间仔细阅读代码以检查所有属性被修改的内容还远非清晰,可维护。
卢卡斯·卢卡奇

@HartSimha我认为上面的帖子指出了以下事实:指针接收器方法不在值类型的“方法集”中。在链接的游乐场中,添加以下行会导致编译错误:Int(5).increment_by_one_ptr()。同样,定义该方法的特征increment_by_one_ptr将不满足type的值Int
Gaurav Agarwal

16

要在@VonC上添加更多有用的答案。

我感到惊讶的是,一旦项目规模扩大,旧开发人员离开而新开发人员来了,没有人真正提到维护成本。肯定是一种年轻的语言。

一般来说,我会尽量避免使用指针,但是它们确实有其应有的地位和美丽。

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

  • 处理大型数据集
  • 具有结构维护状态,例如TokenCache,
    • 我确保所有字段都是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的任何副本都将引用相同的映射。看到这个游乐场-play.golang.com/p/Xda1rsGwvhq
Rich

很高兴我们团结一致。好点。实际上,我认为我在此示例中使用了指针,因为我从一个项目中复制了指针,在该项目中TokenCache不仅处理该映射,而且还处理更多的东西。如果我在一种方法中使用了指针,则在所有方法中都使用它。您是否建议从此特定的SO示例中删除指针?
卢卡斯·卢卡奇

大声笑,复制/粘贴再次罢工!😉IMO,您可以保留它,因为它说明了容易陷入的陷阱,或者可以用显示状态和/或大型数据结构的东西替换地图。
Rich

好吧,我确定他们会阅读评论的... PS:丰富,您的论点似乎很合理,很高兴能在LinkedIn上添加我(我个人资料中的链接)。
卢卡斯·卢卡奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.