Errors/Panic/Recover - Golang learning step 1-12

公開
注意最終更新から 2 年以上経過しています。最新の情報もあわせてご確認ください。

roadmap.sh > Go > Learn the Basics > Errors/Panic/Recover の学習を進めていきます。

学習箇所

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

Errors/Panic/Recover

Errors

Go のエラー処理は、Java、JavaScript、Pythonなどの一般的なプログラミング言語とは少し異なります。Go の組み込みエラーには、スタックトレースが含まれておらず、また一般的な try/catch 方式によるエラー処理もサポートしていません。代わりに、Go ではエラーは関数から返される単なる値であり、他のデータ型と同じように扱うことができます。これにより、驚くほどシンプルで軽量な設計が実現されています。

一般的な値として扱うことのできる Go のエラーは error インターフェースを使って表現され、nil ではない値を返す場合、それがエラーを意味します。error インターフェースは次のように定義されています。

type error interface {
  Error() string
}

つまり、エラーとは Error() メソッドを実装したものであれば何でも良く、このメソッドは文字列としてエラーメッセージを返すだけ。それだけのシンプルな仕組みです。

エラーの作成

エラーは、Go の組み込みパッケージである errors や fmt を使用して、その場で作成することができます。例えば、以下の関数では errors パッケージを使用して、固定のエラーメッセージを持つ新しいエラーを返しています:

package main

import "errors"

func DoSomething() error {
  return errors.New("何かがうまく動作しませんでした")
}

同様に、fmt パッケージを使用すると、int、string、あるいは別の error などの動的なデータをエラーに追加することができます。

package main

import "fmt"

func Divide(a, b int) (int, error) {
  if b == 0 {
  return 0, fmt.Errorf("'%d' をゼロで除算することはできません", a)
  }
  return a / b, nil
}
  • エラーは nil として返すことができ、実際、これは Go におけるエラーのデフォルト値(ゼロ値)です。これは重要なポイントで、if err != nil によるチェックは、エラーが発生したかどうかを判断するための慣用的な方法です(他のプログラミング言語でお馴染みの try/catch 文に代わるものです)。
  • エラーは通常、関数の最後の引数として返されます。そのため、上記の例では int と error をこの順序で返しています。
  • (英語の場合)エラーメッセージは通常、小文字で書かれ、句読点で終わることはありません。ただし、固有名詞や大文字で始まる関数名を含む場合などは例外とすることができます。

エラーハンドリングの基本的なパターン

典型的なエラーハンドリングの例は、関数がエラーと通常の値の 2 つを返し、呼び出し元がエラーをチェックするパターンです。

以下の例では、ゼロ除算を試みるとエラーが返され、エラーメッセージが表示されます。

package main

import (
  "errors"
  "fmt"
)

func divide(a, b float64) (float64, error) {
  if b == 0 {
    return 0, errors.New("division by zero")
  }
  return a / b, nil
}

func main() {
  result, err := divide(10, 0)
  if err != nil {
    fmt.Println("エラーが発生:", err)
    return
  }
  fmt.Println("結果:", result)
}

Go では、エラーを作成するために errors.New と fmt.Errorf を使用します。

  • errors.New(): シンプルなエラーメッセージを生成するために使用します。例えば、err := errors.New("something went wrong") という形です。
  • fmt.Errorf(): フォーマット付きメッセージを作成したい場合に便利で、フォーマット指定子を使って詳細なメッセージを提供できます。例えば、err := fmt.Errorf("an error occurred: %v", detail) です。

想定されるエラーの定義

Go におけるもう一つの重要なテクニックは、想定されるエラーを定義することです。これにより、コードの他の部分で明示的にそのエラーをチェックすることができます。これは、特定の種類のエラーが発生した場合に異なるコードの分岐を実行する必要がある際に役立ちます。

package main

import (
  "errors"
  "fmt"
)

// 想定されるエラーを定義
var ErrDivideByZero = errors.New("divide by zero")

func Divide(a, b int) (int, error) {
  if b == 0 {
    return 0, ErrDivideByZero
  }
  return a / b, nil
}

