英文:
Is there a performance penalty for passing "this" by value in Go methods?
问题
我在9年的C++开发之后开始探索Go。在C++中,除了内置类型的变量之外,按值传递函数参数是一种不好的做法,因为会有性能损失:参数的所有字段都会被复制,而且在大多数情况下,这将是一个非常昂贵的操作。
在Go中是否也是如此?将“this”按值传递只是为了给方法赋予“const”语义,这看起来非常昂贵。Go编译器是否足够聪明,能够在第一次修改之前防止变量被复制?为什么在Go中将“this”按值传递不像在C/C++中那样是一种反模式?
英文:
I'm exploring Go after 9 years of C++ development. In C++ it is a bad practice to pass function's arguments by value except variables of built-in types because of performance penalty: all fields of the argument will be copied and in most cases it will be a very costly operation.
Is this true for Go? It looks very expensive to pass "this" by value only to assign "const" semantic to the method. Is Go compiler smart enough to prevent variable from being copied before first modification? Why isn't passing "this" by value an anti-pattern in Go as it is in C/C++?
答案1
得分: 13
其他答案都很好,但在我看来,有一些信息是缺失的。
在Go语言中,接收者只是一种语法糖,可以通过以下代码进行演示:
package main
import "fmt"
type Something struct {
Value int
}
func (s *Something) ChangeValue(n int) {
s.Value = n
}
func main() {
o := new(Something) // o的类型是*Something
fmt.Println(o.Value) // 输出0
o.ChangeValue(8) // 将o.Value改为8
fmt.Println(o.Value) // 输出8
(*Something).ChangeValue(o, 16) // 等同于调用o.ChangeValue(16)
fmt.Println(o.Value) // 输出16
}
根据这个例子,考虑一下如果ChangeValue
的接收者是Something
类型的值而不是指针会发生什么...
没错!你实际上无法通过这个方法来改变o
的Value
字段。大多数情况下,你使用指针接收者来实现封装。
英文:
The other answers are good but in my opinion, there's some information missing.
Receivers in Go are just syntactic sugar, as demonstrated by the following code:
package main
import "fmt"
type Something struct {
Value int
}
func (s *Something) ChangeValue(n int) {
s.Value = n
}
func main() {
o := new(Something) // o is of type *Something
fmt.Println(o.Value) // Prints 0
o.ChangeValue(8) // Changes o.Value to 8
fmt.Println(o.Value) // Prints 8
(*Something).ChangeValue(o, 16) // Same as calling o.ChangeValue(16)
fmt.Println(o.Value) // Prints 16
}
Based on this, consider what would happen if the receiver of ChangeValue
was a value of type Something
instead of a pointer to one...
That's right! You could never actually mutate o
's Value
field through this method. Most of the time, you use pointer receivers to do encapsulation.
答案2
得分: 6
在Go语言中,"this"被称为接收器。是的,使用非指针接收器来模拟"const"语义可能非常昂贵。但是Go语言出于很好的原因放弃了"const"修饰符。因此,以不必要的复制为代价来接管特定的语言设计决策可能不是一个好主意,特别是在处理超过几个机器字的情况下。
顺便说一句,"this"或"self"与"receiver"之间的术语差异意味着它们也具有不同的语义。我记得,在其他一些语言中,不能更改"this"或"self"的值,但在Go语言中,接收器只是另一个函数参数(实际上是从编译器的角度来看的第一个参数)。
也就是说,这就是我不鼓励使用变量名为"this"或"self"的方式编写方法的原因。对于习惯于其他一些语言的人来说,这是具有误导性的。
下面是一个完全虚构的例子,希望能够说明这个想法:
func (n *node) walk(f func(*node)) {
for n != nil {
f(n)
n = n.next
}
}
英文:
"this" in Go is called a receiver. Yes, it may be very expensive to use a non pointer receiver only to emulate a "const" semantics. But Go dropped the "const" modifier for good reasons. Thus, it's probably not a good idea to take over that specific language design decision at the cost of unnecessary copying - in the case of anything bigger than few machine words.
BTW, the terminology difference between "this" or "self" and "receiver" implies it has also different semantics. IIRC, one cannot change the value of "this" or "self" in some other languages, but in Go, the receiver is just another function parameter (actually the first one from the compiler's point of view).
That said, this is the reason I discourage writing methods in which the receiver variable is named this
or self
. It's misleading for people used to some other languages.
A completely made-up example illustrating hopefully the idea:
func (n *node) walk(f func(*node)) {
for n != nil {
f(n)
n = n.next
}
}
答案3
得分: 6
我会说你的C++知识在Go中的转换效果很好,关于函数参数的昂贵性(通过值传递结构体)和不昂贵性(内置类型,例如int)。
主要的区别在于引用类型、切片、映射和通道。尽管它们看起来是通过值传递的(你不需要使用指针),但实际上是通过引用传递的,所以通常不要使用指向切片、映射或通道的指针。
字符串也是特殊的 - 它们在底层是引用类型,但它们也是不可变的,所以直接传递它们。
至于this
或在Go中称为接收器的特定情况 - 同样适用相同的规则(请注意,你可以将内置类型作为接收器,不像C++),我认为编译器不够聪明以避免复制,所以对于大型结构体使用指针。
英文:
I would say your C++ knowledge will translate fine into Go about what is expensive as a function argument (passing structs by value) and what isn't (builtin types, eg int).
The major difference would be the reference types, slices, map
s and channel
s. These, though they appear to be passed by value (you don't need to use a pointer) are actually passed by reference, so don't in general use a pointer to a slice, map or channel.
string
s are also special - they are reference types under the hood, but they are also immutable, so pass them around directly.
As for the specific case of this
or the receiver as it is called in Go - same rules apply (note that you can have builtin types as a receiver unlike C++), and I don't think the compiler is smart enough to avoid copies, so use a pointer for large structs.
答案4
得分: 5
这取决于接收器的大小。如果接收器小于几十个字节,复制它实际上可能比传递指针需要的指针追踪(额外的内存访问)更便宜。此外,使用指针使得结构体更有可能在堆上分配,给垃圾回收器增加了额外负担。
在Go中,复制总是按字节复制,因此成本仅取决于结构体的大小。在C++中,它可能调用一个复制构造函数,这可能需要很长时间。
因此,除了非常大的对象,根据方法的语义和与API的一致性,只需使用最合适的接收器类型。
英文:
It depends on the size of the receiver. If the receiver is less than a few dozen bytes, copying it might actually be cheaper than the pointer chasing (extra memory accesses) that would be required if you passed a pointer. Also, using a pointer makes it somewhat more likely that the struct will be allocated on the heap, putting an extra burden on the garbage collector.
In Go, the copy is always a byte-by-byte copy, so the cost depends only on the size of the struct. In C++ it might call a copy constructor, which could potentially take a lot of time.
So, except for really big objects, just use whatever kind of receiver makes the most sense based on the semantics of the method and consistency with the rest of your API.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论