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

Chr*_*ort 89 pointers function 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
Run Code Online (Sandbox Code Playgroud)

文档还表示"对于类型,如基本类型,切片和小结构,值接收机是非常便宜,所以,除非所述方法的语义需要一个指针,一个值是接收机有效和明确的."

首先它说它"非常便宜",但更问题的是它比指针接收器便宜.所以我做了一个小的基准测试(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
Run Code Online (Sandbox Code Playgroud)

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

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

Von*_*onC 104

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

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

正如这个帖子中提到的:

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

现在:

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

代码评审意见可以帮助:

  • 如果接收器是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) {
...
}
Run Code Online (Sandbox Code Playgroud)

  • `有关接收器的指针与值的规则是可以在指针和值上调用值方法,但指针方法只能在指针上调用`实际上不是真的.值接收器和指针接收器方法都可以在正确类型的指针或非指针上调用.无论调用何种方法,在方法体内,接收器的标识符在使用值接收器时引用副本值,在使用指针接收器时引用指针:请参阅https://play.golang .ORG/p/3WHGaAbURM (11认同)
  • 很好的答案,但我强烈不同意这一点:“因为它将使意图更加明确”,NOPE,干净的API,X作为参数和Y作为返回值是明确的意图。通过指针传递Struct并花一些时间仔细阅读代码以检查所有属性是否被修改,这远非清晰,可维护。 (3认同)
  • 有一个很好的解释[这里](https://github.com/golang/go/wiki/MethodSets#variables)"如果x是可寻址的并且&x的方法集包含m,则xm()是(&x).m的缩写()." (2认同)
  • 我很惊讶这里没有人谈论接口方法,因为它们可能会导致非常微妙的错误。按照接收器类型应该一致的建议,如果您有一个指针接收器,那么您的“(p *type) String() string”方法也应该使用指针接收器,但这*不*实现“Stringer”接口,除非你的API的调用者也使用指向你的类型的指针,这可能是你的API的可用性问题。我不知道这里的一致性是否胜过可用性。 (2认同)

Blo*_*kas 13

另外添加@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
}
Run Code Online (Sandbox Code Playgroud)

我避免指针的原因:

  • 指针不是同时安全的(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) 
Run Code Online (Sandbox Code Playgroud)

更新:

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

  • 有趣的是我的答案。+1 (2认同)