对于我来说,在哪种情况下我想使用值接收器而不是总是使用指针接收器是非常不清楚的。

从文件中总结一下:

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

文档还说:“对于基本类型、切片和小结构等类型,值接收器非常便宜,所以除非方法的语义需要指针,否则值接收器是有效和清晰的。”

第一点,他们说值接收器“非常便宜”,但问题是它是否比指针接收器便宜。所以我做了一个小的基准测试(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版本中无效,见评论。)

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

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


注意,FAQ中提到了一致性

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

如本文所述:

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

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

值接收器和指针接收器方法都可以在正确类型的指针或非指针上调用。 不管对什么方法调用,在方法体中,当使用值接收器时,接收器的标识符指向逐拷贝值,当使用指针接收器时,接收器指向指针:示例。

Now:

有人能告诉我一个例子,一个值接收器明显比一个指针接收器更有意义吗?

代码评审注释可以帮助:

If the receiver is a map, func or chan, don't use a pointer to it. If the receiver is a slice and the method doesn't reslice or reallocate the slice, don't use a pointer to it. If the method needs to mutate the receiver, the receiver must be a pointer. If the receiver is a struct that contains a sync.Mutex or similar synchronizing field, the receiver must be a pointer to avoid copying. If the receiver is a large struct or array, a pointer receiver is more efficient. How large is large? Assume it's equivalent to passing all its elements as arguments to the method. If that feels too large, it's also too large for the receiver. Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer. If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating, prefer a pointer receiver, as it will make the intention more clear to the reader. If the receiver is a small array or struct that is naturally a value type (for instance, something like the time.Time type), with no mutable fields and no pointers, or is just a simple basic type such as int or string, a value receiver makes sense. A value receiver can reduce the amount of garbage that can be generated; if a value is passed to a value method, an on-stack copy can be used instead of allocating on the heap. (The compiler tries to be smart about avoiding this allocation, but it can't always succeed.) Don't choose a value receiver type for this reason without profiling first. Finally, when in doubt, use a pointer receiver.

粗体部分可以在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()字符串方法也应该使用一个指针接收器。 但是这并没有实现Stringer接口,除非API的调用者也使用指向类型的指针,这可能是API的可用性问题。 我不知道这里的一致性是否胜过可用性。


指出:

方法集(指针vs值接收器)

您可以将方法与值接收器和方法与指针接收器混合和匹配,并将它们与包含值和指针的变量一起使用,而不用担心哪个是哪个。 两者都可以工作,而且语法是相同的。 然而,如果需要带有指针接收器的方法来满足接口,那么只能将指针赋值给接口——值将无效。

“Go接口和自动生成函数”,摘自Chris Siebenmann(2017年6月)

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

接口值基本上是指针,而你的值接收器方法需要值;因此,每次调用都需要Go创建一个新的值副本,用它调用你的方法,然后扔掉这个值。 只要使用值接收器方法并通过接口值调用它们,就无法避免这种情况;这是围棋的基本要求。

“了解Go的不可寻址值和切片”(剧照来自克里斯(2018年9月))

不可寻址值的概念,与可寻址值相反。谨慎的技术版本是在Go规范的地址操作符中,但是简单的总结版本是大多数匿名值是不可寻址的(一个大的例外是复合字面量)


补充@VonC伟大的,翔实的答案。

我很惊讶,当项目变大,老开发者离开,新开发者出现时,没有人真正提到维护成本。围棋当然是一门年轻的语言。

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

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

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

E.g:

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
}

我避免使用指针的原因:

pointers are not concurrently safe (the whole point of GoLang) once pointer receiver, always pointer receiver (for all Struct's methods for consistency) mutexes are surely more expensive, slower and harder to maintain comparing to the "value copy cost" speaking of "value copy cost", is that really an issue? Premature optimization is root to all evil, you can always add pointers later it directly, conciously forces me to design small Structs pointers can be mostly avoided by designing pure functions with clear intention and obvious I/O garbage collection is harder with pointers I believe easier to argue about encapsulation, responsibilities keep it simple, stupid (yes, pointers can be tricky because you never know the next project's dev) unit testing is like walking through pink garden (slovak only expression?), means easy no NIL if conditions (NIL can be passed where a pointer was expected)

我的经验法则是,写尽可能多的封装方法,比如:

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


It is a question of semantics. Imagine you write a function taking two numbers as arguments. You don't want to suddenly find out that either of these numbers got mutated by the calling function. If you pass them as pointers that is possible. Lots of things should act just like numbers. Things like points, 2D vectors, dates, rectangles, circles etc. These things don't have identity. Two circle at the same position and with the same radius should not be distinguished from each other. They are value types.

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

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