これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: Golang Defer: From Basic To Trap.
defer ステートメントは、おそらく Go を学習し始めるときに最初に非常に興味深いと思うものの 1 つですよね?
しかし、多くの人がつまずくのはそれだけではありません。また、使用中に触れていない魅力的な側面もたくさんあります。
たとえば、defer ステートメントには実際には 3 つのタイプがあります (Go 1.22 の時点では、後で変更される可能性があります): オープンコーディングされた defer、ヒープ割り当てされた defer、およびスタック割り当て。それぞれに異なるパフォーマンスがあり、最適に使用されるシナリオも異なります。パフォーマンスを最適化したい場合は、これを知っておくと良いでしょう。
このディスカッションでは、基本からより高度な使用方法まですべてを取り上げ、内部の詳細の一部についても少しだけ掘り下げていきます。
深く掘り下げる前に、defer について簡単に見てみましょう。
Go では、defer は、周囲の関数が終了するまで関数の実行を遅らせるために使用されるキーワードです。
func main() { defer fmt.Println("hello") fmt.Println("world") } // Output: // world // hello
このスニペットでは、defer ステートメントにより、fmt.Println("hello") が main 関数の最後に実行されるようにスケジュールされています。したがって、 fmt.Println("world") がすぐに呼び出され、最初に "world" が出力されます。その後、defer を使用したため、メインが終了する前の最後のステップとして "hello" が出力されます。
これは、関数が終了する直前に、後で実行するタスクを設定するのと同じです。これは、データベース接続を閉じる、ミューテックスを解放する、ファイルを閉じるなどのクリーンアップ アクションに非常に役立ちます:
func doSomething() error { f, err := os.Open("phuong-secrets.txt") if err != nil { return err } defer f.Close() // ... }
上記のコードは、defer がどのように機能するかを示す良い例ですが、defer の使用方法としては不適切です。これについては次のセクションで説明します。
「わかりました、いいですが、最後に f.Close() を入れてみませんか?」
これには正当な理由がいくつかあります:
パニックが発生すると、スタックが巻き戻され、遅延関数が特定の順序で実行されます。これについては次のセクションで説明します。
関数内で複数の defer ステートメントを使用すると、それらは「スタック」順序で実行されます。つまり、最後の遅延関数が最初に実行されます。
func main() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) } // Output: // 3 // 2 // 1
defer ステートメントを呼び出すたびに、次のようにその関数を現在のゴルーチンのリンク リストの先頭に追加します。
そして関数が戻ると、リンクされたリストを調べて、上の画像に示されている順序でそれぞれを実行します。
しかし、覚えておいてください。ゴルーチンのリンク リスト内のすべての defer が実行されるのではなく、返された関数内の defer のみが実行されます。これは、defer のリンク リストには、さまざまな関数からの多くの defer が含まれる可能性があるためです。
func B() { defer fmt.Println(1) defer fmt.Println(2) A() } func A() { defer fmt.Println(3) defer fmt.Println(4) }
したがって、現在の関数 (または現在のスタック フレーム) 内の遅延関数のみが実行されます。
しかし、現在のゴルーチン内のすべての遅延関数がトレースされて実行されるという典型的なケースが 1 つあり、そのときにパニックが発生します。
コンパイル時エラーの他に、ゼロ除算 (整数のみ)、範囲外、nil ポインタの逆参照など、実行時エラーが多数あります。これらのエラーにより、アプリケーションはパニックを起こします。
パニックは、現在の goroutine の実行を停止し、スタックを巻き戻し、現在の goroutine で遅延関数を実行して、アプリケーションをクラッシュさせる方法です。
予期しないエラーを処理し、アプリケーションのクラッシュを防ぐために、遅延関数内で回復関数を使用して、パニック状態のゴルーチンの制御を取り戻すことができます。
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() panic("This is a panic") } // Output: // Recovered: This is a panic
通常、パニック状態にエラーを入れて、recover(..) でそれをキャッチしますが、文字列、整数など、何でも構いません。
上記の例では、recover を使用できるのは遅延関数内だけです。これについてもう少し説明しましょう。
ここに挙げられる間違いがいくつかあります。実際のコードでこのようなスニペットを少なくとも 3 つ見たことがあります。
最初の方法は、recover を遅延関数として直接使用することです:
func main() { defer recover() panic("This is a panic") }
上記のコードは依然としてパニックを起こしますが、これは Go ランタイムの仕様によるものです。
recover 関数はパニックを捕捉することを目的としていますが、正しく動作するには遅延関数内で呼び出す必要があります。
舞台裏では、recover への呼び出しは実際には runtime.gorecover であり、recover 呼び出しが適切なコンテキストで、具体的にはパニック発生時にアクティブだった正しい遅延関数から行われているかどうかをチェックします。
「ということは、このように、遅延関数内の関数ではリカバリを使用できないということですか?」
func myRecover() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } } func main() { defer func() { myRecover() // ... }() panic("This is a panic") }
その通り、上記のコードはご想像どおりに機能しません。これは、recover が遅延関数から直接呼び出されるのではなく、入れ子になった関数から呼び出されるからです。
さて、別の間違いは、別のゴルーチンからのパニックをキャッチしようとすることです:
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() go panic("This is a panic") time.Sleep(1 * time.Second) // Wait for the goroutine to finish }
当然ですよね?遅延チェーンが特定のゴルーチンに属していることはすでにわかっています。各ゴルーチンには独自のスタックがあるため、あるゴルーチンが別のゴルーチンに介入してパニックに対処できる場合は困難です。
残念ながら、この場合の唯一の解決策は、ゴルーチンでパニックを処理しない場合、アプリケーションをクラッシュさせることです。
私は以前、古いデータが分析システムにプッシュされるというこの問題に遭遇したことがありますが、その理由を理解するのは困難でした。
これが言いたいことです:
func pushAnalytic(a int) { fmt.Println(a) } func main() { a := 10 defer pushAnalytic(a) a = 20 }
出力は何になると思いますか? 20ではなく10です。
これは、defer ステートメントを使用すると、その時点で値が取得されるためです。これを「価値による捕捉」といいます。したがって、pushAnalytic に送信される a の値は、a が後で変更されても、延期がスケジュールされているときに 10 に設定されます。
これを修正するには 2 つの方法があります。
...
投稿全文はこちらからご覧いただけます: Golang Defer: 基本からトラップまで。
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3