Last Updated: 2/6/2024, 5:44:57 AM

# Defer, Panic, and Recover
The Go Blog

この記事は Defer, Panic, and Recover - The Go Blog (opens new window) の翻訳になります。 Python の例外機構 try 文と比較するために翻訳しました。

複数の返り値の最後の値を error インターフェースを満たす型で返し、 呼び出し元でそのエラーを元に処理を行います。 panic-defer-recover を使用したエラーハンドリングは 多くの場合推奨されません。
Go言語におけるエラーハンドリングを今一度振り返る (opens new window)

本文

Go は制御フローのための通常の機構を備えています: 例えば if, for, switch, goto です。 Go は、また離れた goroutine でコードを走らせるために go 文を持っています。 ここでは私はあまり一般的でない機構について話したいと思います: defer, panic そして recover についてです。

Go has the usual mechanisms for control flow: if, for, switch, goto. It also has the go statement to run code in a separate goroutine. Here I'd like to discuss some of the less common ones: defer, panic, and recover.

defer 文は関数呼び出しをリストに押し込みます。 その関数呼び出しを保存したリストは、関数が戻るときに実行されます。 defer は一般に様々な clean-up actions を実行する関数を単純にするために使用されます。

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

例えば、2つのファイルを開き、一方の中身を他方にコピーする関数を見て見ましょう。

For example, let's look at a function that opens two files and copies the contents of one file to the other:

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 の呼び出しが失敗した場合、コピー元のファイルを閉じることなく関数は戻ってしまいます。 これは src.Close への呼び出しを2つ目の return 文の前におけば解決しますが もし関数がより複雑であれば、問題は気づきにくく解決しにくいものになるかもしれません (具体的になにを指しているかは、分からない)。 defer 文を導入することで、ファイルが常に閉じられることを確実にできます。

This works, but there is a bug. If the call to os.Create fails, the function will return without closing the source file. This can be easily remedied by putting a call to src.Close before the second return statement, but if the function were more complex the problem might not be so easily noticed and resolved. By introducing defer statements we can ensure that the files are always closed:

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)
}
# 訳者注釈
# 上記の Go コードと等価な Python コードを書きました。

def copy_file(dst_name, src_name):
    try:
        with open(dst_name, 'w') as dst, open(src_name) as src:
            for line in src:
                dst.write(line)
    except:
        return None

    return dst


"""
Python ではこのように例外が発生した場合に
ファイルが close されない問題に対して with 文で対応します。

まだ確証は得られていないのですが
**恐らく** close してくれています。

意図的に例外を発せさせたところ
変数 dst を参照できなくなっていました。

ガベレージコレクションが働いていることが考えられます。
ガベレージコレクションが働くと自動的にファイルが閉じられる仕様のようです。
詳細はまだ確認中です。

> つまり、どうやら GC に回収されたファイルオブジェクトは自動的にクローズされるようだ。  
> [Python: ファイルオブジェクトのクローズについて](https://blog.amedama.jp/entry/2015/12/03/225138)
"""

defer 文は各ファイルを開いた後にファイルを閉じることについて考えることを許してくれる、 それは関数内にいくつ return があろうとも、ファイルが閉じられることを確実に保証してくれる。

Defer statements allow us to think about closing each file right after opening it, guaranteeing that, regardless of the number of return statements in the function, the files will be closed.

defer 文の動作は、明快で予想できるものです。 3つの簡潔なルールがあります。

The behavior of defer statements is straightforward and predictable. There are three simple rules:

1. deffer された関数の引数は、defer 文が評価されたときに、評価されます。

この例では i という式は Println の呼び出しが defer されたときに評価されます。 defer された呼び出しは、関数が戻るときに 0 を出力します。

1. A deferred function's arguments are evaluated when the defer statement is evaluated.

In this example, the expression "i" is evaluated when the Println call is deferred. The deferred call will print "0" after the function returns.

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

2. defer された関数呼び出しは、自身を包含する一番内側の関数が戻るときに Last In First Out の順番に実行されます。

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

2. Deferred function calls are executed in Last In First Out order after the surrounding function returns.

This function prints "3210":

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

3. defer された関数は、巻き戻っている関数の名前付き返り値を読み込、新しい値を代入するかもしれない。

この例では、defer された関数は、自身を包含する一番内側の関数が戻った後に、返り値 i をインクリメントする。 つまり、この関数は 2 を返す。

3. Deferred functions may read and assign to the returning function's named return values.

In this example, a deferred function increments the return value i after the surrounding function returns. Thus, this function returns 2:

名前付き結果パラメータ, named result parameters (opens new window)
シグネチャfunc c() (i int) に注目してください。 空の引数リスト () のすぐ隣に (i int) があります。 これは「名前付き結果パラメータ」というものらしいです。 返り値に名前をつけることができます。

返り値に名前をつけて、いつ使うんだ?って感じですが、 関数が return に達し巻き戻るときの defer 文で活用されます。

関数 c を見てみましょう。 関数の処理が return に達し巻き戻るときに変数 i に戻り値である 1 が束縛されます。 そのため defer 文の付与された無名関数の中で 1 が束縛された i に 1 が加算されて i は 2 になります。

まだ理解できていないのですがかなり奥深い機能のようで Google で検索するといろいろな記事がヒットします。

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

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

This is convenient for modifying the error return value of a function; we will see an example of this shortly.

panic は組み込み関数です、通常の管理フローを止めて、 panic を開始します。 関数 F が panic を呼び出したとき F の実行が止まり F の中にある deffer された関数が通常通り実行され、 F はその呼び出し元に戻ります。 呼び出し側では、F は panic 呼び出しのように振る舞います。 このプロセスは現在の gorutine 内にある全ての関数が戻るまで、スタックを上昇します、そこでプログラムはクラッシュします。 panic というフローは panic を直接呼び出すことで起動されます。 panic というフローは、また out-of-bounds array accesss のようなランタイムエラーでも引き起こされます。

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

recover は組み込み関数です、panic している goroutine の制御を再取得します。 recover は内側の defer された関数にだけ有効です。 通常の実行の間、recover の呼び出しは nil を返し、それ以外の影響はありません。 もし現在の gorutine が panic しているなら、recover の呼び出しは panic に与えられた値が取得され、通常の実行が再開されます。

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

panic と defer の仕組みを示した例のプログラムをここに記します。

Here's an example program that demonstrates the mechanics of panic and 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 より大きいなら panic を起動し、 そうでなければ引数に i+1 を行い再帰呼び出しを行います。 関数 f は recover を呼び出し、recover された値を出力します(recover された値が nil でなければ) この先を読み進める前に、このプログラムの実行結果を想像して見てください。

The function g takes the int i, and panics if i is greater than 3, or else it calls itself with the argument i+1. The function f defers a function that calls recover and prints the recovered value (if it is non-nil). Try to picture what the output of this program might be before reading on.

実行結果は次のようなものになるでしょう。

The program will output:

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 から defer された関数を取り除くと panic は recover されず goroutein のコールスタッックの一番上にたどり着き、 プログラムを終了します。修正したプログラムの実行結果は、次のようなものになるでしょう:

If we remove the deferred function from f the panic is not recovered and reaches the top of the goroutine's call stack, terminating the program. This modified program will output:

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]

panic と recover の現実世界での例は Go の標準ライブラリにある json package をご覧ください。 json pacakge は再帰的な関数群を用いて JSON-encoded data をデコードします。 正しく成形されていない JSON が与えられたとき、 解析器、パーサは panic を呼び出し、最上位の関数呼び出しに向けてスタックを解きほぐしていきます、 そして panic から回復し、適切なエラー値を返します (decode.go にある decodeState type の 'error' と 'unmarshal' メソッドをご覧ください)。

For a real-world example of panic and recover, see the json package from the Go standard library. It decodes JSON-encoded data with a set of recursive functions. When malformed JSON is encountered, the parser calls panic to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value (see the 'error' and 'unmarshal' methods of the decodeState type in decode.go).

Go ライブラリの慣習では、package が内部で完結する panic を使った時でさえも、 それでも外部に対する API は明示的なエラー返り値を提供します。

The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.

(前述した file.Close の範疇を超えて)defer のそのほかの使用例として、mutex を解放することもありますし:

Other uses of defer (beyond the file.Close example given earlier) include releasing a mutex:

mu.Lock()
defer mu.Unlock()

footer を出力することもあります:

printing a footer:

printHeader()
defer printFooter()

そして、ほかにもまだたくさん。

and more.

まとめ、defer 文は (panic, recover を伴う、伴わないに関わらず) 制御フローに対する、普通とはちょっとかわった、そして強力な機能です。 defer 文は、他の言語の中にある特殊な目的の構造で実装された多くの機能をモデルするのに使えます (この文章が具体的に何を指しているか、よくわからない)。ぜひ使って見ましょう。

In summary, the defer statement (with or without panic and recover) provides an unusual and powerful mechanism for control flow. It can be used to model a number of features implemented by special-purpose structures in other programming languages. Try it out.

Andrew Gerrand 著

By Andrew Gerrand

(翻訳) Defer, Panic, and Recover - The Go Blog