这个“pattern”背后的动机是什么?

huangapple go评论82阅读模式
英文:

What is the motivation behind this "pattern"?

问题

我看到这样的代码时有点困惑:

bigBox := &BigBox{}
bigBox.BubbleGumsCount = 4          // 正确...
bigBox.SmallBox.AnyMagicItem = true // 也是正确的

为什么或者何时我会选择使用 bigBox := &BigBox{} 而不是 bigBox := BigBox{}?这样做有什么效率上的优势吗?

代码示例来自这里

示例2:

package main

import "fmt"

type Ints struct {
  x int
  y int
}

func build_struct() Ints {
  return Ints{0,0}
}

func build_pstruct() *Ints {
  return &Ints{0,0}
}

func main() {
  fmt.Println(build_struct())
  fmt.Println(build_pstruct())
}

示例3:(为什么在这个例子中我要使用 &BigBox,而不是直接使用 BigBox 作为结构体?)

func main() {
  bigBox := &BigBox{}
  bigBox.BubbleGumsCount = 4 
  fmt.Println(bigBox.BubbleGumsCount)
}

有没有理由调用 build_pstruct 而不是 build_struct 变体?难道这不是垃圾回收的作用吗?

英文:

I'm a bit confused when I see code such as:

bigBox := &BigBox{}
bigBox.BubbleGumsCount = 4          // correct...
bigBox.SmallBox.AnyMagicItem = true // also correct

Why, or when, would I want to do bigBox := &BigBox{} instead of bigBox := BigBox{} ? Is it more efficient in some way?

Code sample was taken from here.

Sample no.2:

package main

import "fmt"

type Ints struct {
  x int
  y int
}

func build_struct() Ints {
  return Ints{0,0}
}

func build_pstruct() *Ints {
  return &Ints{0,0}
}

func main() {
  fmt.Println(build_struct())
  fmt.Println(build_pstruct())
}

Sample no. 3: ( why would I go with &BigBox in this example, and not with BigBox as a struct directly ? )

func main() {
  bigBox := &BigBox{}
  bigBox.BubbleGumsCount = 4 
  fmt.Println(bigBox.BubbleGumsCount)
}

Is there ever a reason to call build_pstruct instead of the the build_struct variant? Isn't that why we have the GC?

答案1

得分: 5

我找到了这种代码的一个动机:避免“意外复制结构体”。

如果你使用一个结构体变量来保存新创建的结构体:

bigBox := BigBox{}

你可能会意外地复制结构体,就像这样:

myBox := bigBox // 你只想要 bigBox 的引用。
myBox.BubbleGumsCount = 4

或者像这样:

changeBoxColorToRed(bigBox)

其中 changeBoxColorToRed 是:

// 它将整个结构体作为参数进行复制。
func changeBoxColorToRed(box bigBox){
    // !!!! 这个函数有 bug,它不会按预期工作 !!!
    // 请看最后的修复方法。
    box.Color = red
}

但是如果你使用结构体指针:

bigBox := &BigBox{}

在以下情况中不会发生复制:

myBox := bigBox

并且

changeBoxColorToRed(bigBox)

将无法编译通过,这给了你重新思考 changeBoxColorToRed 设计的机会。修复方法很明显:

func changeBoxColorToRed(box *bigBox){
    box.Color = red
}

新版本的 changeBoxColorToRed 不会复制整个结构体,并且可以正常工作。

英文:

I figured out one motivation for this kind of code: avoidance of "struct copying by accident".


If you use a struct variable to hold the newly created struct:

bigBox := BigBox{}

you may copy the struct by accident like this

myBox := bigBox // Where you just want a refence of bigBox.
myBox.BubbleGumsCount = 4

or like this

changeBoxColorToRed(bigBox)

where changeBoxColorToRed is

// It makes a copy of entire struct as parameter. 
func changeBoxColorToRed(box bigBox){
    // !!!! This function is buggy. It won't work as expected !!!
    // Please see the fix at the end.
    box.Color=red
}

But if you use a struct pointer:

bigBox := &BigBox{}

there will be no copying in

myBox := bigBox

and

changeBoxColorToRed(bigBox)

will fail to compile, giving you a chance to rethink the design of changeBoxColorToRed. The fix is obvious:

func changeBoxColorToRed(box *bigBox){
    box.Color=red
}

The new version of changeBoxColorToRed does not copy the entire struct and works correctly.

答案2

得分: 3

bb := &BigBox{} 创建了一个结构体,但将变量设置为指向它的指针。这与 bb := new(BigBox) 是相同的。另一方面,bb := BigBox{} 直接将bb作为类型为BigBox的变量。如果你想要一个指针(因为可能你将通过指针使用数据),那么最好将bb设置为指针,否则你将经常写 &bb。如果你要直接将数据作为结构体使用,那么你希望bb是一个结构体,否则你将使用 *bb 进行解引用。

