Go言語入門:効果的なGo -Recover-

スポンサーリンク
Go言語入門:効果的なGo -Recover- ノウハウ
Go言語入門:効果的なGo -Recover-
この記事は約25分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語を効果的に使うためのガイドラインについて解説しています。

スポンサーリンク

背景

Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。

リカバー

パニックが呼び出されると、スライスの範囲外インデックスや型アサーション失敗などの実行時エラーに対する暗黙的な呼び出しも含めて、現在の関数の実行を即座に停止し、ゴルーチンのスタックを巻き戻し始め、その過程で延期された関数を実行します。その巻き戻しがゴルーチンのスタックの上部に達すると、プログラムは終了します。しかし、組み込み関数recoverを使用してゴルーチンの制御を取り戻し、通常の実行を再開することが可能です。

recoverの呼び出しは巻き戻しを停止し、panicに渡された引数を返します。巻き戻し中に実行されるコードは延期された関数内のみであるため、recoverは延期された関数内でのみ有用です。

recoverの一つの応用は、他の実行中のゴルーチンを殺すことなく、サーバー内の失敗したゴルーチンをシャットダウンすることです。

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

この例では、do(work)がパニックすると、結果がログに記録され、ゴルーチンは他を妨げることなくきれいに終了します。延期されたクロージャで他に何もする必要はありません。recoverを呼び出すことで条件を完全に処理します。

recoverは延期された関数から直接呼び出されない限り常にnilを返すため、延期されたコードはpanicrecoverを使用するライブラリルーチンを失敗することなく呼び出すことができます。例として、safelyDoの延期された関数はrecoverを呼び出す前にログ関数を呼び出すかもしれませんが、そのログコードはパニック状態に影響されることなく実行されます。

回復パターンが適用されると、do関数(およびそれが呼び出すもの)はpanicを呼び出すことでどんな悪い状況からもきれいに抜け出すことができます。この考えを使用して複雑なソフトウェアでのエラー処理を簡素化できます。ローカルエラー型でpanicを呼び出すことで解析エラーを報告するregexpパッケージの理想化されたバージョンを見てみましょう。以下はErrorの定義、エラーメソッド、およびCompile関数です。

// Errorは解析エラーの型;errorインターフェースを満たします。
type Error string
func (e Error) Error() string {
    return string(e)
}

// errorは*Regexpのメソッドで、Errorでパニックすることで解析エラーを報告します。
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compileは正規表現の解析された表現を返します。
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParseは解析エラーがある場合パニックします。
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // 戻り値をクリア。
            err = e.(Error) // 解析エラーでない場合は再パニック。
        }
    }()
    return regexp.doParse(str), nil
}

doParseがパニックすると、回復ブロックは戻り値をnilに設定します—延期された関数は名前付き戻り値を変更できます。そして、errへの代入で、ローカル型Errorであることをアサートすることで、問題が解析エラーであったかを確認します。そうでない場合、型アサーションは失敗し、何も中断されなかったかのようにスタック巻き戻しを続ける実行時エラーを引き起こします。このチェックは、範囲外インデックスなどの予期しないことが起こった場合、解析エラーを処理するためにpanicrecoverを使用していても、コードが失敗することを意味します。

エラー処理が適切に行われていると、errorメソッド(型にバインドされたメソッドなので、組み込みのerror型と同じ名前を持つことは問題なく、むしろ自然です)により、解析スタックを手動で巻き戻すことを心配せずに解析エラーを報告することが簡単になります:

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

このパターンは有用ですが、パッケージ内でのみ使用されるべきです。Parseは内部のパニック呼び出しをエラー値に変換します。クライアントにパニックを公開しません。それは従うべき良いルールです。

ところで、この再パニックイディオムは、実際のエラーが発生した場合、パニック値を変更します。しかし、元の失敗と新しい失敗の両方がクラッシュレポートに表示されるため、問題の根本原因は依然として見えます。したがって、この単純な再パニックアプローチは通常十分です—結局のところクラッシュなのですから—しかし、元の値のみを表示したい場合は、予期しない問題をフィルタリングして元のエラーで再パニックするために、もう少しコードを書くことができます。それは読者への演習として残しておきます。