func main() {
  a, b := 10, 0
  result, err := Divide(a, b)
  if err != nil {
    switch {
    case errors.Is(err, ErrDivideByZero): // ErrDivideByZero であるかを確認できる
      fmt.Println("divide by zero error")
    default:
      fmt.Printf("unexpected division error: %s\n", err)
    }
    return
  }

  fmt.Printf("%d / %d = %d\n", a, b, result)
}

カスタムエラー型の実装

Go では、カスタムエラー型を定義することで、独自のエラーメッセージや詳細な情報を持つエラーを作成できます。これは error インターフェースの Error() メソッドを実装することで行います。

package main

import (
  "errors"
  "fmt"
)

// ResourceNotFoundError カスタムエラー型
type ResourceNotFoundError struct {
  Code  int
  Message string
}

func (e *ResourceNotFoundError) Error() string {
  return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func main() {
  err := &ResourceNotFoundError{
    Code:  404,
    Message: "Resource not found",
  }
  fmt.Println(err)
  // => Error 404: Resource not found
}

このようにして、より詳細なエラー情報を保持するカスタムエラーを作成し、特定のエラー処理を柔軟に行うことが可能です。

エラーのラッピング

Go 1.13 以降では、fmt.Errorf を使ってエラーをラップすることができます。エラーをラップする目的は、より詳細なコンテキスト(エラーが発生した場所や原因)を追加しつつ、元のエラーの情報を保持することです。

例えば、ある関数がエラーを返し、そのエラーに追加情報を付加したい場合、fmt.Errorf を使って元のエラーをラップできます。%w を使うことで、元のエラーを含んだ新しいエラーを作成します。

package main

import (
  "errors"
  "fmt"
)

func add(a, b int) (int, error) {
  if a == 0 || b == 0 {
    return 0, errors.New("両方 0 以上にしてください")
  }

  return a + b, nil
}

func addExecute() (int, error) {
  i, err := add(1, 0)
  if err != nil {
    // エラーをラップして、追加のコンテキストを与える
    return 0, fmt.Errorf("addExecute でエラー: %w", err)
  }
  return i, nil
}

func main() {
  i, err := addExecute()
  if err != nil {
    // エラーを表示
    fmt.Println("エラー:", err)
    // => エラー: addExecute でエラー: 両方 0 以上にしてください
    return
  }
  fmt.Println(i)
}
  • fmt.Errorf("sample010101でエラー: %w", err) では、元のエラー err を %w を使ってラップしています。
  • ラップすることで、エラーメッセージに追加のコンテキストが含まれますが、元のエラーも保持されます。

errors.Unwrap

errors.Unwrap は、ラップされたエラーから元のエラーを取り出すために使用されます。これにより、エラーをチェックする際に、ラップされたエラーの中から元の原因となるエラーを取得できるようになります。

package main

import (
  "errors"
  "fmt"
)

func add(a, b int) (int, error) {
  if a == 0 || b == 0 {
    return 0, errors.New("両方 0 以上にしてください")
  }

  return a + b, nil
}

func addExecute() (int, error) {
  i, err := add(1, 0)
  if err != nil {
    // エラーをラップして、追加のコンテキストを与える
    return 0, fmt.Errorf("addExecute でエラー: %w", err)
  }
  return i, nil
}

func main() {
  i, err := addExecute()
  if err != nil {
    fmt.Println("エラー:", err)
    // => エラー: addExecute でエラー: 両方 0 以上にしてください

    // ラップされたエラーを解除して元のエラーを取得
    origErr := errors.Unwrap(err)
    fmt.Println("元のエラー:", origErr)
    // => 元のエラー: 両方 0 以上にしてください

    return
  }
  fmt.Println(i)
}
  • errors.Unwrap(err) は、エラーがラップされている場合に、ラップされた元のエラーを取り出すために使います。
  • ラップされたエラーをチェックする際、元のエラーの情報を知りたい場合に便利です。

errors.Is と errors.As

Go 1.13 以降では、errors.Unwrap だけでなく、エラーチェックを行うための便利な関数 errors.Is と errors.As も提供されています。

  • errors.Is: 特定のエラーと一致するかどうかを確認します。ラップされたエラーであっても、元のエラーを辿って一致するかどうかをチェックします。
  • errors.As: エラーを特定の型(例えば、カスタムエラー型)に変換できるかを確認します。
errors.Is の例:
package main

import (
  "errors"
  "fmt"
)

var ErrLowLevel = errors.New("低レベルのエラーが発生しました")

func fuga() error {
  return ErrLowLevel
}
func hoge() error {
  err := fuga()
  if err != nil {
    return fmt.Errorf("hoge でエラー: %w", err)
  }
  return nil
}

func main() {
  err := hoge()
  if err != nil {
    fmt.Println("エラー:", err)

    // エラーが特定のエラー(ErrLowLevel)かどうかをチェック
    if errors.Is(err, ErrLowLevel) {
      fmt.Println("ErrLowLevel が原因のエラーです")
    }
  }
}
// 出力:
// エラー: hoge でエラー: 低レベルのエラーが発生しました
// ErrLowLevel が原因のエラーです
errors.As の例:

カスタムエラー型をチェックするための例も示します。

package main

import (
  "errors"
  "fmt"
)

type MyError struct {
  Message string
}

func (e *MyError) Error() string {
  return e.Message
}

func DoSomething() error {
  return &MyError{"カスタムエラーが発生しました"}
}

func main() {
  err := DoSomething()

  var myErr *MyError
  // エラーが MyError 型かどうかを確認
  if errors.As(err, &myErr) {
    fmt.Println("カスタムエラー:", myErr)
  } else {
    fmt.Println("異なる型のエラー:", err)
  }
}

エラーハンドリングの効率化

Go でのエラーハンドリングは、各ステップでエラーをチェックし、エラーが発生したら早期に返すことで効率化できます。これにより、余計な処理を行わず、エラー発生箇所が明確になります。fmt.Errorf を使用することで、エラーの詳細なメッセージを付加しつつ、元のエラー情報を保持することが可能です。例えば、次のようなコードで、ステップごとにエラーメッセージを追加して返すことができます。

func DoSomething() error {
  if err := firstStep(); err != nil {
  return fmt.Errorf("failed at first step: %w", err)
  }
  if err := secondStep(); err != nil {
  return fmt.Errorf("failed at second step: %w", err)
  }
  return nil
}

このようにエラーチェックを整理することで、コードはシンプルかつ明瞭になり、エラーハンドリングが効率的になります。

Panic

panic は、プログラムが続行できない致命的なエラーを示します。深刻なエラーが発生した際に使用され、プログラムの実行を即座に停止させます。panic を呼び出すと、現在の関数の実行が停止し、呼び出された側の関数や上位の関数も順次停止します。最終的にプログラムはクラッシュします。

以下のような場合に使用されます。

  • 回復不可能なエラー
  • プログラムの前提条件が満たされていない
  • プログラミングエラー(配列の範囲外アクセスなど)

例えば、panic を使うコードは次の通りです。このプログラムは "Start" を表示した後、panic が発生しプログラムが終了します。

package main

import (
  "errors"
  "fmt"
)

func main() {
  fmt.Println("Start")
  panic("something went wrong")
  fmt.Println("End") // この行は実行されません
}
// 出力:
// Start
// panic: something went wrong

Panic についての詳細

パニックは、プログラムが続行できない重大なエラー状態を表します。通常のエラー処理では対応できない致命的な問題が発生した際に使用されます。パニックが発生すると、プログラムは実行中の関数を中断し、遅延関数(defer)を実行した後、呼び出し元に戻ります。このプロセスは、パニックが処理されるか、プログラムが終了するまで続きます。

パニックが発生する一般的なシナリオ

1. ランタイムパニック

Go のランタイムによって自動的に発生するパニックです。

func main() {
   // 1. 配列の境界外アクセス
   arr := []int{1, 2, 3}
   fmt.Println(arr[10]) // panic: runtime error: index out of range

   // 2. nilポインタの参照
   var p *int
   fmt.Println(*p) // panic: runtime error: nil pointer dereference

   // 3. 型アサーションの失敗
   var i interface{} = "hello"
   num := i.(int) // panic: interface conversion
}
2. アプリケーションパニック

開発者が意図的に発生させるパニックです。

func initializeApp() {
    // 1. 重要な設定ファイルが見つからない
    config, err := loadConfig("config.yaml")
    if err != nil {
        panic(fmt.Sprintf("アプリケーションの初期化に失敗: %v", err))
    }

    // 2. 必須の環境変数が設定されていない
    dbURL := os.Getenv("DATABASE_URL")
    if dbURL == "" {
        panic("DATABASE_URL が設定されていません")
    }

    // 3. データベース接続の確立に失敗
    db, err := sql.Open("postgres", dbURL)
    if err != nil {
        panic(fmt.Sprintf("データベース接続に失敗: %v", err))
    }
}

パニックスタックトレースの読み方

パニックが発生すると、Go はスタックトレースを出力します。これは問題の原因を特定するための重要な情報源です。

panic: runtime error: index out of range [10] with length 3

goroutine 1 [running]:
main.main()
        /home/user/example/main.go:15 +0x1234

スタックトレースの主要な要素:

  1. パニックの種類と理由
  2. パニックが発生した goroutine の情報
  3. 関数呼び出しのチェーン(最も最近の呼び出しが最初)
  4. ソースコードの位置(ファイル名と行番号)

パニックの伝播と回復

パニックは呼び出しスタックを遡って伝播します。各レベルで defer 関数が実行され、recover による回復の機会が提供されます。

package main

import (
  "errors"
  "fmt"
)

// パニックの伝播例
func level3() {
  panic("level 3 panic")
}

func level2() {
  defer func() {
    fmt.Println("level 2 defer")
  }()
  level3()
}

func level1() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Printf("Recovered: %v\n", r)
    }
  }()
  level2()
}

