Goのエラーハンドリングをerrors.Isとerrors.Asのラップで強化する
Grace Collins
Solutions Engineer · Leapcell

Goのエラーを解き明かす:より良い診断のために
エラーハンドリングは、堅牢なソフトウェアを作成する上で不可欠な要素であり、Goは明示的なエラー返り値により、その点に強い重点を置いています。長らく、Go開発者は、エラーを検査・識別するために文字列マッチングや型アサーションに頼ることが多く、その結果、脆弱で保守が困難なコードにつながっていました。Go 1.13でのエラーラップの導入は、errors.Is
およびerrors.As
関数と相まって、この状況を一変させました。この最新のアプローチは、Goのシンプルさを犠牲にすることなく、より豊かなコンテキストとより正確な意思決定を可能にする、はるかに強力で慣用的なエラー処理方法を提供します。本記事では、エラーラップ、errors.Is
、errors.As
の現代的な使用法を深く掘り下げ、それらが開発者に、より回復力があり、観測可能なGoアプリケーションを構築することをどのように可能にするかを実証します。
エラーラップと検査の謎を解く
実践的な例に入る前に、最新のGoエラーハンドリングに関わるコアコンセプトを明確に理解しましょう。
-
エラーラップ: このメカニズムは、エラーが別のものである下位のエラーを含むことを可能にします。元のエラーにコンテキストのレイヤーを追加すると考えてください。これにより、中間的な詳細を公開しながら、根本原因までさかのぼってエラーの明確な系統を確立できます。Goでは、エラーラップは通常、
fmt.Errorf
で%w
動詞を使用して実現されます。 -
errors.Is
: この関数は、エラーチェーンを再帰的にアンラップし、チェーン内のどのエラーがターゲットエラーと「一致」するかをチェックします。これは、呼び出しスタックのどこから発生したかに関係なく、特定の種類のエラーが発生したかどうかを判断するために設計されています。これは、事前定義されたセンチネルエラーに対してチェックするのに特に役立ちます。 -
errors.As
:errors.Is
と同様に、errors.As
もエラーチェーンをアンラップします。ただし、等価性をチェックするのではなく、ターゲット型に割り当てることができるエラーをチェーン内で見つけようとします。一致が見つかった場合、下位のエラーがターゲット変数に割り当てられます。これは、チェーン内のカスタムエラー型から特定の情報や動作を抽出したい場合に非常に役立ちます。
これらのコンセプトをコード例で説明しましょう。
基本的なエラーラップ
ファイル操作が失敗するシナリオを考えてみましょう。元のOSエラーをより多くのコンテキストでラップできます。
package main import ( "errors" "fmt" "os" ) func readFile(filename string) ([]byte, error) { data, err := os.ReadFile(filename) if err != nil { // 元のエラーを追加のコンテキストでラップする return nil, fmt.Errorf("failed to read file '%s': %w", filename, err) } return data, nil } func main() { _, err := readFile("nonexistent.txt") if err != nil { fmt.Println("Error:", err) // 出力: Error: failed to read file 'nonexistent.txt': open nonexistent.txt: no such file or directory } }
この例では、fmt.Errorf("failed to read file '%s': %w", filename, err)
がos.ReadFile
のエラーをラップしています。%w
動詞は、fmt.Errorf
にerr
がerrors.Unwrap
(ひいてはerrors.Is
およびerrors.As
)によって取得可能であることを伝えます。
センチネルエラーのためのerrors.Is
の使用
特定の条件を示すためにセンチネルエラーを定義することはよくあります。errors.Is
は、これらの条件をチェックすることを容易にします。
package main import ( "errors" "fmt" "os" ) var ErrRecordNotFound = errors.New("record not found") func getUser(id int) (string, error) { if id < 1 { return "", fmt.Errorf("invalid user ID: %d: %w", id, ErrRecordNotFound) } if id == 123 { return "John Doe", nil } return "", fmt.Errorf("user with ID %d not found: %w", id, ErrRecordNotFound) } func main() { _, err := getUser(0) if errors.Is(err, ErrRecordNotFound) { fmt.Println("User not found or invalid ID:", err) // 出力: User not found or invalid ID: invalid user ID: 0: record not found } _, err = getUser(456) if errors.Is(err, ErrRecordNotFound) { fmt.Println("User not found or invalid ID:", err) // 出力: User not found or invalid ID: user with ID 456 not found: record not found } _, err = getUser(123) if err != nil { fmt.Println("This should not happen:", err) } }
ここでは、getUser
がさまざまなシナリオでErrRecordNotFound
をラップしています。main
では、返されるエラー文字列が異なる場合でも、errors.Is(err, ErrRecordNotFound)
が根本原因がErrRecordNotFound
である場合を正しく識別します。
カスタムエラー型のためのerrors.As
の活用
カスタムエラー型から特定のデータやメソッドの動作を抽出する必要がある場合は、errors.As
が使用するツールです。
package main import ( "errors" "fmt" "time" ) // PermissionErrorは、不足している権限に関する詳細を含むカスタムエラー型です。 type PermissionError struct { User string Action string Missing string When time.Time } func (e *PermissionError) Error() string { return fmt.Sprintf("user %s cannot %s, missing permission '%s' at %v", e.User, e.Action, e.Missing, e.When) } func checkPermission(user, action string) error { if user == "guest" && action == "delete" { return &PermissionError{ User: user, Action: action, Missing: "delete_access", When: time.Now(), } } return nil } func performAction(user, action string) error { err := checkPermission(user, action) if err != nil { // アクション試行に関する追加コンテキストとともに権限エラーをラップする return fmt.Errorf("failed to perform action '%s' for user '%s': %w", action, user, err) } fmt.Printf("User %s successfully performed action %s.\n", user, action) return nil } func main() { err := performAction("guest", "delete") if err != nil { fmt.Println("Encountered error:", err) var pErr *PermissionError if errors.As(err, &pErr) { fmt.Printf("A permission error occurred! User: %s, Action: %s, Missing Permission: %s\n", pErr.User, pErr.Action, pErr.Missing) // 出力: User: guest, Action: delete, Missing Permission: delete_access } } err = performAction("admin", "delete") if err != nil { fmt.Println("This should not happen:", err) } // 出力: User admin successfully performed action delete. }
この例では、performAction
がPermissionError
をラップしています。main
では、errors.As(err, &pErr)
がラップされたエラーチェーンから*PermissionError
を正常に抽出し、そのフィールド(User
、Action
、Missing
)にアクセスできるようにします。これにより、文字列解析に依存するのではなく、特定の誤った条件を処理し、プログラムでそれらに応答するための堅牢な方法が提供されます。
いつラップし、いつラップしないか
エラーラップは、元の原因を保持しながら、エラーにコンテキスト情報を追加する必要がある場合に輝きます。これはデバッグとロギングに不可欠です。ただし、万能薬ではありません。エラーをラップしない場合の例を次に示します。
- エラーが一時的または再試行可能である場合: 呼び出しコードが再試行すべきかどうかを知るだけでよい場合、単純な、ラップされていないエラーまたはブール値の返り値で十分な場合があります。
- エラーが内部的であり、伝播されるべきではない場合: エラーが内部実装の詳細であり、それを上位レイヤーに公開するとカプセル化が破れる場合は、より汎用的な外部エラーに変換するか、単にログに記録して定義済みのエラーを返すことを検討してください。
- アプリケーションの最上位レベルにある場合: 最上位では、エラーチェーン全体をログに記録して終了するか、汎用的なユーザーフレンドリーなメッセージを返すだけかもしれません。
一般的な原則は、追加のコンテキストの形で価値を加えるときにエラーをラップし、ドゥンストリームコンシューマーが特定のエラー条件に反応する必要がある場合に検査可能にすることです。
統一されたエラー戦略の力
適切なエラーラップとerrors.Is
およびerrors.As
を中心とする最新のGoエラーハンドリングアプローチは、より保守性が高く回復力のあるコードベースを促進します。それは、脆弱な文字列比較や型アサーションから、エラーの識別とそれへの応答のための構造化された意味論的な方法へと移行します。明確なエラー系統を提供し、正確な検査を可能にすることで、これらのツールは、複雑なアプリケーションでの問題の診断可能性を大幅に向上させ、最終的にはより堅牢で信頼性の高いソフトウェアにつながります。
要約すると、errors.Is
およびerrors.As
によるGoのエラーラップは、エラーコンテキストを豊かにし、エラーチェーンを正確に検査するための強力で慣用的な方法を提供します。これにより、より堅牢で、保守性が高く、デバッグしやすいGoアプリケーションが実現します。