首页 文章

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

提问于
浏览
61

我不清楚在哪种情况下我会想要使用值接收器而不是总是使用指针接收器 .
从文档中回顾一下:

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 也说"For types such as basic types, slices, and small structs, a value receiver is very cheap so unless the semantics of the method requires a pointer, a value receiver is efficient and clear."

First point 它说它是"very cheap",但问题是它比指针接收器更便宜 . 所以我做了一个小的基准测试(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

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

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

2 回答

  • 3

    请注意the FAQ does mention consistency

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

    如上所述in this thread

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

    现在:

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

    Code Review comment可以提供帮助:

    如果接收器是map,func或chan,请不要使用指向它的指针 . 如果接收器是切片并且该方法不重新切片或重新分配切片,则不要使用指向它的指针 . 如果该方法需要改变接收器,则接收器必须是指针 . 如果接收器是包含sync.Mutex或类似同步字段的结构,则接收器必须是避免复制的指针 . 如果接收器是大型结构或数组,则指针接收器更有效 . 有多大?假设它相当于将所有元素作为参数传递给方法 . 如果感觉太大,对接收器来说也太大了 . 函数或方法可以同时或在从这种方法调用时改变接收器吗?在调用方法时,值类型会创建接收器的副本,因此外部更新将不会应用于此接收器 . 如果必须在原始接收器中看到更改,则接收器必须是指针 . 如果接收器是结构,数组或切片,并且其任何元素是指向可能变异的东西的指针,则更喜欢指针接收器,因为它将使读者更清楚地意图 . 如果接收器是一个小数组或结构,它自然是一个值类型(例如,类似于time.Time类型),没有可变字段和没有指针,或者只是一个简单的基本类型,如int或string, Value 接收者是有道理的值接收器可以减少可以生成的垃圾量;如果将值传递给值方法,则可以使用堆栈上的副本而不是在堆上进行分配 . (编译器试图避免这种分配,但它不能总是成功 . )不要为此而选择值接收器类型而不先进行分析 . 最后,如有疑问,请使用指针接收器 .

    例如,在_352713中找到粗体部分:

    // 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) {
    ...
    }
    
  • 81

    另外添加@VonC伟大,内容丰富的答案 .

    令我感到惊讶的是,一旦项目变得越来越大,没有人真正提到维护成本,旧开发者离开并且新的开始出现 . 肯定是一种年轻的语言 .

    一般来说,我尽量避免使用指针,但他们确实有自己的位置和美感 .

    I use pointers when:

    • 使用大型数据集

    • 具有结构维持状态,例如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
    }
    

    Reasons why I avoid pointers:

    • 指针不是同时安全的(GoLang的整点)

    • 一次指针接收器,总是指针接收器(对于所有Struct的方法一致)
      与"value copy cost"相比,

    • 互斥锁肯定更贵,更慢,更难维护

    • 说到"value copy cost",这真的是一个问题吗?过早优化是万恶之源,你可以随时添加指针

    • 它直接,有意识地迫使我设计小结构
      通过设计具有明确意图和明显I / O的纯函数,可以大大避免

    • 指针
      我相信

    • 垃圾收集更难用指针

    • 更容易争论封装,责任

    • 保持简单,愚蠢(是的,指针可能很棘手,因为你永远不知道下一个项目的DEV)

    • 单元测试就像穿过粉红色的花园(斯洛伐克只表达?),意味着简单

    • no 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)
    

    UPDATE:

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

相关问题