Go エラーハンドリング応用
基礎的な error インターフェース(Interface)と if err != nil をマスターすれば、Go言語における日常的なエラー処理の80%はカバーできたと言えます。
しかし、ビジネスロジックが複雑化するにつれ、単に "division by zero" のようなシンプルな文字列(String)を返すだけでは不十分な場面が増えてきます。「エラー発生時の具体的なパラメータは何だったのか?」「このエラーは下位レイヤー(Low-level)のネットワークタイムアウトによるものか、あるいはデータベース接続の切断によるものか?」といった情報を把握する必要があります。
これらの課題を解決するために、本章ではカスタムエラー型(Custom Error Types)の作成、および Go 1.13 で導入されたエラーラッピング(Error Wrapping)メカニズムについて深く掘り下げていきます。
1. カスタムエラー型 (Custom Error Types) の構築
組み込みの errors.New() は、シンプルなテキスト情報しか保持できません。より構造化されたデータが必要な場合は、構造体(Struct)を定義し、それに Error() string メソッド(Method)を実装させることで、独自のカスタムエラーを作成できます。
1.1 カスタムエラー構造体の定義
ここでは、何のエラーが起きたかだけでなく、エラーの原因となった具体的な入力値も記録できる MathError を定義してみましょう。
package main
import (
"fmt"
"math"
)
// 1. コンテキスト情報を保持する構造体を定義
type MathError struct {
Message string
Input float64
}
// 2. 構造体に Error() string メソッドを実装 (ポインタレシーバを使用)
func (e *MathError) Error() string {
return fmt.Sprintf("数学演算エラー: %s, 異常な入力値: %f", e.Message, e.Input)
}
// 3. ビジネスロジック関数でカスタムエラーを使用
func squareRoot(x float64) (float64, error) {
if x < 0 {
// カスタムエラーオブジェクトのポインタを返す
return 0, &MathError{Message: "負の数の平方根は計算できません", Input: x}
}
return math.Sqrt(x), nil
}1.2 カスタムエラー情報の抽出 (型アサーション)
呼び出し側が error インターフェースを受け取った際、内部の Input フィールドなどを抽出するには、型アサーション(Type Assertion)を使用して具体的な *MathError 型に復元する必要があります。
func main() {
result, err := squareRoot(-4)
if err != nil {
// 通常の error を具体的な *MathError に型アサーション
mathErr, ok := err.(*MathError)
if ok {
fmt.Printf("詳細な MathError をキャッチしました!内容: %s, 原因となった値: %f\n", mathErr.Message, mathErr.Input)
} else {
fmt.Println("一般的なエラーが発生しました:", err)
}
return
}
fmt.Println("計算結果:", result)
}2. エラーラッピングとエラーチェーン
多階層で構成されるシステムでは、下位ファンクションから返されたエラーに対し、上位レイヤーでコンテキスト情報を付加したい場合があります(例:「設定ファイルの読み込み失敗」 <- 「ファイルのオープン失敗」 <- 「権限不足」)。
Go 1.13 より前はエラーを文字列として結合するしかなく、元のエラー型が失われていました。現在は fmt.Errorf と %w 動詞(Verb)を使用することで、マトリョーシカのようにエラーを「ラッピング(包装)」できるようになっています。
2.1 %w によるエラーのラッピング
package main
import (
"errors"
"fmt"
)
// 下位レイヤーのセンチネルエラー (Sentinel Error) を定義
var ErrNegativeValue = errors.New("ボトムレイヤーエラー: 負の数が入力されました")
func innerFunction(value int) error {
if value < 0 {
return ErrNegativeValue // 下位エラーを返す
}
return nil
}
func outerFunction(value int) error {
err := innerFunction(value)
if err != nil {
// %w を使用して内部の err をラッピングし、コンテキストを追加
return fmt.Errorf("外部呼び出しに失敗: %w", err)
}
return nil
}
func main() {
err := outerFunction(-5)
if err != nil {
fmt.Println(err) // 出力: 外部呼び出しに失敗: ボトムレイヤーエラー: 負の数が入力されました
}
}3. エラーチェーンの検証とアンラップ
エラーがラッピングされた後、その内部に特定のターゲットエラーが含まれているかどうかを確認するにはどうすればよいでしょうか。標準ライブラリの errors パッケージには、強力なツールが用意されています。
3.1 コアツールの比較表
| 関数 | 主な役割 | ユースケース |
|---|---|---|
errors.Is(err, target) | エラーチェーン内に特定のターゲットエラーが含まれるか判定する。 | センチネルエラー(定義済みのエラー変数)との比較。 |
errors.Unwrap(err) | 一番外側のラッピングを剥がし、次レイヤーのエラーを返す。 | エラーチェーンを逐次解析する(ラッピングされていない場合は nil)。 |
エンジニア向けのヒント: エラーを比較する際、ターゲットとなるエラーはあらかじめ定義された変数(ErrNegativeValue など)である必要があります。errors.Is(err, errors.New("...")) といった比較は避けてください。errors.New は呼び出すたびに異なるポインタ(Pointer)を生成するため、比較結果が常に false になってしまいます。
3.2 エラーチェーン追跡の実践
先ほどのラッピングコードに基づき、main ファンクションで追跡を行ってみましょう。
func main() {
err := outerFunction(-5)
if err != nil {
// 1. errors.Is を使用してラッピングを貫通し、根本原因をマッチング
if errors.Is(err, ErrNegativeValue) {
fmt.Println("追跡確認:エラーチェーンに '負の数入力' という根本原因が含まれています。")
}
// 2. errors.Unwrap で手動で一層剥がす
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
fmt.Println("外装を剥がした後の次レイヤーのエラー:", unwrappedErr)
}
}
}4. エラー処理における高度なベストプラクティス
- エラーを絶対に無視しない: 空白識別子 _ を使ってエラーを捨てるのは避けてください。処理されないエラーは、予期せぬ場所でアプリケーションをクラッシュさせる原因となります。
- コンテキストを追加し、原因を保護する: エラーをインターセプトして上位に返す際は、単にそのまま返すのではなく
fmt.Errorf("...: %w", err)でラッピングしましょう。これにより、トラブルシューティング時のログ解析(Logging)が非常にスムーズになります。 - 発生源で処理する: エラーは、それが発生した場所に最も近く、十分なコンテキストを持っているレイヤーで処理(あるいはラッピングして上位へ投げる)されるべきです。
- センチネルエラー (Sentinel Errors) の活用: 頻出するエラー(例:
var ErrNotFound = errors.New("not found"))はパッケージレベルのグローバル変数として定義し、呼び出し側がerrors.Isで精密にマッチングできるように設計しましょう。