在Go中堆栈vs堆结构的分配,以及它们如何与垃圾收集相关联

我是Go的新手,我正在体验C风格的基于堆栈的编程,即自动variables存在于堆栈中,分配的内存驻留在堆栈上,以及Python风格的基于堆栈的编程只有堆栈上的东西是引用/指向堆上的对象的指针。

据我所知,以下两个函数给出了相同的输出:

func myFunction() (*MyStructType, error) { var chunk *MyStructType = new(HeaderChunk) ... return chunk, nil } func myFunction() (*MyStructType, error) { var chunk MyStructType ... return &chunk, nil } 

即分配一个新的结构并返回它。

如果我用C编写的话,第一个会把一个对象放在堆上,第二个放在堆栈上。 第一个会返回一个指向堆的指针,第二个会返回一个指向堆栈的指针,这个指针在函数返回的时候会蒸发掉,这将是一件坏事。

如果我用Python(或除C#之外的其他许多现代语言)编写它,则示例2将不可能。

我得到的垃圾收集这两个值,所以上述两种forms都很好。

去引用:

请注意,与C不同的是,返回局部variables的地址是完全正确的。 与该variables相关的存储在函数返回后仍然存在。 实际上,每一次复合文本的地址分配一个新的实例,所以我们可以结合这两行。

http://golang.org/doc/effective_go.html#functions

但是它提出了一些问题。

1 – 在示例1中,结构在堆中声明。 例2呢? 这是在堆栈中以同样的方式在C中声明,还是在堆上呢?

2 – 如果在堆栈中声明了示例2,函数返回后如何保持可用状态?

3 – 如果实例2实际上是在堆中声明的,它是如何通过值而不是通过引用传递的? 这种情况下的指针是什么?

值得注意的是,“stack”和“heap”不会出现在语言规范的任何地方。 你的问题的措辞是“…在栈上声明”和“…在堆上声明”,但要注意,Go声明语法没有提到栈或堆。

这在技术上使所有问题的答案都依赖于实现。 实际上,当然有一个堆栈(每个goroutine!)和一个堆,有些东西放在堆栈上,一些放在堆上。 在某些情况下,编译器遵循严格的规则(比如“ new总是在堆上分配”),而在其他情况下,编译器会做“逃逸分析”来决定一个对象是否可以在堆栈中生存,或者是否必须在堆上分配。

在你的示例2中,转义分析将显示指向结构转义的指针,因此编译器将不得不分配结构。 我认为Go的当前实现在这种情况下遵循严格的规则,但是,如果地址是结构的任何部分的话,那么结构就会堆积在堆上。

对于问题3,我们可能会对术语感到困惑。 Go中的所有东西都是按照价值传递的,没有通过引用。 这里你正在返回一个指针值。 什么是指针? 考虑你的例子的以下修改:

 type MyStructType struct{} func myFunction1() (*MyStructType, error) { var chunk *MyStructType = new(MyStructType) // ... return chunk, nil } func myFunction2() (MyStructType, error) { var chunk MyStructType // ... return chunk, nil } type bigStruct struct { lots [1e6]float64 } func myFunction3() (bigStruct, error) { var chunk bigStruct // ... return chunk, nil } 

我修改myFunction2来返回结构,而不是结构的地址。 现在比较myFunction1和myFunction2的汇编输出,

 --- prog list "myFunction1" --- 0000 (s.go:5) TEXT myFunction1+0(SB),$16-24 0001 (s.go:6) MOVQ $type."".MyStructType+0(SB),(SP) 0002 (s.go:6) CALL ,runtime.new+0(SB) 0003 (s.go:6) MOVQ 8(SP),AX 0004 (s.go:8) MOVQ AX,.noname+0(FP) 0005 (s.go:8) MOVQ $0,.noname+8(FP) 0006 (s.go:8) MOVQ $0,.noname+16(FP) 0007 (s.go:8) RET , --- prog list "myFunction2" --- 0008 (s.go:11) TEXT myFunction2+0(SB),$0-16 0009 (s.go:12) LEAQ chunk+0(SP),DI 0010 (s.go:12) MOVQ $0,AX 0011 (s.go:14) LEAQ .noname+0(FP),BX 0012 (s.go:14) LEAQ chunk+0(SP),BX 0013 (s.go:14) MOVQ $0,.noname+0(FP) 0014 (s.go:14) MOVQ $0,.noname+8(FP) 0015 (s.go:14) RET , 

不要担心,这里的myFunction1输出与peterSO(优秀)答案不同。 我们显然运行不同的编译器。 否则,请参阅我修改myFunction2返回myStructType而不是* myStructType。 对runtime.new的调用消失了,在某些情况下这将是一件好事。 坚持,虽然这是myFunction3,

 --- prog list "myFunction3" --- 0016 (s.go:21) TEXT myFunction3+0(SB),$8000000-8000016 0017 (s.go:22) LEAQ chunk+-8000000(SP),DI 0018 (s.go:22) MOVQ $0,AX 0019 (s.go:22) MOVQ $1000000,CX 0020 (s.go:22) REP , 0021 (s.go:22) STOSQ , 0022 (s.go:24) LEAQ chunk+-8000000(SP),SI 0023 (s.go:24) LEAQ .noname+0(FP),DI 0024 (s.go:24) MOVQ $1000000,CX 0025 (s.go:24) REP , 0026 (s.go:24) MOVSQ , 0027 (s.go:24) MOVQ $0,.noname+8000000(FP) 0028 (s.go:24) MOVQ $0,.noname+8000008(FP) 0029 (s.go:24) RET , 

仍然没有调用runtime.new,是的,它真的有效的价值返回一个8MB的对象。 它的工作原理,但你通常不希望。 这里的一个指针点将避免推动8MB的对象。

 type MyStructType struct{} func myFunction1() (*MyStructType, error) { var chunk *MyStructType = new(MyStructType) // ... return chunk, nil } func myFunction2() (*MyStructType, error) { var chunk MyStructType // ... return &chunk, nil } 

在这两种情况下,Go的当前实现将为堆中的MyStructTypetypes的struct分配内存并返回其地址。 function是等同的; 编译器的asm源码是一样的。

 --- prog list "myFunction1" --- 0000 (temp.go:9) TEXT myFunction1+0(SB),$8-12 0001 (temp.go:10) MOVL $type."".MyStructType+0(SB),(SP) 0002 (temp.go:10) CALL ,runtime.new+0(SB) 0003 (temp.go:10) MOVL 4(SP),BX 0004 (temp.go:12) MOVL BX,.noname+0(FP) 0005 (temp.go:12) MOVL $0,AX 0006 (temp.go:12) LEAL .noname+4(FP),DI 0007 (temp.go:12) STOSL , 0008 (temp.go:12) STOSL , 0009 (temp.go:12) RET , --- prog list "myFunction2" --- 0010 (temp.go:15) TEXT myFunction2+0(SB),$8-12 0011 (temp.go:16) MOVL $type."".MyStructType+0(SB),(SP) 0012 (temp.go:16) CALL ,runtime.new+0(SB) 0013 (temp.go:16) MOVL 4(SP),BX 0014 (temp.go:18) MOVL BX,.noname+0(FP) 0015 (temp.go:18) MOVL $0,AX 0016 (temp.go:18) LEAL .noname+4(FP),DI 0017 (temp.go:18) STOSL , 0018 (temp.go:18) STOSL , 0019 (temp.go:18) RET , 

呼叫

在函数调用中,函数值和参数按通常的顺序进行评估。 在它们被评估之后,调用的参数被传递给函数,并且被调用的函数开始执行。 当函数返回时,函数的返回参数被传递给调用函数。

所有函数和返回参数都是按值传递的。 types为*MyStructType的返回参数值是一个地址。

根据Go的FAQ :

如果编译器不能在函数返回后certificatevariables没有被引用,那么编译器必须在垃圾回收堆上分配variables,以避免指针错误。

你并不总是知道你的variables是分配在堆栈还是堆中。

如果你需要知道你的variables被分配到哪里,通过“-m”gc标志去“去build立”或“去运行”(例如, go run -gcflags -m app.go )。

资料来源: http : //devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars