我在c风格的基于堆栈的编程和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。

我知道Go垃圾收集这两个值,所以上面的两种形式都是好的。

引用:

注意,与C语言不同,返回a的地址是完全可以的 局部变量;与该变量关联的存储保存下来 在函数返回之后。实际上,取一个合成对象的地址 每次求值时,Literal都会分配一个新实例,因此我们 可以结合这最后两行。 http://golang.org/doc/effective_go.html#functions

但它提出了几个问题。

在例1中,该结构在堆上声明。例2呢?它在堆栈上的声明和在C语言中一样吗?还是它也在堆上? 如果例子2是在堆栈上声明的,它如何在函数返回后保持可用? 如果例子2实际上是在堆上声明的,为什么结构体是按值而不是按引用传递的?在这种情况下指针的意义是什么?


当前回答

值得注意的是,语言规范中没有出现“堆栈”和“堆”这两个词。是在堆栈上声明的,”和“…在堆上声明的”,但请注意,Go声明语法没有提到堆栈或堆。

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

在例2中,转义分析将显示指向转义结构的指针,因此编译器必须分配该结构。然而,我认为当前的Go实现在这种情况下遵循了一个严格的规则,即如果一个结构体的任何部分的地址被取走,该结构体就会被放到堆上。

对于问题3,我们可能会对术语感到困惑。围棋中的所有东西都是通过值传递的,没有引用传递。这里返回一个指针值。指针的意义是什么?考虑对您的示例进行以下修改:

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。对运行时的调用。新事物消失了,这在某些情况下是件好事。等等,这是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     ,

仍然没有调用运行时。new,是的,它确实可以按值返回一个8MB的对象。它是有效的,但你通常不想这样做。这里使用指针的目的是避免推入8MB的对象。

其他回答

根据Go的常见问题解答:

如果编译器不能证明变量之后没有被引用 函数返回,则编译器必须在上分配变量 垃圾收集堆,以避免悬空指针错误。

你并不总是知道你的变量是分配在堆栈上还是堆上。 ... 如果你需要知道变量的分配位置,传递"-m" gc标志给"go build"或"go run"(例如,去运行-gcflags -m app.go)。

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

func Function1() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


func Function2() (*MyStructType, error) {
    var chunk MyStructType

    ...

    return &chunk, nil
}

Function1和Function2可以是内联函数。return变量不会转义。没有必要在堆上分配变量。

我的示例代码:

   package main
   
   type S struct {
           x int
   }
   
   func main() {
           F1()
           F2()
          F3()
  }
  
  func F1() *S {
          s := new(S)
          return s
  }
  
  func F2() *S {
          s := S{x: 10}
          return &s
  }
  
  func F3() S {
          s := S{x: 9}
          return s
  }

根据cmd的输出:

go run -gcflags -m test.go

输出:

# command-line-arguments
./test.go:13:6: can inline F1
./test.go:18:6: can inline F2
./test.go:23:6: can inline F3
./test.go:7:6: can inline main
./test.go:8:4: inlining call to F1
./test.go:9:4: inlining call to F2
./test.go:10:4: inlining call to F3
/var/folders/nr/lxtqsz6x1x1gfbyp1p0jy4p00000gn/T/go-build333003258/b001/_gomod_.go:6:6: can inline init.0
./test.go:8:4: main new(S) does not escape
./test.go:9:4: main &s does not escape
./test.go:14:10: new(S) escapes to heap
./test.go:20:9: &s escapes to heap
./test.go:19:2: moved to heap: s

如果编译器足够聪明,F1() F2() F3()可能不会被调用。因为它没有任何意义。

不要关心变量是分配在堆上还是堆栈上,只需使用它。如果需要,可以通过互斥或通道来保护它。

值得注意的是,语言规范中没有出现“堆栈”和“堆”这两个词。是在堆栈上声明的,”和“…在堆上声明的”,但请注意,Go声明语法没有提到堆栈或堆。

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

在例2中,转义分析将显示指向转义结构的指针,因此编译器必须分配该结构。然而,我认为当前的Go实现在这种情况下遵循了一个严格的规则,即如果一个结构体的任何部分的地址被取走,该结构体就会被放到堆上。

对于问题3,我们可能会对术语感到困惑。围棋中的所有东西都是通过值传递的,没有引用传递。这里返回一个指针值。指针的意义是什么?考虑对您的示例进行以下修改:

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。对运行时的调用。新事物消失了,这在某些情况下是件好事。等等,这是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     ,

仍然没有调用运行时。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实现将为堆上类型为MyStructType的结构分配内存并返回其地址。函数是等价的;编译器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     ,

调用 在函数调用中,函数值和参数在 通常的顺序。在它们被求值之后,调用的参数 是通过值传递给函数和被调用的函数开始 执行。函数的返回参数是按值传递的 当函数返回时,返回到调用函数。

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