这超出了问题的范围,但通常最好一次性创建数据,而不是通过创建对象然后逐步更新它。

bb := &BigBox{
    BubbleGumsCount: 4,
    SmallBox: {
        AnyMagicItem: true,
    },
}
英文:

bb := &BigBox{} creates a struct, but sets the variable to be a pointer to it. It's the same as bb := new(BigBox). On the other hand, bb := BigBox{} makes bb a variable of type BigBox directly. If you want a pointer (because perhaps because you're going to use the data via a pointer), then it's better to make bb a pointer, otherwise you're going to be writing &bb a lot. If you're going to use the data as a struct directly, then you want bb to be a struct, otherwise you're going to be dereferencing with *bb.

It's off the point of the question, but it's usually better to create data in one go, rather than incrementally by creating the object and subsequently updating it.

bb := &BigBox{
    BubbleGumsCount: 4,
    SmallBox: {
        AnyMagicItem: true,
    },
}

答案3

得分: 2

&表示取地址操作符。它的意思是“我想要一个指针”,而不是“我想要一个实例”。包含值的变量的大小取决于值的大小,可能是大的或小的。包含指针的变量的大小是8个字节。

以下是示例及其含义:

bigBox0 := &BigBox{} // bigBox0是指向BigBox{}实例的指针
bigBox1 := BigBox{} // bigBox1包含BigBox{}的实例
bigBox2 := bigBox // bigBox2是bigBox的副本
bigBox3 := &bigBox // bigBox3是bigBox的指针
bigBox4 := *bigBox3 // bigBox4是bigBox的副本,从bigBox3(一个指针)解引用而来

为什么要使用指针?

  1. 防止在将大对象作为参数传递给函数时进行复制。
  2. 通过将其作为参数传递来修改值。
  3. 保持由数组支持的切片的大小较小。[10]BigBox将占用“BigBox的大小”* 10字节。[10]BigBox将占用8字节 10。当切片调整大小时,当达到其容量时,必须创建一个更大的数组。这意味着旧数组的内存必须复制到新数组。

为什么不使用指针?

  1. 如果对象很小,最好只复制一个副本。特别是如果它的大小<= 8字节。
  2. 使用指针可能会创建垃圾。这些垃圾必须由垃圾收集器收集。垃圾收集器是一种标记-清除的停止-世界实现。这意味着它必须冻结您的应用程序以收集垃圾。它需要收集的垃圾越多,暂停的时间就越长。例如,这个人经历了长达10秒的暂停。
  3. 复制对象使用堆栈而不是堆。堆栈通常比堆快得多。在Go中,您真的不必考虑堆栈与堆,因为它决定了应该将什么放在哪里,但也不应忽视它。这真的取决于编译器的实现,但指针可能导致内存进入堆,从而需要进行垃圾收集。
  4. 直接内存访问更快。如果您有一个切片[]BigBox,并且它的大小不会改变,那么访问速度可能更快。[]BigBox读取速度更快,而[]*BigBox调整大小速度更快。

我的一般建议是谨慎使用指针。除非处理需要传递的非常大的对象,否则通常最好在堆栈上传递副本。减少垃圾是一件大事。垃圾收集器会变得更好,但您最好尽量保持垃圾量最低。

像往常一样,测试您的应用程序并进行性能分析

英文:

The &amp; takes an address of something. So it means "I want a pointer to" rather than "I want an instance of". The size of a variable containing a value depends on the size of the value, which could be large or small. The size of a variable containing a pointer is 8 bytes.

Here are examples and their meanings:

bigBox0 := &amp;BigBox{} // bigBox0 is a pointer to an instance of BigBox{}
bigBox1 := BigBox{} // bigBox1 contains an instance of BigBox{}
bigBox2 := bigBox // bigBox2 is a copy of bigBox
bigBox3 := &amp;bigBox // bigBox3 is a pointer to bigBox
bigBox4 := *bigBox3 // bigBox4 is a copy of bigBox, dereferenced from bigBox3 (a pointer)

Why would you want a pointer?

  1. To prevent copying a large object when passing it as an argument to a function.
  2. You want to modify the value by passing it as an argument.
  3. To keep a slice, backed by an array, small. [10]BigBox would take up "the size of BigBox" * 10 bytes. [10]*BigBox would take up 8 bytes * 10. A slice when resized has to create a larger array when it reaches its capacity. This means the memory of the old array has to be copied to the new array.