recoverとは?

recoverは、パニックによる実行停止を「キャッチ」して、プログラムの正常実行を回復させる組み込み関数です。ただし、defer文の中でのみ有効です。

基本的なrecoverの使用例

package main

import (
    "fmt"
    "log"
)

func dangerousFunction(x int) {
    if x == 0 {
        panic("ゼロは許可されていません")
    }
    fmt.Printf("処理: %d\n", 100/x)
}

func safeWrapper(x int) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("パニックをキャッチしました: %v\n", err)
        }
    }()
    
    dangerousFunction(x)
    fmt.Println("正常に完了")
}

func basicRecoverExample() {
    fmt.Println("=== 基本的なrecoverの例 ===")
    
    // 正常なケース
    fmt.Println("--- 正常なケース ---")
    safeWrapper(5)
    
    // パニックが発生するケース
    fmt.Println("\n--- パニックケース ---")
    safeWrapper(0)
    
    fmt.Println("\nプログラム継続中...")
}

func main() {
    basicRecoverExample()
}

サーバーでの実用例

package main

import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"
)

type Work struct {
    ID   int
    Data string
}

// 危険な処理をシミュレート
func do(work *Work) {
    fmt.Printf("作業 %d を処理中: %s\n", work.ID, work.Data)
    
    // ランダムに処理時間を設定
    time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
    
    // 一定確率でパニックを発生
    if rand.Float32() < 0.3 {
        panic(fmt.Sprintf("作業 %d で予期しないエラーが発生", work.ID))
    }
    
    fmt.Printf("作業 %d 完了\n", work.ID)
}

// 安全な実行ラッパー
func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("作業失敗: %v\n", err)
        }
    }()
    
    do(work)
}

// サーバー
func server(workChan <-chan *Work, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for work := range workChan {
        go func(w *Work) {
            safelyDo(w)
        }(work)
    }
}

func serverExample() {
    fmt.Println("=== サーバーでのrecover例 ===")
    
    workChan := make(chan *Work, 5)
    var wg sync.WaitGroup
    
    // サーバーを開始
    wg.Add(1)
    go server(workChan, &wg)
    
    // 作業を送信
    for i := 1; i <= 10; i++ {
        work := &Work{
            ID:   i,
            Data: fmt.Sprintf("タスク-%d", i),
        }
        workChan <- work
        time.Sleep(100 * time.Millisecond)
    }
    
    close(workChan)
    wg.Wait()
    
    fmt.Println("全ての作業が処理されました")
}

func main() {
    serverExample()
}

正規表現パーサーの例

package main

import (
    "fmt"
    "strings"
)

// カスタムエラー型
type ParseError string

func (e ParseError) Error() string {
    return string(e)
}

// 正規表現構造体
type Regexp struct {
    pattern string
    tokens  []string
}

// パニックによるエラー報告
func (re *Regexp) error(err string) {
    panic(ParseError(err))
}

// トークン化のシミュレート
func (re *Regexp) doParse(pattern string) *Regexp {
    re.pattern = pattern
    
    // 簡単な解析ルール
    if strings.HasPrefix(pattern, "*") {
        re.error("'*' illegal at start of expression")
    }
    
    if strings.Contains(pattern, "((") {
        re.error("unclosed parenthesis")
    }
    
    if pattern == "" {
        re.error("empty pattern")
    }
    
    // 正常なケース
    re.tokens = strings.Split(pattern, "")
    return re
}

// パニックをエラーに変換するコンパイル関数
func Compile(pattern string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    
    defer func() {
        if e := recover(); e != nil {
            regexp = nil              // 戻り値をクリア
            err = e.(ParseError)      // ParseErrorで型アサーション
        }
    }()
    
    return regexp.doParse(pattern), nil
}

func (re *Regexp) String() string {
    return fmt.Sprintf("Regexp{pattern: %s, tokens: %v}", re.pattern, re.tokens)
}

func regexpParserExample() {
    fmt.Println("=== 正規表現パーサーの例 ===")
    
    patterns := []string{
        "abc",           // 正常
        "a*b",           // 正常
        "*abc",          // エラー: *が先頭
        "a((b",          // エラー: 未閉じ括弧
        "",              // エラー: 空パターン
        "hello",         // 正常
    }
    
    for _, pattern := range patterns {
        fmt.Printf("\nパターン: '%s'\n", pattern)
        
        regexp, err := Compile(pattern)
        if err != nil {
            fmt.Printf("  解析エラー: %v\n", err)
        } else {
            fmt.Printf("  成功: %s\n", regexp)
        }
    }
}

func main() {
    regexpParserExample()
}

高度なrecoverパターン

package main

import (
    "fmt"
    "runtime"
)

// 複数のエラー型
type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation error in field '%s': %s", e.Field, e.Message)
}

type SystemError struct {
    Component string
    Message   string
}

func (e SystemError) Error() string {
    return fmt.Sprintf("system error in %s: %s", e.Component, e.Message)
}

// 階層的なrecoverハンドリング
func advancedRecover() error {
    defer func() {
        if r := recover(); r != nil {
            // パニックの種類に応じて処理
            switch e := r.(type) {
            case ValidationError:
                fmt.Printf("検証エラーをキャッチ: %v\n", e)
                
            case SystemError:
                fmt.Printf("システムエラーをキャッチ: %v\n", e)
                // システムエラーは再パニック
                panic(e)
                
            case string:
                fmt.Printf("文字列パニックをキャッチ: %s\n", e)
                
            default:
                fmt.Printf("未知のパニックをキャッチ: %v (型: %T)\n", e, e)
                // スタックトレースを表示
                buf := make([]byte, 1024)
                n := runtime.Stack(buf, false)
                fmt.Printf("スタックトレース:\n%s", buf[:n])
                // 再パニック
                panic(e)
            }
        }
    }()
    
    // 様々なエラーをテスト
    validateUser("", 25)        // ValidationError
    connectToDatabase()         // SystemError
    
    return nil
}

func validateUser(name string, age int) {
    if name == "" {
        panic(ValidationError{
            Field:   "name",
            Message: "name cannot be empty",
        })
    }
    
    if age < 0 {
        panic(ValidationError{
            Field:   "age",
            Message: "age cannot be negative",
        })
    }
}

func connectToDatabase() {
    panic(SystemError{
        Component: "database",
        Message:   "connection timeout",
    })
}

func advancedRecoverExample() {
    fmt.Println("=== 高度なrecoverパターンの例 ===")
    
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("最上位でキャッチしたパニック: %v\n", r)
        }
    }()
    
    err := advancedRecover()
    if err != nil {
        fmt.Printf("エラーとして返却: %v\n", err)
    }
}

func main() {
    advancedRecoverExample()
}

パニックチェーンとrecoverの組み合わせ

package main

import (
    "fmt"
    "log"
)

// ネストしたパニックの処理
func levelOne() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Level 1でパニックをキャッチ: %v", r)
            // さらに処理を追加してから再パニック
            panic(fmt.Sprintf("Level 1経由: %v", r))
        }
    }()
    
    levelTwo()
}

func levelTwo() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Level 2でパニックをキャッチ: %v", r)
            // 条件によって処理を分岐
            if str, ok := r.(string); ok && str == "recoverable" {
                log.Println("回復可能なエラーのため処理継続")
                return // パニックを吸収
            }
            // その他は再パニック
            panic(fmt.Sprintf("Level 2経由: %v", r))
        }
    }()
    
    levelThree()
}

func levelThree() {
    fmt.Println("Level 3で処理中...")
    panic("critical error") // パニックを発生
}

// 回復可能なケース
func recoverableCase() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("回復可能ケースでパニックをキャッチ: %v", r)
        }
    }()
    
    levelTwoRecoverable()
}

func levelTwoRecoverable() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Level 2で回復可能パニックをキャッチ: %v", r)
            // 回復可能エラーは吸収
            return
        }
    }()
    
    panic("recoverable")
}

func panicChainExample() {
    fmt.Println("=== パニックチェーンの例 ===")
    
    // Case 1: 回復不可能なパニック
    fmt.Println("--- 回復不可能なケース ---")
    func() {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("最終レベルでキャッチ: %v", r)
            }
        }()
        
        levelOne()
    }()
    
    // Case 2: 回復可能なパニック
    fmt.Println("\n--- 回復可能なケース ---")
    recoverableCase()
    fmt.Println("プログラム継続")
}