func main() {
  level1()
}

// 出力:
// level 2 defer
// Recovered: level 3 panic

Recover

recover は、panic によってプログラムがクラッシュするのを防ぐために使用されます。recover は defer ステートメントと一緒に使われ、panic が発生した場合にそのパニック状態をキャッチしてプログラムの回復を試みます。

次の例では、recover を使って panic をキャッチしています。このプログラムでは、panic が発生した後でも recover がそのエラーをキャッチして処理し、プログラムが強制終了せずに続行できます。

func handlePanic() {
  if r := recover(); r != nil {
  fmt.Println("Recovered from panic:", r)
  }
}

func riskyFunction() {
  defer handlePanic()
  
  // 何らかの処理
  panic("something went wrong")
}

func main() {
  riskyFunction()
  fmt.Println("プログラムは続行します")
}

defer について

defer は、関数の実行を遅延させ、それを囲む関数(外側の関数)の終了時に実行されることを保証する Go の重要な機能です。defer は主にリソースの解放、ファイルのクローズ、ロックの解除などのクリーンアップ処理に使用され、特に panic からの recover と組み合わせて重要な役割を果たします。

defer の基本的な特徴

1. LIFO(後入れ先出し)の実行順序*

複数の defer 文がある場合、最後に追加された defer から順に実行されます

