The Go Blog

Defer、Panic、およびRecover

Andrew Gerrand
2010年8月4日

Goには、if、for、switch、gotoといった通常の制御フローメカニズムがあります。また、別のゴルーチンでコードを実行するためのgoステートメントもあります。ここでは、あまり一般的ではないdefer、panic、recoverについて説明したいと思います。

deferステートメントは、関数呼び出しをリストにプッシュします。保存された呼び出しのリストは、囲む関数が戻った後に実行されます。deferは、様々なクリーンアップアクションを実行する関数を簡素化するためによく使用されます。

たとえば、2つのファイルを開き、一方のファイルの内容をもう一方のファイルにコピーする関数を見てみましょう。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

これは機能しますが、バグがあります。os.Createへの呼び出しが失敗すると、関数はソースファイルを閉じずに戻ります。これは、2番目のreturnステートメントの前にsrc.Closeへの呼び出しを置くことで簡単に修正できますが、関数がより複雑な場合、問題がそれほど簡単に気付かれず、解決されない可能性があります。deferステートメントを導入することで、ファイルが常に閉じられることを保証できます。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

deferステートメントを使用すると、各ファイルを開いた直後に閉じることを考えることができ、関数内のreturnステートメントの数に関係なく、ファイルが必ず閉じられることを保証します。

deferステートメントの動作は簡単で予測可能です。3つの簡単なルールがあります。

  1. 遅延関数の引数は、deferステートメントが評価されるときに評価されます。

この例では、Println呼び出しが遅延されるときに式「i」が評価されます。遅延された呼び出しは、関数が戻った後に「0」を出力します。

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  1. 遅延された関数呼び出しは、囲む関数が戻った後、Last In First Outの順序で実行されます。

この関数は「3210」を出力します。

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. 遅延関数は、戻り関数の名前付き戻り値を読み書きできます。

この例では、遅延関数は、囲む関数が戻ったに、戻り値iをインクリメントします。したがって、この関数は2を返します。

func c() (i int) {
    defer func() { i++ }()
    return 1
}

これは、関数のエラー戻り値を変更するのに便利です。これについてはすぐに例を挙げます。

Panicは、通常の制御フローを停止し、パニック状態を開始する組み込み関数です。関数Fがpanicを呼び出すと、Fの実行は停止し、F内の遅延関数は通常どおり実行され、Fはその呼び出し元に戻ります。呼び出し元にとって、Fはpanicの呼び出しのように動作します。このプロセスは、現在のゴルーチン内のすべての関数が戻るまでスタックを遡り、その時点でプログラムはクラッシュします。panicは、panicを直接呼び出すことによって開始できます。また、配列の範囲外アクセスなどの実行時エラーによっても発生する可能性があります。

Recoverは、パニック状態のゴルーチンの制御を取り戻す組み込み関数です。recoverは遅延関数内でしか役に立ちません。通常の実行中、recoverの呼び出しはnilを返し、他の効果はありません。現在のゴルーチンがパニック状態の場合、recoverの呼び出しはpanicに与えられた値をキャプチャし、通常の実行を再開します。

以下は、panicとdeferのメカニズムを示すプログラムの例です。

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

関数gはint iを受け取り、iが3より大きい場合はパニックを起こし、それ以外の場合は引数i+1で自分自身を呼び出します。関数fは、recoverを呼び出し、回復された値(nilでない場合)を出力する関数を遅延させます。読み進める前に、このプログラムの出力がどうなるか想像してみてください。

プログラムは以下を出力します。

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

fから遅延関数を削除すると、パニックは回復されず、ゴルーチンの呼び出しスタックのトップに到達し、プログラムが終了します。この修正されたプログラムは以下を出力します。

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4

panic PC=0x2a9cd8
[stack trace omitted]

panicrecoverの実際の例については、Go標準ライブラリのjsonパッケージを参照してください。これは、一連の再帰関数を使用してインターフェースをエンコードします。値のトラバース中にエラーが発生した場合、panicが呼び出され、スタックをトップレベルの関数呼び出しまで巻き戻し、そこでパニックから回復し、適切なエラー値を返します(encode.goのencodeState型の「error」メソッドと「marshal」メソッドを参照)。

Goライブラリの慣例では、パッケージが内部でpanicを使用する場合でも、その外部APIは明示的なエラー戻り値を提示します。

deferの他の使用例(ファイル.Closeの例以外)には、ミューテックスの解放があります。

mu.Lock()
defer mu.Unlock()

フッターの印刷

printHeader()
defer printFooter()

などがあります。

まとめると、deferステートメント(panicとrecoverの有無にかかわらず)は、制御フローのための珍しい強力なメカニズムを提供します。これは、他のプログラミング言語で特殊な構造によって実装されている多くの機能をモデル化するために使用できます。ぜひ試してみてください。

次の記事: Goが2010年Bossie賞を受賞
前の記事: 通信によるメモリ共有
ブログインデックス