Goにおけるエラー生成の微妙な技術:errors.Newとfmt.Errorfの理解
Emily Parker
Product Engineer · Leapcell

Goのエラーハンドリングへのアプローチは、イディオマティックです:エラーは単なる値です。このシンプルさは強力で柔軟なシステムを暗示していますが、強力なツールと同様に、効果的に使用するには理解が必要です。エラーを作成するための2つの基本的な関数はerrors.New
とfmt.Errorf
です。どちらもerror
インターフェースを返しますが、その意図されたユースケースと根本的な機能は大きく異なります。これらの違いを理解することは、堅牢で保守可能でデバッグしやすいGoアプリケーションを作成するために不可欠です。
errors.New
:シンプルで不透明なエラー
errors.New
関数は、基本的なエラーを作成する最も簡単な方法です。単一の文字列引数を取り、その文字列を返すError()
メソッドを実装するerror
値を返します。
package main import ( "errors" "fmt" ) func validateInput(input string) error { if input == "" { return errors.New("input cannot be empty") } if len(input) > 20 { return errors.New("input exceeds max length of 20 characters") } return nil } func main() { if err := validateInput(""); err != nil { fmt.Println("Error:", err) } if err := validateInput("This is a very long input string that will definitely exceed the limit."); err != nil { fmt.Println("Error:", err) } if err := validateInput("valid input"); err == nil { fmt.Println("Input is valid.") } }
errors.New
の使用時期:
-
センチネルエラー: その主なユースケースは、「センチネルエラー」—呼び出し側が直接の等価性(
==
)を使用してチェックできる、特定の前定義されたエラー値—を定義することです。これらのエラーは非常に具体的な種類の失敗を伝えます。package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("item not found") var ErrPermissionDenied = errors.New("permission denied") func findItem(id int) (string, error) { if id == 1001 { return "", ErrNotFound } if id == 2002 { return "", ErrPermissionDenied } return fmt.Sprintf("Item-%d", id), nil } func main() { if _, err := findItem(1001); err != nil { if errors.Is(err, ErrNotFound) { fmt.Println("Client error: The requested item was not found.") } else if errors.Is(err, ErrPermissionDenied) { fmt.Println("Authentication error: You do not have permission.") } else { fmt.Println("Unexpected error:", err) } } }
この例では、
ErrNotFound
とErrPermissionDenied
は、正確なエラー処理を可能にする、区別されたセンチネルエラーです。errors.Is
関数は、Go 1.13で導入され、センチネルエラーをチェックする標準的な方法であり、ラップされたエラーを正しく処理します(fmt.Errorf
でさらに詳しく説明します)。 -
シンプルで静的なエラー: エラーメッセージが固定されており、動的な情報が必要ない場合。
errors.New
の制限:
- コンテキストの欠如: 静的な文字列のみを提供します。エラーの発生に固有の動的な情報(エラーを引き起こした特定の値、開けなかったファイルパス、またはこのエラーにつながった根本的なエラーなど)を含めることはできません。
- エラーのラップ不可:
errors.New
で根本的なエラーをラップすることはできません。これは、呼び出しスタックと元の障害のコンテキストを失うことを意味します。これにより、デバッグが大幅に困難になります。
fmt.Errorf
:動的で、コンテキストで、ラップ可能なエラー
fmt.Errorf
関数は、エラーを作成するためのより強力で汎用性の高い方法です。fmt.Sprintf
と同様に機能し、さまざまな動詞を使用してエラー文字列をフォーマットできます。特に、エラーラッピングのための%w
動詞をサポートしています。
package main import ( "errors" "fmt" "os" ) func readFile(filepath string) ([]byte, error) { data, err := os.ReadFile(filepath) if err != nil { // ここで、os.ReadFileから返された根本的なエラーをラップします。 // %w動詞は、'err'がラップされたエラーであることを示します。 return nil, fmt.Errorf("failed to read file '%s': %w", filepath, err) } return data, nil } func processFile(filename string) error { _, err := readFile(filename) if err != nil { // ここでもラップできます。このレイヤーにさらにコンテキストを追加します。 return fmt.Errorf("error processing data from %s: %w", filename, err) } return nil } func main() { // ファイルが見つからないエラーをシミュレート if err := processFile("non_existent_file.txt"); err != nil { fmt.Println("Main handler received error:", err) // 完全なエラーチェーンを表示します。 // 元のエラーがosパッケージからの「見つからない」エラーであったか確認 var pathErr *os.PathError if errors.As(err, &pathErr) { fmt.Printf("Specifically, it was a PathError for path: %s, operation: %s\n", pathErr.Path, pathErr.Op) } // 特定のセンチネルエラーがチェーン内にあるか確認(深くネストされていても) if errors.Is(err, os.ErrNotExist) { fmt.Println("The ultimate cause was that the file did not exist.") } } // 動的なエラーメッセージを示す別の例 userID := 123 dbErr := errors.New("database connection failed") err := fmt.Errorf("failed to fetch user %d data: %w", userID, dbErr) fmt.Println(err) }
fmt.Errorf
の使用時期:
-
動的なエラーメッセージ: エラーメッセージに、エラーのコンテキスト(無効な入力値、失敗したリソースIDなど)からの特定の含める必要がある場合。
fmt.Errorf("invalid age %d; must be positive", age)
-
エラーラッピング(
%w
): これは最も重要な差別化要因です。関数が依存関係(別の関数呼び出し、ライブラリ、OS)からエラーに遭遇した場合、追加のコンテキストを提供するために、通常はそのエラーをラップする必要があります。ラッパーを使用すると、元のエラーチェーンを保持でき、デバッグに非常に役立ちます。errors.Is(err, target)
:err
またはそのチェーン内のいずれかのエラーがtarget
であるかを確認します。センチネルエラーのチェックに最適です。errors.As(err, &target)
:err
をアンラップしてtarget
に割り当て可能なエラーを見つけます。特定のエラータイプ(*os.PathError
、*net.OpError
など)をチェックし、それらから情報を抽出するのに役立ちます。errors.Unwrap(err)
:err
によってラップされた根本的なエラーを返します。err
がエラーをラップしていない場合はnil
を返します。これは主に内部でerrors.Is
およびerrors.As
によって使用されます。
エラーラッピングの利点:
- 根本原因の維持: 最終的な障害につながったエラーの完全なシーケンスを追跡できます。
- コンテキスト情報: コールスタックの各レイヤーは、元の詳細を失うことなく、独自のコンテキスト情報を追加できます。
- プログラムによる検査: より高レベルのコードは、エラー文字列を解析することなく(
errors.Is
またはerrors.As
を使用して)特定の下位レベルのエラーをチェックし続けることができます。これにより、より堅牢で壊れにくいエラー処理ロジックが可能になります。
errors.New
とfmt.Errorf
の選択
簡単な決定木を以下に示します。
- この特定のエラー値を
errors.Is()
でチェックする必要がありますか?- はい:
errors.New
を使用してvar
を定義します(例:var ErrInvalidInput = errors.New("invalid input")
)。
- はい:
- エラーメッセージに動的な情報(特定の値、IDなど)を含める必要がありますか?
- はい:
fmt.Errorf
を使用します。
- はい:
- 下位レベルの関数または依存関係からエラーを伝播していますか?
- はい: 根本的なエラーをラップするために
%w
を使用してfmt.Errorf
を使用します。
- はい: 根本的なエラーをラップするために
- 既存のものをラップせず、メッセージが静的な、完全に新しいエラーを作成していますか?
- はい: 簡単のために
errors.New
を使用できますが、fmt.Errorf("static error message")
が習慣的によく使用されており、まったく問題ありません。後で動的なコンテンツを追加したり、ラップしたりする可能性がまったくない場合は、fmt.Errorf
の方が将来のリファクタリングにとって安全です。
- はい: 簡単のために
一般的なガイドライン:
ほとんどの場合、特にエラーがアプリケーションの複数のレイヤーを伝播する場合、fmt.Errorf
に傾倒します。コンテキストを追加し、根本的なエラーをラップする機能は、errors.New
に対するfmt.Errorf
のわずかなオーバーヘッドをはるかに超える強力なデバッグ補助です。errors.New
は、主にアプリケーションロジック内で明示的にチェックされるvar
センチネルエラーを定義するために予約してください。
エラーメッセージのベストプラクティス
- 小文字から始める: エラーメッセージは通常、大文字で始まらず、句読点で終わらないことが一般的です。これは、
fmt.Println
が改行やその他のフォーマットを追加するためです。- 良い:
return fmt.Errorf("failed to open file '%s'", filename)
- 悪い:
return errors.New("Failed to open file.")
- 良い:
- 簡潔かつ有益に: 何が起こったのかを理解するのに十分な情報を提供しますが、冗長すぎないようにします。
- 冗長性を避ける: エラーをラップする場合、外側のエラーメッセージは、内側のエラーを単に繰り返すのではなく、新しいコンテキストを追加するはずです。
- 良い:
fmt.Errorf("failed to process request for user %d: %w", userID, err)
- 悪い:
fmt.Errorf("error: %s", err)
(これはコンテキストを追加せずに根本的なエラーを繰り返すだけです)
- 良い:
- エラーは主に人間のためではなく、機械のためです: 人間が読めるものではありますが、エラーオブジェクトの主な消費者は、エラーに基づいて決定を下すスタックの上位にあるコードであることがよくあります。詳細なメッセージはデバッグ用です。ユーザー向けのメッセージは、内部エラーをよりフレンドリーなメッセージにマッピングすることによって、別個に生成されるべきです。
結論
errors.New
とfmt.Errorf
の両方Goのエラーハンドリングにおける不可欠なツールです。errors.New
は直接比較のためのシンプルで静的なセンチネルエラーの定義に優れていますが、fmt.Errorf
は、そのフォーマット機能、そしてより重要なことには、エラーラッピング(%w
)サポートにより、リッチでコンテキストがあり、デバッグ可能なエラーチェーンを作成するための主力です。これらの関数を適切に活用し、ベストプラクティスに従うことで、Go開発者はエラーハンドリングが堅牢であるだけでなく、デバッグや保守も大幅に容易になるアプリケーションを構築できます。意味のあるエラーを作成する微妙な技術は、機械が反応するのに十分な情報と、人間が問題の根本原因を理解するために十分な情報を提供することにあります。