func deferOrder() {
    defer fmt.Println("1番目")
    defer fmt.Println("2番目")
    defer fmt.Println("3番目")
}
// 出力:
// 3番目
// 2番目
// 1番目
2. 引数の評価タイミング
  • defer 文の引数は、defer 文が評価された時点で計算されます
  • 実行は遅延されますが、引数の値は即座に確定します
func argumentEvaluation() {
    i := 0
    defer fmt.Println("deferred value:", i)
    i = 1
    fmt.Println("current value:", i)
}
// 出力:
// current value: 1
// deferred value: 0

defer の主な使用パターン

1. リソースのクリーンアップ
  • ファイルのクローズ
  • データベース接続の解放
  • ミューテックスのアンロック
func readFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()  // 関数終了時に確実にファイルをクローズ

    // ファイル操作...
    return nil
}
2. panic からの recover

defer 関数内で recover を使用することで、panic をキャッチできます。これにより、プログラムのクラッシュを防ぎ、適切なエラーハンドリングが可能になります。

func mayPanic() error {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered from panic: %v\n", r)
            // パニック情報のログ記録やクリーンアップ処理
        }
    }()

    // パニックが発生する可能性のある処理
    panic("something went wrong")
}
3. トレース/プロファイリング
  • 関数の実行時間の計測
  • ログの記録
func timeTrack(start time.Time, name string) {
    elapsed := time.Since(start)
    fmt.Printf("%s took %s\n", name, elapsed)
}

func longProcess() {
    defer timeTrack(time.Now(), "longProcess")
    
    // 時間のかかる処理...
    time.Sleep(2 * time.Second)
}

defer 使用時の注意点

1. メモリ使用量
  • defer はスタックに情報を保持するため、大量に使用すると若干のメモリオーバーヘッドが発生します
  • ループ内での不適切な defer の使用に注意
// 悪い例: ループ内で defer を使用
func badExample() {
    for i := 0; i < 100000; i++ {
        file, _ := os.Open("file.txt")
        defer file.Close()  // ループが終わるまでクローズされない
    }
}

// 良い例: 関数に分離
func goodExample() {
    for i := 0; i < 100000; i++ {
        processFile("file.txt")
    }
}

func processFile(filename string) {
    file, _ := os.Open(filename)
    defer file.Close()
    // ファイル処理
}
2. メソッド値のキャプチャ

defer にメソッドを登録する際、レシーバの値も一緒にキャプチャされます

type Person struct {
    name string
}

func (p *Person) updateName() {
    defer p.printName()
    p.name = "New Name"  // defer された printName は古い名前を使用
}

func (p *Person) printName() {
    fmt.Println(p.name)
}
3. 名前付き返り値との関係

defer 内で名前付き返り値を変更できまが、これは時として予期しない動作を引き起こす可能性があります

func namedReturnValue() (result int) {
    defer func() {
        result *= 2  // 返り値を変更
    }()
    
    return 10  // 20 が返される
}

defer ベストプラクティス

  1. リソース管理には必ず defer を使用
    • ファイル、接続、ロックなどのリソースは必ず defer で解放
    • エラーハンドリングを簡潔に保つ
  2. panic/recover との組み合わせ
    • recover は必ず defer 内で使用
    • パニックが発生した場合のクリーンアップ処理も忘れずに実装
  3. シンプルに保つ
    • defer 内の処理はできるだけシンプルに
    • 複雑な処理が必要な場合は、別の関数に分離
  4. 適切なスコープで使用
    • リソースを確保したらすぐに defer で解放を登録
    • ループ内での使用は避ける

エラーハンドリングのベストプラクティス

1. 通常のエラー処理には error を使用

  • エラーは明示的に処理し、暗黙的な処理を避ける
  • エラーチェックは冗長に見えても、省略しない
// 良い例:エラーを明示的に処理
func processUser(id string) error {
  user, err := getUser(id)
  if err != nil {
  return fmt.Errorf("getting user %s: %w", id, err)
  }

  err = validateUser(user)
  if err != nil {
  return fmt.Errorf("validating user %s: %w", id, err)
  }

  return saveUser(user)
}

// 悪い例:エラーを無視
func processUser(id string) {
  user, _ := getUser(id)  // エラーを無視
  saveUser(user)    // エラーを無視
}

2. panic は本当に必要な場合のみ使用

panic を使用すべき場合

01. 初期化時の致命的なエラー

func initDatabase() {
  db, err := sql.Open("postgres", connectionString)
  if err != nil {
  panic(fmt.Sprintf("fatal error: cannot connect to database: %v", err))
  }
}
  • プログラミングエラー(実行時には発生しないはずのエラー)
func NewConfig(path string) *Config {
  if path == "" {
  panic("programming error: config path cannot be empty")
  }
  // ...
}

02. エラーをラップして文脈を追加する

  • %w を使用して元のエラーを保持
  • エラーチェーンを作成する際は、有用な情報を追加
  • エラーの発生場所や状況を明確に
package main

import (
  "errors"
  "fmt"
)

// ErrNotFound 基本的なエラー定数
var ErrNotFound = errors.New("not found")

// MyError カスタムエラー型
type MyError struct {
  Message string
}

func (e *MyError) Error() string {
  return e.Message
}

func getData() error {
  // ErrNotFound をラップして返す(errors.Is で補足する用)
  return fmt.Errorf("failed to get data: %w", ErrNotFound)

  // カスタムエラーを作成してラップ(errors.As で補足する用)
  //myErr := &MyError{Message: "something went wrong"}
  //return fmt.Errorf("failed to get data: %w", myErr)
}

func main() {
  err := getData()
  if err != nil {
    // Is: エラー値の比較
    if errors.Is(err, ErrNotFound) {
      fmt.Println("見つかりませんでした")
      return
    }

    // As: エラー型の検査
    var myErr *MyError
    if errors.As(err, &myErr) {
      fmt.Printf("MyErrorです: %s\n", myErr.Message)
      return
    }

    fmt.Printf("その他のエラー: %v\n", err)
  }
}

Go 1.13 以降では errors パッケージに追加された Is、As、Unwrap 関数を活用することで、より柔軟なエラーハンドリングが可能になっています。

03. エラーは早期にチェック

  • 前提条件は関数の開始時にチェック
  • ネストを減らし、コードの可読性を向上
  • 早期リターンパターンを活用
// 良い例:早期リターン
func processUserData(data []byte, config *Config) error {
  // 前提条件のチェック
  if len(data) == 0 {
  return errors.New("data is empty")
  }
  if config == nil {
  return errors.New("config is nil")
  }

  // メイン処理
  return processData(data, config)
}

// 悪い例:深いネスト
func processUserData(data []byte, config *Config) error {
  if len(data) > 0 {
  if config != nil {
    return processData(data, config)
  } else {
    return errors.New("config is nil")
  }
  } else {
  return errors.New("data is empty")
  }
}

04. recover は適切なスコープで使用

  • パニックの影響範囲を制限
  • ログ記録やクリーンアップに活用
  • goroutine ごとに適切に処理
// Web APIでのパニック処理
func withRecover(handler http.HandlerFunc) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
  defer func() {
    if err := recover(); err != nil {
    // パニックをログに記録
    log.Printf("panic: %v\n%s", err, debug.Stack())
    // クライアントにエラーレスポンスを返す
    w.WriteHeader(http.StatusInternalServerError)
    json.NewEncoder(w).Encode(map[string]string{
      "error": "Internal Server Error",
    })
    }
  }()
  handler(w, r)
  }
}

// goroutineでのパニック処理
func runWorker(jobs <-chan Job) {
  go func() {
  defer func() {
    if err := recover(); err != nil {
    log.Printf("worker panic: %v", err)
    // 必要に応じてWorkerを再起動
    }
  }()

  for job := range jobs {
    processJob(job)
  }
  }()
}

// サーバーの初期化
func main() {
  http.HandleFunc("/api", withRecover(apiHandler))
  http.ListenAndServe(":8080", nil)
}

まとめ

  • Go のエラー処理は他の言語とは異なり、関数から返されるエラー値をチェックするシンプルな仕組みを採用しています。
  • errors.New や fmt.Errorf を使って、エラーの生成や詳細なメッセージの追加が簡単に行えます。
  • Go 1.13 以降では、errors.Is や errors.As を使って、特定のエラーの検出やカスタムエラー型の確認が可能です。
  • panic は深刻なエラーやプログラムの前提条件が満たされない場合に使用され、プログラムを即座に終了させます。
  • recover を使用することで、panic の影響を最小限に抑え、プログラムのクラッシュを回避できます。
  • defer は関数終了時にクリーンアップ処理やリソースの解放を保証するための重要な機能です。
  • panic と recover の組み合わせは、適切なエラーハンドリングや回復が必要な場面で活用されます。
Ritolabo