🤸

Goのdeferを図解で理解する

に公開

はじめに

Go言語を学び始めると、必ず出会う便利な機能 defer。ファイルのクローズ処理やDBコネクションの切断など、後片付け処理を忘れずに実行できるので非常に強力です。

ですが、複数の defer が出てきたり、関数をまたいだりすると、「あれ、どの処理がどの順番で実行されるんだっけ…?🤔」と混乱した経験はありませんか?

この記事では、そんな defer の実行順序のルールを、シンプルなコード例と図解でスッキリと解説します。この記事を読み終わる頃には、defer の動きに迷いがなくなっているはずです!

deferの絶対ルールは「後入れ先出し(LIFO)」

defer の実行順序を理解するための最も重要なルールは、**「後入れ先出し(LIFO: Last-In, First-Out)」**です。

これは、机の上に本を積み上げていく様子をイメージすると分かりやすいです。

  1. 本を積む(deferで処理を登録する)
  2. 本を取る時は、一番**後から積んだ本(一番上にある本)**からしか取れません。

defer もこれと全く同じで、後から defer で指定した処理ほど、先に実行されます。
LIFOについてアルゴリズムの領域でよく出てくるので、詳細は以下で確認してください。
https://e-words.jp/w/LIFO.html#google_vignette

コードベースで確認してみる

百聞は一見にしかず、ということで実際のコードを見てみましょう。

package main

import "fmt"

func main() {
    fmt.Println("main関数の処理を開始します。")

    defer fmt.Println("1番目のdefer: 最初に登録されました。") // 最後に実行される
    defer fmt.Println("2番目のdefer: 2番目に登録されました。")
    defer fmt.Println("3番目のdefer: 最後に登録されました。") // 最初に実行される

    fmt.Println("main関数の処理がもうすぐ終わります。")
}

実行結果は以下です。

main関数の処理を開始します。
main関数の処理がもうすぐ終わります。
3番目のdefer: 最後に登録されました。
2番目のdefer: 2番目に登録されました。
1番目のdefer: 最初に登録されました。

defer で登録した処理が、見事に逆の順番で実行されているのがわかります。
Goは defer 文を見つけるたびに、その処理を「遅延実行リスト(スタック)」に積み上げていき、関数の最後にリストの上から順に実行します。

図で理解するLIFOの動き

この「後入れ先出し」の動きを、図で見てみましょう。処理の流れが視覚的にイメージできるはずです。

deferが有効なのは「その関数の中だけ」

LIFOと並んで重要なのが、defer はそれが書かれている関数が終了する時に実行されるというルールです。これを「関数スコープ」と呼びます。

別の関数の中で書かれた defer は、呼び出し元の関数の終了タイミングとは無関係です。

package main

import "fmt"

func subFunction() {
    fmt.Println("  [subFunction] 開始")
    defer fmt.Println("  [subFunction] のdeferが実行されました。") // subFunctionの終了時に実行
    fmt.Println("  [subFunction] 終了")
}


func main() {
    fmt.Println("[main] 開始")
    defer fmt.Println("[main] のdeferが実行されました。") // main関数の終了時に実行

    subFunction() // subFunctionを呼び出す

    fmt.Println("[main] 終了直前")
}

実行結果は以下です。

[main] 開始
  [subFunction] 開始
  [subFunction] 終了
  [subFunction] のdeferが実行されました。
[main] 終了直前
[main] のdeferが実行されました。

subFunctiondefer は、subFunction が終了した直後に実行されていますね。main 関数の最後まで待っているわけではありません。

関数をまたぐdeferの動き(図解)

この動きも、図で見ると一目瞭然です。ポイントは、deferのスタックが関数ごとに用意されているという点です。

図を見ると、subFunction が自身の defer を実行して完全に終了してから、処理が main 関数に戻っていることがよく分かります。

まとめ

defer の実行順序を理解するためのポイントは、たった2つです。

  1. 後入れ先出し (LIFO): 同じ関数内では、後から defer したものが最初に実行される。
  2. 関数スコープ: defer は、それが書かれている関数が終了する時に実行される。
    この2つのルールを覚えておけば、もう defer の動きに迷うことはないと思います。Goでの開発がもっと楽しく、効率的になるはずです。

また、忘れたときはいつでもこの記事に戻ってきてください!

Discussion