func main() {
    panicChainExample()
}

実用的なHTTPハンドラーでのrecover

package main

import (
    "fmt"
    "log"
    "net/http"
    "runtime"
    "time"
)

// HTTPハンドラーの安全なラッパー
func safeHandler(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // ログに記録
                log.Printf("HTTP handler panic: %v", err)
                
                // スタックトレースも記録
                buf := make([]byte, 1024)
                n := runtime.Stack(buf, false)
                log.Printf("Stack trace:\n%s", buf[:n])
                
                // クライアントにエラーレスポンス
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        
        handler(w, r)
    }
}

// 危険な処理を含むハンドラー
func dangerousHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.URL.Query().Get("id")
    
    if userID == "" {
        panic("User ID is required")
    }
    
    if userID == "999" {
        panic("User 999 triggers system error")
    }
    
    // 模擬的な重い処理
    time.Sleep(100 * time.Millisecond)
    
    fmt.Fprintf(w, "Hello, user %s!", userID)
}

func normalHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "This is a normal handler")
}

func httpRecoverExample() {
    fmt.Println("=== HTTPハンドラーでのrecover例 ===")
    
    // ハンドラーを登録(安全なラッパーで包む)
    http.HandleFunc("/safe", safeHandler(dangerousHandler))
    http.HandleFunc("/normal", safeHandler(normalHandler))
    
    // テスト用のクライアント関数
    testClient := func(url string) {
        resp, err := http.Get(url)
        if err != nil {
            log.Printf("Request error: %v", err)
            return
        }
        defer resp.Body.Close()
        
        fmt.Printf("Response from %s: Status %d\n", url, resp.StatusCode)
    }
    
    // サーバーを別ゴルーチンで開始
    go func() {
        log.Println("Server starting on :8080")
        if err := http.ListenAndServe(":8080", nil); err != nil {
            log.Printf("Server error: %v", err)
        }
    }()
    
    // サーバーの起動を待つ
    time.Sleep(1 * time.Second)
    
    // テストリクエスト
    fmt.Println("テストリクエストを送信中...")
    testClient("http://localhost:8080/normal")
    testClient("http://localhost:8080/safe")
    testClient("http://localhost:8080/safe?id=123")
    testClient("http://localhost:8080/safe?id=999")
    
    fmt.Println("HTTPテスト完了")
}

func main() {
    // HTTPサーバーの例は実際には実行しない(デモ用)
    fmt.Println("HTTP recover の例(実際のサーバーは起動しません)")
    
    // 代わりに他の例を実行
    panicChainExample()
}

重要なポイント

1. recoverの基本原則

  • defer内でのみ有効defer文の中でのみ機能
  • パニックを停止:スタック巻き戻しを停止して制御を回復
  • 値の取得panicに渡された値を取得可能

2. recoverの戻り値

  • パニック時panicに渡された値を返す
  • 正常時nilを返す
  • defer外:常にnilを返す

3. 実用的なパターン

defer func() {
    if err := recover(); err != nil {
        // パニック処理
        log.Printf("Panic: %v", err)
    }
}()

4. 適切な使用場面

  • サーバーの安定性:個別のリクエスト処理でのパニック分離
  • パニックからエラーへの変換:ライブラリの内部実装
  • 複雑なエラー処理:深い関数呼び出しでのエラー伝播

5. 型アサーションとの組み合わせ

if e, ok := err.(SpecificError); ok {
    // 特定のエラー型として処理
} else {
    // 再パニックや別の処理
    panic(err)
}

6. 注意事項

  • パッケージ境界:パニックはパッケージ内で処理し、外部に漏らさない
  • 適切な再パニック:予期しないパニックは再パニックする
  • ログ記録:デバッグのための適切な情報記録

recoverは、Goの堅牢なエラーハンドリング戦略の重要な部分であり、プログラムの安定性を大幅に向上させることができます。ただし、適切に使用することが重要です。

おわりに 

本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました