Golang中的值接收器与指针接收器?

时间:2015-01-05 07:54:12

标签: function pointers 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

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

(编辑:请注意第二点在较新的版本中无效,请参阅注释)
第二点它说,它是高效和清晰的#34;这更像是一种品味问题,不是吗?就个人而言,我喜欢以任何方式使用相同的方式。效率在什么意义上?性能方面看来,指针几乎总是更有效率。使用一个int属性的少量测试运行显示出Value接收器的最小优势(范围为0.01-0.1 ns / op)

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

3 个答案:

答案 0 :(得分:91)

请注意the FAQ does mention consistency

  

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

如上所述in this thread

  

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

现在:

  

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

Code Review comment可以提供帮助:

  
      
  • 如果接收器是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) {
...
}

答案 1 :(得分:8)

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

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

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

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

  • 使用大型数据集
  • 具有结构保持状态,例如令牌缓存,
    • 我确保所有字段均为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

答案 2 :(得分:0)

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

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

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