217

私はGoを初めて使用し、自動変数がスタックに存在し、割り当てられたメモリがヒープに存在するCスタイルのスタックベースのプログラミングと、Pythonスタイルのスタックベースのプログラミングとの間に少しの認識の不一致を経験しています。スタック上に存在するのは、ヒープ上のオブジェクトへの参照/ポインターです。

私の知る限り、次の2つの関数は同じ出力を提供します。

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

つまり、新しい構造体を割り当てて返します。

これをCで記述した場合、最初のオブジェクトはヒープにオブジェクトを配置し、2番目のオブジェクトはスタックにオブジェクトを配置します。1つ目はヒープへのポインターを返し、2つ目はスタックへのポインターを返します。これは、関数が戻るまでに蒸発していたため、悪いことです。

Python(またはC#を除く他の多くの現代言語)で記述した場合、例2は不可能でした。

Goガベージは両方の値を収集するので、上記の両方の形式で問題ありません。

引用するには:

Cとは異なり、ローカル変数のアドレスを返すことはまったく問題ないことに注意してください。変数に関連付けられたストレージは、関数が戻った後も存続します。実際、複合リテラルのアドレスを取得すると、評価されるたびに新しいインスタンスが割り当てられるため、これらの最後の2行を組み合わせることができます。

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

しかし、それはいくつかの疑問を提起します。

  1. 例1では、構造体はヒープ上で宣言されています。例2はどうですか?それはCの場合と同じようにスタックで宣言されますか、それともヒープでも宣言されますか?

  2. 例2がスタックで宣言されている場合、関数が戻った後、どのように使用可能になりますか?

  3. 例2が実際にヒープ上で宣言されている場合、構造体が参照ではなく値で渡されるのはどうしてですか?この場合のポインタのポイントは何ですか?

4

5 に答える 5

214

「スタック」と「ヒープ」という単語は、言語仕様のどこにも表示されないことに注意してください。あなたの質問は「...はスタックで宣言されています」と「...はヒープで宣言されています」と表現されていますが、Go宣言構文はスタックまたはヒープについて何も述べていないことに注意してください。

これにより、技術的には、すべての質問に対する回答が実装に依存するようになります。もちろん、実際には、スタック(ゴルーチンごとに!)とヒープがあり、スタックにあるものとヒープにあるものがあります。コンパイラが厳密なルール(「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のオブジェクトをプッシュしないようにすることです。

于 2012-06-03T00:01:15.370 に答える
71
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の現在の実装では、ヒープ上structのタイプのメモリを割り当て、そのアドレスを返します。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はアドレスです。

于 2012-06-02T22:53:45.393 に答える
36

GoのFAQによると:

関数が戻った後に変数が参照されていないことをコンパイラーが証明できない場合、コンパイラーは、ダングリングポインターのエラーを回避するために、ガベージコレクションされたヒープに変数を割り当てる必要があります。

于 2015-09-29T18:07:03.840 に答える
13

変数がスタックまたはヒープのどちらに割り当てられているかは、常にわかりません。
...
変数がどこに割り当てられているかを知る必要がある場合は、「-m」gcフラグを「gobuild」または「gorun」に渡します(例:)go run -gcflags -m app.go

ソース:http ://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars

于 2015-11-21T12:50:15.477 に答える
0
func Function1() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1とFunction2はインライン関数である可能性があります。そして、戻り変数はエスケープされません。ヒープに変数を割り当てる必要はありません。

私のサンプルコード:

   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()が呼び出されない可能性があります。それは意味がないからです。

変数がヒープに割り当てられているかスタックに割り当てられているかは気にせず、使用するだけです。必要に応じて、ミューテックスまたはチャネルで保護してください。

于 2019-08-17T02:36:47.230 に答える