defer、panic、recover 関数を使用して制御する

完了

次に、Go にだけある制御フロー deferpanicrecover を見てみます。 これらの関数にはいくつかのユース ケースがあります。 ここでは、最も重要なユース ケースについて調べます。

defer 関数

Go の defer ステートメントを使用すると、defer ステートメントが含まれる関数が終了するまで、(すべてのパラメーターを含む) 関数の実行が延期されます。 通常、ファイルのクローズやクリーンアップ プロセスの実行などのタスクについて忘れないようにするため、関数を遅延させます。

必要な数だけいくつでも関数を延期できます。 defer ステートメントは、最後から最初へと、逆の順序で実行されます。

このパターンの動作を確認するため、次のコード例を実行します。

package main

import "fmt"

func main() {
    for i := 1; i <= 4; i++ {
        defer fmt.Println("deferred", -i)
        fmt.Println("regular", i)
    }
}

コードの出力は次のようになります。

regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

この例では、fmt.Println("deferred", -i) が延期されるたびに、i の値が格納され、その関数呼び出しがキューに追加されたことに注意してください。 main() 関数で regular 値の出力が終了すると、すべての遅延呼び出しが実行されます。 遅れた呼び出しからの出力は、待ち行列から飛び出したように、順序が逆になる (後入れ、先だし) ことに注意してください。

defer 関数の一般的なユース ケースは、ファイルの使用が終了した後でファイルを閉じることです。 次に例を示します。

package main

import (
    "io"
    "os"
    "fmt"
)

func main() {
    newfile, error := os.Create("learnGo.txt")
    if error != nil {
        fmt.Println("Error: Could not create file.")
        return
    }
    defer newfile.Close()

    if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
	    fmt.Println("Error: Could not write to file.")
        return
    }

    newfile.Sync()
}

ファイルを作成したり開いたりして、何かを行った後で、ファイルを閉じるのを忘れないように、.Close() 関数を遅延させます。

panic 関数

範囲外のインデックスを使用して配列にアクセスしようとしたり、nil ポインターを逆参照したりして、実行時エラーが発生すると、Go プログラムはパニック状態になります。 プログラムを強制的にパニック状態にすることもできます。

組み込みの panic() 関数を使用すると、Go プログラムでの通常の制御フローは停止します。 panic 呼び出しを使用したとき、遅延されているすべての関数呼び出しは普通に実行されます。 すべての関数から戻るまで、プロセスでスタックが続行されます。 その後、プログラムはログ メッセージを出力してクラッシュします。 このメッセージには、問題の根本原因を診断するのに役立つエラー情報とスタック トレースが含まれています。

panic() 関数を呼び出すときは、任意の値を引数として追加できます。 通常は、パニックを発生させている理由についてのエラー メッセージを送ります。

たとえば、次のコードでは、panic 関数と defer 関数が組み合わされています。 このコードを実行して、制御フローがどのように中断されるのか確認してみてください。 クリーンアップ プロセスはやはり実行されることに注意してください。

package main

import "fmt"

func highlow(high int, low int) {
    if high < low {
        fmt.Println("Panic!")
        panic("highlow() low greater than high")
    }
    defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
    fmt.Println("Call: highlow(", high, ",", low, ")")

    highlow(high, low + 1)
}

func main() {
    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

出力は次のようになります。

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
	/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
	/tmp/sandbox/prog.go:18 +0x298
main.main()
	/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

コードを実行すると、次のような結果になります。

  1. すべてが正常に実行されます。 このプログラムでは、highlow() 関数に渡された高い値と低い値が出力されます。

  2. low の値が high の値より大きい場合、プログラムはパニック状態になります。 Panic! メッセージが表示されます。 この時点で、制御フローが中断され、遅延されていたすべての関数で Deferred... メッセージの出力が開始されます。

  3. プログラムがクラッシュし、完全なスタック トレースが表示されます。 Program finished successfully! メッセージは表示されません。

通常、panic() 関数の呼び出しは、深刻なエラーが予期されない場合に実行されます。 プログラムのクラッシュを回避するには、recover() という名前の別の関数を使用できます。

recover 関数

プログラムがクラッシュするのを回避し、代わりに内部でエラーを報告することが必要になる場合があります。 または、プログラムがクラッシュする前に、混乱を解消する必要があるかもしれません。 たとえば、それ以上問題が発生するのを避けるため、リソースへの接続を閉じることが必要な場合があります。

Go の組み込み関数 recover() を使用すると、パニックの後で制御を取り戻すことができます。 recover の呼び出しは、defer を呼び出している関数内でのみ行います。 recover() 関数を呼び出すと、nil が返され、通常の実行に他の影響はありません。

前のコードで main 関数を変更し、次のように、recover() 関数の呼び出しを追加してみます。

func main() {
    defer func() {
	handler := recover()
        if handler != nil {
            fmt.Println("main(): recover", handler)
        }
    }()

    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

プログラムを実行すると、出力は次のようになります。

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high

Program exited.

前のバージョンとの違いがわかりますか。 主な違いは、スタック トレース エラーが表示されなくなったことです。

main() 関数で、recover() 関数を呼び出す匿名関数が遅延されています。 プログラムがパニックになっていると、recover() の呼び出しで nil を返すことができません。 ここで何かを行って混乱を解消できますが、この例では単に何かを出力するだけです。

panic 関数と recover 関数の組み合わせは、Go での特徴的な例外処理方法です。 他のプログラミング言語では、try/catch ブロックが使用されます。 Go の場合、ここで調べたようなアプローチが好まれます。

詳細については、Go での組み込み関数 try の追加に関する提案についてのページを参照してください。