Andrew Gerrand

    4 August 2010

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

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

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

    1. func CopyFile(dstName, srcName string) (written int64, err error) {
    2. src, err := os.Open(srcName)
    3. if err != nil {
    4. return
    5. }
    6. dst, err := os.Create(dstName)
    7. if err != nil {
    8. return
    9. }
    10. written, err = io.Copy(dst, src)
    11. dst.Close()
    12. src.Close()
    13. return
    14. }

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

    1. func CopyFile(dstName, srcName string) (written int64, err error) {
    2. src, err := os.Open(srcName)
    3. if err != nil {
    4. return
    5. }
    6. defer src.Close()
    7. dst, err := os.Create(dstName)
    8. if err != nil {
    9. return
    10. }
    11. defer dst.Close()
    12. return io.Copy(dst, src)
    13. }

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

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

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

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

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

    この関数は「3210」を印刷します:

    1. func b() {
    2. for i := 0; i < 4; i++ {
    3. defer fmt.Print(i)
    4. }
    5. }
    • 1. 遅延関数は、戻り関数の名前付き戻り値を読み取り、割り当てることができます。

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

    1. func c() (i int) {
    2. defer func() { i++ }()
    3. return 1
    4. }

    これは、関数のエラー戻り値を修正するのに便利です。これについての例をすぐに見ていきます。

    Panicは、通常の制御フローを停止し、パニックを開始する組み込み関数です。関数Fがpanicを呼び出すと、Fの実行は停止し、F内のすべての遅延関数は通常通り実行され、その後Fは呼び出し元に戻ります。呼び出し元にとって、Fはpanicへの呼び出しのように振る舞います。このプロセスはスタックを上に進み、現在のgoroutine内のすべての関数が戻るまで続き、その時点でプログラムがクラッシュします。パニックは、panicを直接呼び出すことで開始できます。また、配列の範囲外アクセスなどのランタイムエラーによっても引き起こされる可能性があります。

    Recoverは、パニック状態のgoroutineの制御を取り戻すための組み込み関数です。Recoverは、遅延関数内でのみ有用です。通常の実行中にrecoverを呼び出すと、nilが返され、他の効果はありません。現在のgoroutineがパニック状態の場合、recoverを呼び出すとpanicに渡された値をキャプチャし、通常の実行を再開します。

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

    1. package main
    2. import "fmt"
    3. func main() {
    4. f()
    5. fmt.Println("Returned normally from f.")
    6. }
    7. func f() {
    8. defer func() {
    9. if r := recover(); r != nil {
    10. fmt.Println("Recovered in f", r)
    11. }
    12. }()
    13. fmt.Println("Calling g.")
    14. g(0)
    15. fmt.Println("Returned normally from g.")
    16. }
    17. func g(i int) {
    18. if i > 3 {
    19. fmt.Println("Panicking!")
    20. panic(fmt.Sprintf("%v", i))
    21. }
    22. defer fmt.Println("Defer in g", i)
    23. fmt.Println("Printing in g", i)
    24. g(i + 1)
    25. }

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

    プログラムは次のように出力します:

    1. Calling g.
    2. Printing in g 0
    3. Printing in g 1
    4. Printing in g 2
    5. Printing in g 3
    6. Panicking!
    7. Defer in g 3
    8. Defer in g 2
    9. Defer in g 1
    10. Defer in g 0
    11. Recovered in f 4
    12. Returned normally from f.

    fから遅延関数を削除すると、パニックは回復されず、goroutineの呼び出しスタックの最上部に達し、プログラムが終了します。この修正されたプログラムは次のように出力します:

    1. Calling g.
    2. Printing in g 0
    3. Printing in g 1
    4. Printing in g 2
    5. Printing in g 3
    6. Panicking!
    7. Defer in g 3
    8. Defer in g 2
    9. Defer in g 1
    10. Defer in g 0
    11. panic: 4
    12. panic PC=0x2a9cd8
    13. [stack trace omitted]

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

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

    deferの他の使用法(前述のfile.Closeの例を超えて)には、ミューテックスの解放が含まれます:

    1. mu.Lock()
    2. defer mu.Unlock()

    フッターの印刷:

    1. printHeader()
    2. defer printFooter()

    などがあります。

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