Goブログ

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ステートメントが評価されるときに評価されます。

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

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  1. 遅延関数呼び出しは、周囲の関数が返された後、後入れ先出しの順序で実行されます。

この関数は「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を直接呼び出すことによって開始できます。また、範囲外の配列アクセスなどのランタイムエラーによっても発生する可能性があります。

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の他の用途(前述のfile.Closeの例以外)には、ミューテックスの解放が含まれます。

mu.Lock()
defer mu.Unlock()

フッターの印刷

printHeader()
defer printFooter()

などです。

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

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