Go语言 价值接收者与指针式接收机

u7up0aaq  于 2023-05-20  发布在  Go
关注(0)|答案(3)|浏览(113)

我不清楚在哪种情况下我会使用值接收器而不是总是使用指针接收器。
从文档中总结:

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)

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

qzwqbdag

qzwqbdag1#

请注意,FAQ中确实提到了一致性
其次是一致性。如果类型的某些方法必须有指针接收器,那么其余的也应该有,所以无论类型如何使用,方法集都是一致的。有关详细信息,请参见方法集部分。
in this thread所述:
关于指针的规则接收器的值的一个特点是值方法可以在指针和值上调用,但指针方法只能在指针上调用
这是不正确的,正如Sart Simha所评论的那样。
值接收器和指针接收器方法都可以在正确类型的指针或非指针上调用。
无论调用什么方法,在方法体中,当使用值接收器时,接收器的标识符引用by-copy值,当使用指针接收器时,接收器的标识符引用指针:example
现在:
有人能告诉我一个例子,值接收器比指针接收器更有意义吗?
Code Review注解可以帮助:

  • 如果接收器是map、func或chan,不要使用指向它的指针。
  • 如果接收器是一个切片,并且该方法不重新切片或重新分配切片,则不要使用指向它的指针。
  • 如果方法需要改变接收器,则接收器必须是指针。
  • 如果接收器是一个包含sync.Mutex或类似同步字段的结构体,则接收器必须是一个指针以避免复制。
  • 如果接收器是一个大的结构体或数组,指针接收器会更有效。多大才算大?假设它等价于将其所有元素作为参数传递给方法。如果这感觉太大,它也太大的接收器。
  • 函数或方法,无论是并发调用还是从该方法调用,是否会改变接收方?值类型在调用方法时创建接收器的副本,因此外部更新将不会应用于此接收器。如果更改必须在原始接收器中可见,则接收器必须是指针。
  • 如果接收器是一个结构体,数组或切片,并且它的任何元素都是指向可能发生变化的东西的指针,那么最好使用指针接收器,因为它会使读者更清楚地了解意图。
    *如果接收器是一个小数组或结构体,它自然是一个值类型(例如,类似time.Time类型),没有可变字段和指针,或者只是一个简单的基本类型,如int或string,值接收器是有意义的
    值接收器可以减少可以生成的垃圾量;如果一个值被传递给一个值方法,那么可以使用一个栈上的副本来代替在堆上分配。(编译器试图聪明地避免这种分配,但它并不总是成功的。)不要因为这个原因而在没有分析的情况下选择一个值接收器类型。
  • 最后,当有疑问时,使用指针接收器。

注意“如果接收器是一个切片,并且方法不重新切片或重新分配切片,则不要使用指向它的指针。
该语句建议,如果您有一个重新切片或重新分配切片的方法,那么您应该使用指针接收器。
换句话说,如果在方法中修改切片,比如追加元素或更改切片的长度/容量,建议使用指针接收器。
在为切片类型实现删除和插入方法的情况下,您可能会修改切片(更改其长度,添加或删除元素)。因此,您应该为这些方法使用指针接收器。
示例(playground):

package main

import "fmt"

type MySlice []int

func (s *MySlice) Insert(index int, value int) {
    // Insert value at index and shift elements
    *s = append((*s)[:index], append([]int{value}, (*s)[index:]...)...)
}

func (s *MySlice) Delete(index int) {
    // Remove the element at index and shift elements
    *s = append((*s)[:index], (*s)[index+1:]...)
}

func main() {
    s := MySlice{1, 2, 3, 4, 5}
    s.Insert(2, 42)
    fmt.Println(s) // Output: [1 2 42 3 4 5]

    s.Delete(2)
    fmt.Println(s) // Output: [1 2 3 4 5]
}

在本例中,InsertDelete方法通过添加和删除元素来修改切片。
因此,使用指针接收器来确保修改在方法外部可见。
例如,在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() string方法也应该使用指针接收器。
但是这并没有实现Stringer接口,除非你的API的调用者也使用指向你的类型的指针,这可能是你的API的可用性问题。
我不知道是否一致性击败可用性在这里。
指出:

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

接口值基本上是指针,而你的值接收器方法需要值;因此每次调用都需要Go创建一个新的值副本,用它调用你的方法,然后扔掉这个值。

只要使用值接收器方法并通过接口值调用它们,就没有办法避免这种情况;这是围棋的基本要求

  • Learning about Go's unaddressable values and slicing”(仍然来自克里斯(9月2018年))
    不可寻址值的概念,与可寻址值相反。详细的技术版本在Go语言规范中的Address操作符中,但总结的版本是大多数匿名值都是不可寻址的(一个很大的例外是复合文字)
bxgwgixi

bxgwgixi2#

另外添加到@VonC伟大的,信息丰富的答案。
我很惊讶,没有人真正提到维护成本,一旦项目变得更大,旧的开发人员离开,新的来了。Go是一门年轻的语言。
一般来说,我尽量避免指针时,我可以,但他们确实有自己的地方和美丽。

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

  • 使用大型数据集
  • 有一个结构维护状态,例如TokenCache,
    • 我确保所有字段都是私有的,只有通过定义的方法接收器才能进行交互
    • 我不把这个函数传递给任何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明显的纯函数,可以在很大程度上避免指针
  • 我相信使用指针进行垃圾收集会更困难
  • 更容易讨论封装、责任
  • 保持简单,愚蠢(是的,指针可能很棘手,因为你永远不知道下一个项目的开发)
  • 单元测试就像是在粉红色的花园里散步(斯洛伐克语的表达方式?),意思是容易
    *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)

更新:

这个问题启发了我对这个主题进行更多的研究,并就此写了一篇博客文章https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

ie3xauqp

ie3xauqp3#

这是一个语义学的问题。假设你写了一个函数,以两个数字作为参数。你不想突然发现这些数字中的任何一个被调用函数改变了。如果你把它们作为指针传递,这是可能的。很多事情应该表现得像数字一样。像点,2D矢量,日期,矩形,圆形等。这些东西没有身份。两个在同一位置且半径相同的圆不应相互区分。它们是值类型。
但是,像数据库连接或文件句柄这样的东西,GUI中的按钮是身份重要的东西。在这些情况下,你需要一个指向对象的指针。
当某个东西本质上是一个值类型时,比如矩形或点,最好能够在不使用指针的情况下传递它们。为什么?因为这意味着你肯定会避免改变对象。它向代码的读者阐明了语义和意图。很明显,接收对象的函数不能也不会改变对象。

相关问题