【Go入門】deferによる遅延処理とランタイムパニック

シェアする

こんにちは。Go入門ブログの第16回です。
本記事では、goで遅延処理を実行する為の制御構文であるdeferについて解説します。
あわせて、Goにおけるランタイムエラーといえるpanicについても触れていきます。

deferステートメント

deferステートメントは、deferへ渡した処理の実行を、呼び出し元の関数の終わり(returnする)まで遅延させるものです。
言い換えると、関数の終了時に実行する処理を指定することができます。

deferの仕様として、以下の2つの特徴が挙げられます。

  • deferを評価したタイミングの値が保持される
  • 複数の処理をスタックすることができ、LIFO(後入れ先出し)の実行で実行される

deferの評価結果

deferへ渡した関数の引数は、すぐに評価されます。
deferを評価したタイミングの値が保持される点に注意してください。

それでは実例をみてみましょう。

package main

import "fmt"

func main() {
    
    // 変数valに値をセット
    val := "変更前"
    
    // deferで式を登録
    defer fmt.Println(val)
    
    // defer記述後に変数valの値を変更
    val = "変更後"

    fmt.Println("処理完了")
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
処理完了
変更前

deferの登録後に行った変数valの変更が、deferの実行結果に反映されていないことを確認してください。

複数のdeferの評価順序

関数内で複数のdeferステートメントを記述することもできます。
この場合、deferで記述された処理の呼び出しはスタックされ、関数の終了時にLIFO(後入れ先出し)で実行されます。
この処理順序を誤解していると不具合の原因となりますので注意しましょう。

package main

import "fmt"

func main() {
    
    defer fmt.Println("defer1")
    defer fmt.Println("defer2")
    defer fmt.Println("defer3")

    fmt.Println("deferは後入れ先出しで実行されます")
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
deferは後入れ先出しで実行されます
defer3
defer2
defer1

このように、後に記述したdeferほど先に実行されることに留意してください。

deferのメリット

実際にdeferを使用することにどのようなメリットがあるでしょうか。
もっとも効果を発揮するのは、メモリの解放やファイルのクローズといったリソースの解放処理でしょう。

たとえば以下の例では、ファイルのオープンを行っており、その直後にdeferを使用してファイルのクローズを記述しています。
これにより、関数の実行終了時に確実にファイルがクローズさせます。

// ファイルをオープン
file, err := os.Open(/home/user/user-file)
if err != nil {
    // ファイルのオープンに失敗したらreturn
    return
}
// deferでファイルクローズ処理を登録
defer file.close()

// 以降にファイルを操作する処理を記述
...

もちろん、関数の最後にファイルのクローズ処理を記述することもできます。
しかし、メモリ確保と解放、ファイルのオープンとクローズ、ロックの獲得と解放といった対となる動作は、なるべく連続して記述するというのが、間違いを防ぐためのベストプラクティスといえます。

panicとrecover

deferと関連の深い機能にpanicrecoverがあります。
この2つは、正確には制御構文ではなく、定義済みの組み込み関数です。

これらの関数は、ランタイムパニックおよびプログラム定義のエラーにおける、状態の報告と制御を手助けします。
ランタイムパニックは、他の言語で言うところのランタイムエラーであると考えて差し支えありません。

panicの実行

panicを実行すると、即座にランタイムパニックが発生し、実行中の関数は中断されます。
panicはinterface型の引数を取ることができ、ランタイムの停止時に所定の形式で表示されます。

package main

import "fmt"

func main() {
    fmt.Println("[処理開始]")
    
    // panicの実行
    panic("ランタイムエラーが発生しました!")
    
    fmt.Println("[処理終了]") // 実行されない
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[処理開始]
panic: ランタイムエラーが発生しました!

goroutine 1 [running]:
main.main()
/home/user/main.go:9 +0x9d
exit status 2

panicに引数で渡した値が出力されていること、panicの後に記述された処理が実行されていないことを確認してください。

panicとdefer

panicの実行前にdeferで式が登録されている場合、処理の強制終了前にそれらの式はすべて実行されます。
先ほどの例にdeferを追記してみましょう。

package main

import "fmt"

func main() {
    fmt.Println("[処理開始]")
    
    // panicの前にdeferを登録
    defer fmt.Println("defer1")
    defer fmt.Println("defer2")
    
    // panicの実行
    panic("ランタイムエラーが発生しました!")

    // panicの後にdeferを登録
    defer fmt.Println("defer3")

    fmt.Println("[処理終了]") // 実行されない
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[処理開始]
defer2
defer1
panic: ランタイムエラーが発生しました!

goroutine 1 [running]:
main.main()
/home/user/main.go:9 +0x9d
exit status 2

panicの前に登録されたdeferのみ、panicの発生直前に実行されていることを確認してください。

recoverによる回復

recoverはpanicによって発生したランタイムパニックによるプログラムの中断を回復します。

その性質から、recoverはdeferと組み合わせて実行するのが原則です。
panicは処理の実行を中断して、deferに登録された式の実行に移行するので、recoverはdeferステートメントの中でしか動作しません。

recoverはinterface型の値を返します。
その値がnilでなければ、panicが実行されたと判断することができます。


import "fmt"

func main() {
    fmt.Println("[処理開始]")

    // deferでrecover処理を登録
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err) // panic関数の引数が出力される
        }
    }()

    // panicの実行
    panic("ランタイムエラーが発生しました!")

    fmt.Println("[処理終了]") // 実行されない
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[処理開始]
ランタイムエラーが発生しました!

recoverを使用しない場合と比べ、ランタイムパニック自体は出力されず、panicに渡した引数がrecoverによって出力されています。

panicとrecoverの利用

panicとrecoverを組み合わせて使用することで、ある種の例外処理を実現することができます。

package main

import "fmt"

func main() {
    fmt.Println("[処理開始]")

    // panic発生関数の実行
    genPanic(100)    // int
    genPanic("hoge") // string
    genPanic(3.14)   // float

    fmt.Println("[処理終了]")
}

// panic発生関数
func genPanic(panicVal interface{}) {

    defer func() {
        if err := recover(); err != nil {
            // panicの引数の型によって処理を分岐する
            switch v := err.(type) {
            case int:
                fmt.Printf("ランタイムエラーが発生しました! 型:int 値:%v\n", v)
            case string:
                fmt.Printf("ランタイムエラーが発生しました! 型:string 値:%v\n", v)
            default:
                fmt.Println("ランタイムエラーが発生しました! 型:不明")
            }
        }
    }()

    // panicを発生させる
    panic(panicVal)

    return
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[処理開始]
ランタイムエラーが発生しました! 型:int 値:100
ランタイムエラーが発生しました! 型:string 値:hoge
ランタイムエラーが発生しました! 型:不明
[処理終了]

このように、recoverを使用することで、ランタイムパニックから復帰して処理を継続できます。
また、panicに渡した引数をrecoverで評価し、これによって処理分岐を行うことが可能です。

しかし、このような用法は原則として避けるべきです。
安定してプログラムを運用するためには、ランタイムパニックをいかに発生させないかが重要です。
正当な理由がある場合を除いては、任意の関数がpanicを起こす可能性があることを前提に設計すべきではありません。

終わりに

本記事では、主にリソースの解放処理で活用できる制御構文であるdeferステートメントについて解説しました。
関連情報として解説したpanicとrecoverですが、これらを実際に使用することは基本的にはないと考えてよいです。
ただし、Goにおけるランタイムエラーの基礎知識として理解しておくべきでしょう。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク