Go/Standard library/errors
表示
< Go | Standard library
はじめに
[編集]Goにおけるエラーハンドリングは、例外処理ではなく明示的なエラー値を返すことで行われます。errorsパッケージは、このエラーハンドリングの中核となる機能を提供し、エラーの作成、ラップ、アンラップなどの操作を可能にします。
基本的なエラーの作成
[編集]errors.New()
[編集]最も基本的なエラーの作成方法はerrors.New()関数を使用することです。
package main import ( "errors" "fmt" ) func divide(x, y float64) (quotient float64, err error) { switch { case x == 0 && y == 0: err = errors.New("division zero by zero") case y == 0: err = errors.New("division by zero") default: quotient = x / y } return } func main() { if result, err := divide(10, 0); err != nil { fmt.Printf("エラーが発生しました: %v\n", err) } else { fmt.Printf("結果: %f\n", result) } }
fmt.Errorf()
[編集]より柔軟なエラーメッセージを作成したい場合は、fmt.Errorf()を使用します。
func validateAge(age int) (err error) { switch { case age < 0: err = fmt.Errorf("年齢は負の値にできません: %d", age) case age > 150: err = fmt.Errorf("年齢が異常に大きいです: %d", age) } return }
エラーのラップ(Wrapping)
[編集]Go 1.13以降、エラーをラップする機能が追加されました。これにより、エラーの文脈を保持しながら新しいエラー情報を追加できます。
fmt.Errorf()による基本的なラップ
[編集]package main import ( "fmt" "os" ) func readConfig(filename string) error { if file, err := os.Open(filename); err != nil { return fmt.Errorf("設定ファイルを読み込めません: %w", err) } else { file.Close() } return nil } func main() { if err := readConfig("config.txt"); err != nil { fmt.Printf("エラー: %v\n", err) } }
%w動詞を使用することで、元のエラーをラップできます。
複数レベルのラップ
[編集]func loadDatabase() error { if err := connectToDatabase(); err != nil { return fmt.Errorf("データベース接続に失敗: %w", err) } return nil } func connectToDatabase() error { // 実際のデータベース接続処理 return fmt.Errorf("接続タイムアウト") } func initializeApp() error { if err := loadDatabase(); err != nil { return fmt.Errorf("アプリケーション初期化に失敗: %w", err) } return nil }
エラーのアンラップ(Unwrapping)
[編集]errors.Unwrap()
[編集]ラップされたエラーから元のエラーを取得するにはerrors.Unwrap()を使用します。
func main() { if err := initializeApp(); err != nil { fmt.Printf("最上位エラー: %v\n", err) // 一段階アンラップ if unwrapped := errors.Unwrap(err); unwrapped != nil { fmt.Printf("アンラップされたエラー: %v\n", unwrapped) } } }
errors.Is()
[編集]特定のエラーが含まれているかチェックするにはerrors.Is()を使用します。
var ErrNotFound = errors.New("データが見つかりません") func findUser(id int) error { if id == 0 { return fmt.Errorf("無効なユーザーID: %w", ErrNotFound) } return nil } func main() { if err := findUser(0); errors.Is(err, ErrNotFound) { fmt.Println("ユーザーが見つかりませんでした") } }
errors.As()
[編集]エラーを特定の型にキャストするにはerrors.As()を使用します。
type ValidationError struct { Field string Value interface{} } func (e *ValidationError) Error() string { return fmt.Sprintf("バリデーションエラー: フィールド '%s' の値 '%v' が無効です", e.Field, e.Value) } func validateUser(name string, age int) error { if age < 0 { return &ValidationError{Field: "age", Value: age} } return nil } func main() { if err := validateUser("太郎", -1); err != nil { var validationErr *ValidationError if errors.As(err, &validationErr) { fmt.Printf("バリデーションエラーが発生: %s\n", validationErr.Field) } } }
カスタムエラー型
[編集]基本的なカスタムエラー
[編集]type FileError struct { Filename string Operation string Err error } func (e *FileError) Error() string { return fmt.Sprintf("ファイル '%s' での %s 操作に失敗: %v", e.Filename, e.Operation, e.Err) } func (e *FileError) Unwrap() error { return e.Err } func readFile(filename string) error { if _, err := os.Open(filename); err != nil { return &FileError{ Filename: filename, Operation: "読み込み", Err: err, } } return nil }
エラーチェーンの実装
[編集]type ChainedError struct { Message string Cause error } func (e *ChainedError) Error() string { if e.Cause != nil { return fmt.Sprintf("%s: %v", e.Message, e.Cause) } return e.Message } func (e *ChainedError) Unwrap() error { return e.Cause } func processData(data []byte) error { if len(data) == 0 { return &ChainedError{ Message: "データ処理に失敗", Cause: errors.New("空のデータ"), } } return nil }
エラーハンドリングのベストプラクティス
[編集]エラーを無視しない
[編集]// 悪い例 file, _ := os.Open("config.txt") // 良い例 file, err := os.Open("config.txt") if err != nil { return fmt.Errorf("設定ファイルを開けません: %w", err) } defer file.Close()
適切なエラーメッセージ
[編集]// 悪い例 func validateInput(input string) error { if input == "" { return errors.New("エラー") } return nil } // 良い例 func validateInput(input string) error { if input == "" { return errors.New("入力値が空です") } return nil }
エラーの早期返却
[編集]func processUser(userID int) error { user, err := getUser(userID) if err != nil { return fmt.Errorf("ユーザー取得に失敗: %w", err) } if err := validateUser(user); err != nil { return fmt.Errorf("ユーザー検証に失敗: %w", err) } if err := saveUser(user); err != nil { return fmt.Errorf("ユーザー保存に失敗: %w", err) } return nil }
エラーのロギング
[編集]構造化ログ
[編集]import ( "log" "fmt" ) func handleError(err error) { if err != nil { log.Printf("エラーが発生しました: %v", err) // エラーチェーンを辿る current := err for current != nil { log.Printf(" - %v", current) current = errors.Unwrap(current) } } }
エラーレベルの分類
[編集]type ErrorLevel int const ( ErrorLevelInfo ErrorLevel = iota ErrorLevelWarning ErrorLevelError ErrorLevelFatal ) type LeveledError struct { Level ErrorLevel Message string Err error } func (e *LeveledError) Error() string { return fmt.Sprintf("[%s] %s: %v", e.levelString(), e.Message, e.Err) } func (e *LeveledError) levelString() string { switch e.Level { case ErrorLevelInfo: return "INFO" case ErrorLevelWarning: return "WARNING" case ErrorLevelError: return "ERROR" case ErrorLevelFatal: return "FATAL" default: return "UNKNOWN" } }
テストでのエラーハンドリング
[編集]エラーの検証
[編集]func TestDivideByZero(t *testing.T) { _, err := divide(10, 0) if err == nil { t.Error("ゼロ除算でエラーが発生しませんでした") } expected := "division by zero" if err.Error() != expected { t.Errorf("期待されたエラーメッセージ: %s, 実際: %s", expected, err.Error()) } }
エラーの型チェック
[編集]func TestValidationError(t *testing.T) { err := validateUser("", -1) var validationErr *ValidationError if !errors.As(err, &validationErr) { t.Error("ValidationErrorが期待されましたが、異なる型でした") } if validationErr.Field != "age" { t.Errorf("期待されたフィールド: age, 実際: %s", validationErr.Field) } }
まとめ
[編集]errorsパッケージは、Goにおけるエラーハンドリングの基盤を提供します。適切なエラーの作成、ラップ、アンラップの技術を習得することで、保守性が高く、デバッグしやすいコードを書くことができます。
- 重要なポイント:
- エラーは値として扱い、明示的にチェックする
fmt.Errorf()と%wを使用してエラーをラップするerrors.Is()とerrors.As()を使用してエラーを適切に判定する- カスタムエラー型を定義して、より詳細な情報を提供する
- エラーメッセージは明確で実用的にする
- テストでエラーハンドリングを検証する
これらの原則に従うことで、堅牢で保守性の高いGoアプリケーションを開発できます。