值接收器与指针接收器

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

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

(编辑:请注意第二点在较新的 go 版本中无效,请参阅评论。)

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

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

注意 the FAQ does mention consistency

Next is consistency. If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used. See the section on method sets for details.

如前所述in this thread:

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

这不是真的,因为 by Sart Simha

Both value receiver and pointer receiver methods can be invoked on a correctly-typed pointer or non-pointer.

Regardless of what the method is called on, within the method body the identifier of the receiver refers to a by-copy value when a value receiver is used, and a pointer when a pointer receiver is used: example.

现在:

Can someone tell me a case where a value receiver clearly makes more sense then a pointer receiver?

Code Review comment可以帮助:

  • 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 points out in 关于接口方法的警告

Following the advice that the receiver type should be consistent, if you have a pointer receiver, then your (p *type) String() string method should also use a pointer receiver.

But this does not implement the Stringer interface, unless the caller of your API also uses pointer to your type, which might be a usability problem of your API.

I don't know if consistency beats usability here.


指出:

  • ""

you can mix and match methods with value receivers and methods with pointer receivers, and use them with variables containing values and pointers, without worrying about which is which.
Both will work, and the syntax is the same.

However, if methods with pointer receivers are needed to satisfy an interface, then only a pointer will be assignable to the interface — a value won't be valid.

Calling value receiver methods through interfaces always creates extra copies of your values.

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

Concept of unaddressable values, which are the opposite of addressable values. The careful technical version is in the Go specification in Address operators, but the hand waving summary version is that most anonymous values are not addressable (one big exception is composite literals)

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

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

一般来说,我尽量避免使用指针,但它们确实有其用武之地和美感。

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

  • 处理大型数据集
  • 有一个结构维护状态,例如令牌缓存,
    • 我确保所有字段都是私有的,只能通过定义的方法接收器进行交互
    • 我不会将此函数传递给任何 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 方法的一致性)
  • 与 "value copy cost"
  • 相比,互斥体肯定更昂贵、更慢且更难维护
  • 说到 "value copy cost",这真的是个问题吗?不成熟的优化是万恶之源,以后可以随时指点
  • 它直接有意识地迫使我设计小型结构
  • 可以通过设计意图明确且明显的纯函数来避免大部分指针 I/O
  • 垃圾收集更难,我相信指针
  • 更容易争论封装、责任
  • 保持简单,愚蠢(是的,指针可能很棘手,因为你永远不知道下一个项目的开发者)
  • 单元测试就像穿过粉红色的花园(只有斯洛伐克语表达?),意味着简单
  • 没有 NIL if conditions(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) 

更新:

这个问题激发了我对这个主题进行更多研究并写了一篇博客 post 关于它 https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

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

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

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