Why do you not what to use a pointer?

  1. If an object is small, it's better just to make a copy. Especially if it's <= 8 bytes.
  2. Using pointers can create garbage. This garbage has to be collected by the garbage collector. The garbage collector is a mark-and-sweep stop-the-world implementation. This means that it has to freeze your application to collect the garbage. The more garbage it has to collect, the longer that pause is. This individual, for example. experienced a pause up to 10 seconds.
  3. Copying an object uses the stack rather than the heap. The stack is usually always faster than the heap. You really don't have to think about stack vs heap in Go as it decides what should go where, but you shouldn't ignore it either. It really depends on the compiler implementation, but pointers can result in memory going on the heap, resulting in the need for garbage collection.
  4. Direct memory access is faster. If you have a slice []BigBox and it doesn't change size it can be faster to access. []BigBox is faster to read, whereas []*BigBox is faster to resize.

My general advice is use pointers sparingly. Unless you're dealing with a very large object that needs to be passed around, it's often better to pass around a copy on the stack. Reducing garbage is a big deal. The garbage collector will get better, but you're better off by keeping it as low as possible.

As always test your application and profile it.

答案4

得分: 1

创建引用对象(使用&符号)与创建值对象(不使用&符号)之间的区别在于,引用对象是对现有对象的引用,而值对象是对对象的实际值的复制。

关于值传递和引用传递的一般概念,这里有一个很好的解释... https://stackoverflow.com/questions/373419/whats-the-difference-between-passing-by-reference-vs-passing-by-value

关于Go语言中这些概念的讨论,可以参考这里... http://www.goinggo.net/2013/07/understanding-pointers-and-memory.html

英文:

The difference is between creating a reference object (with the ampersand) vs. a value object (without the ampersand).

There's a nice explanation of the general concept of value vs. reference type passing here... https://stackoverflow.com/questions/373419/whats-the-difference-between-passing-by-reference-vs-passing-by-value

There is some discussion of these concepts with regards to Go here... http://www.goinggo.net/2013/07/understanding-pointers-and-memory.html

答案5

得分: 1

一般来说,&BigBox{}BigBox{}之间没有区别。只要语义正确,Go编译器可以自由地进行任何操作。

以下是汇编代码的总结:

  • StructToStruct:13行,没有分配内存
  • PointerToStruct:16行,没有分配内存
  • StructToPointer:20行,堆上分配内存
  • PointerToPointer:12行,堆上分配内存

如果编译器完美无缺,*ToStruct函数和*ToPointer函数将是相同的。Go的逃逸分析足够好,可以判断指针是否跨模块边界逃逸。编译器将选择最高效的方式进行操作。

如果你真的对微观优化很在意,注意在语法与语义相符的情况下,Go的效率最高(结构体用作结构体,指针用作指针)。或者你可以忘记这些,按照变量的使用方式声明变量,大多数情况下都是正确的。

注意:如果Foo非常大,PointerToStruct将在堆上分配内存。规范中甚至威胁说StructToStruct也可以这样做,但我无法实现。这里的教训是编译器会按照自己的意愿进行操作。就像寄存器的细节对代码是屏蔽的一样,堆/栈的状态也是如此。不要因为你认为自己知道编译器如何使用堆而改变你的代码。

英文:

In general there is no difference between a &amp;BigBox{} and BigBox{}. The Go compiler is free to do whatever it likes as long as the semantics are correct.

func StructToStruct() {
	s := Foo{}
	StructFunction(&amp;s)
}

func PointerToStruct() {
	p := &amp;Foo{}
	StructFunction(p)
}

func StructToPointer() {
	s := Foo{}
	PointerFunction(&amp;s)
}

func PointerToPointer() {
	p := &amp;Foo{}
	PointerFunction(p)
}

//passed as a pointer, but used as struct
func StructFunction(f *Foo) {
	fmt.Println(*f)
}

func PointerFunction(f *Foo) {
	fmt.Println(f)
}

Summary of the assembly:

  • StructToStruct: 13 lines, no allocation
  • PointerToStruct: 16 lines, no allocation
  • StructToPointer: 20 lines, heap allocated
  • PointerToPointer: 12 lines, heap allocated

With a perfect compiler the *ToStruct functions would be the identical as would the *ToPointer functions. Go's escape analysis is good enough to tell if a pointer escapes even across module boundries. Which ever way is most efficient is the way the compiler will do it.

If you're really into micro-optimization note that Go is most efficient when the syntax lines up with the semantics (struct used as a struct, pointer used as a pointer). Or you can just forget about it and declare the variable the way it will be used and you will be right most of the time.

Note: if Foo is really big PointerToStruct will heap allocate it. The spec threatens to that even StructToStruct is allowed to do this but I couldn't make it happen. The lesson here is that the compiler will do whatever it wants. Just as the details of the registers is shielded from the code, so is the state of the heap/stack. Don't change your code because you think you know how the compiler is going to use the heap.

huangapple
  • 本文由 发表于 2013年9月22日 06:03:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/18938138.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定