演習 - defer、panic、recover 関数を使用する

完了

ここでは、Go のそれほど一般的でない制御フローである deferpanicrecover を検討します。 既に説明したように、Go には特徴的な方法がいくつかあり、これら 3 つの制御フローは Go に固有のものです。

これらの関数にはいくつかのユース ケースがあります。 ここでは、最も重要なユース ケースについて調べます。 それでは最初の関数から始めましょう。

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"
)

func main() {
    f, err := os.Create("notes.txt")
    if err != nil {
        return
    }
    defer f.Close()

    if _, err = io.WriteString(f, "Learning Go!"); err != nil {
        return
    }

    f.Sync()
}

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

panic 関数

実行時エラーが発生すると、Go プログラムはパニックになります。 プログラムを強制的にパニックにさせることができますが、範囲外の配列のアクセスや nil ポインターの逆参照などの実行時エラーによっても、パニックが発生する可能性があります。

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

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

たとえば、panic 関数と defer 関数を組み合わせて、制御フローがどのように中断されるかを確認します。 ただし、クリーンアップ プロセスの実行は続けます。 次のようなコード スニペットを使用します。

package main

import "fmt"

func main() {
    g(0)
    fmt.Println("Program finished successfully!")
}

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

このコードを実行すると、出力は次のようになります。

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: Panic in g() (major)

goroutine 1 [running]:
main.g(0x4)
        /Users/johndoe/go/src/helloworld/main.go:13 +0x22e
main.g(0x3)
        /Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.g(0x2)
        /Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.g(0x1)
        /Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.g(0x0)
        /Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.main()
        /Users/johndoe/go/src/helloworld/main.go:6 +0x2a
exit status 2

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

  1. すべてが正常に実行されます。 g() 関数が受け取った値が、プログラムによって出力されます。

  2. i が 3 より大きいと、プログラムはパニックになります。 Panicking! メッセージが表示されます。 この時点で、制御フローが中断され、遅延されていたすべての関数で Defer in g() メッセージの出力が開始されます。

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

通常、panic() の呼び出しは、深刻なエラーが予期されない場合に実行されます。 プログラムのクラッシュを回避するには、recover() 関数を使用します。 次のセクションでは、この関数について学習します。

recover 関数

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

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

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

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in main", r)
        }
    }()
    g(0)
    fmt.Println("Program finished successfully!")
}

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

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

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 main Panic in g() (major)

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

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

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

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