3種類のエラー
Goのコードは、単純なものから豊かなものまで、3つの形のエラーを使います。
- その場限りのエラー:その場で作る
errors.New("...")やfmt.Errorf("...")。呼び出し側はメッセージを読むことしかできません。 - 番兵エラー:
io.EOFのようなパッケージレベルの変数。呼び出し側はerrors.Isでそれをテストできます。 - エラー型:フィールドを持ち、
Error()メソッドを持つ構造体。呼び出し側はerrors.Asでそれを取り出し、フィールドを読めます。
呼び出し側が必要なことをできる、最も単純なものを選びます。
番兵エラー
番兵は、一度だけ宣言され、同一性で比較されるエラーの値です。名前は慣習として Err で始めます。
出力:
mug bought
cap is out of stock, notify me later
buy "hat": not found
同じテキストで2回 errors.New を呼ぶと、別々のエラーになります。errors.New("x") == errors.New("x") は false です。番兵が1つの共有された変数でなければならないのはこのためです。
番兵はパッケージのAPIの一部になります。呼び出し側が ErrNotFound をチェックするようになったら、それを返すのをやめると呼び出し側が壊れます。呼び出し側が本当に分岐に使う必要があるものだけを公開しましょう。
独自のエラー型
呼び出し側が詳細(どのフィールドか、どのステータスコードか、どれだけ待ってリトライするか)を必要とするときは、型を定義します。
出力:
register: age: must be between 0 and 150
bad field: age
メソッドはポインタレシーバーを持ち、関数は &ValidationError{...} を返すので、errors.As の対象は *ValidationError で、そのアドレス(&ve、**ValidationError)を渡します。この段階を間違えるのが、errors.As の典型的な間違いです。値レシーバーなら ValidationError{...} を返し、var ve ValidationError と宣言します。go vet は最もよくあるうっかりミス、&ve の代わりに ve を渡すことを検出します:second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type。
%wによるラップ
%w を使った fmt.Errorf は、ラップしたエラーを覚えているエラーを返します。各層が文脈を加え、連鎖はそのまま保たれます。
出力:
start: connect db: timeout
*fmt.wrapError: start: connect db: timeout
*fmt.wrapError: connect db: timeout
*errors.errorString: timeout
true
false
%v ではメッセージは同じですが連鎖が切れるので、errors.Is は false を返します。呼び出し側が原因を見られるべきなら %w を、原因が依存してほしくない実装の詳細なら %v を選びます。
Go 1.20以降、1回の呼び出しで複数のエラーをラップできます:fmt.Errorf("%w; %w", err1, err2)。すると errors.Is はどちらにも一致します。
errors.Join:複数のエラーをまとめる
検証や後始末では、エラーが1つで済まないことがよくあります。errors.Join(Go 1.20)はそれらをまとめます。
出力:
name: required
email: required
age: negative
---
true
true
errors.Join はすべての引数がnilなら nil を返すので、上の関数には「エラーなし」のための特別な処理が要りません。
UnwrapとIsのメソッド
errors.Is と errors.As は、Unwrap メソッドを呼んでラップされたエラーを見つけます。原因を保持する独自の型は、それを公開するべきです。
複数のエラーをラップする型は、代わりに Unwrap() []error を実装します。
型は Is(target error) bool を定義して、等しさを自分で決めることもできます。たとえば、同じステータスコードを持つ任意の *HTTPError に一致させる場合です。必要になることはまれで、たいていは番兵をラップして Unwrap から返せば十分です。
型付きnilの罠
エラーの変数を具体的な型で宣言し、それを error として返してはいけません。
func check() error {
var err *ValidationError // nil pointer
// ... no problem found
return err // non-nil error! Its type is *ValidationError
}
型付きのnilポインタを保持するインターフェースはnilではないので、呼び出し側の if err != nil は真になります。成功時はリテラルの nil を返し、ローカルのエラー変数は error 型にしておきます。理由はインターフェースのページで説明しています。
どれを選ぶか
| 呼び出し側がしたいこと | 提供するもの |
|---|---|
| 失敗をログに出すか表示するだけ | fmt.Errorf("...: %w", err) |
| 特定の1つの状態で分岐する | 番兵 var ErrX = errors.New(...) |
| 失敗の詳細を読む | フィールドを持つエラー型 |
| 独立した複数の失敗を見る | errors.Join |
よくある間違い
- ラップしたエラーを
==で比較する。errors.Isを使います。 errors.Asにポインタでないものを渡す。 対象の型の変数へのポインタが必要です。- 関数の中で「番兵」を作る。
return errors.New("not found")は呼び出しのたびに新しい値を作るので、呼び出し側は比較できません。 - すべてのエラーを公開する。 公開した番兵や型は、どれもAPIとしての約束です。
err.Error()でマッチさせる。 文字列は人間のためのものです。
よくある質問
Goで独自のエラーを作るには?
決まった状態なら、パッケージレベルの番兵を宣言します:var ErrNotFound = errors.New("not found")。データを運ぶエラーなら、Error() string メソッドを持つ型を定義します:type ValidationError struct { Field string } と func (e *ValidationError) Error() string { return e.Field + " is invalid" }。
Goでエラーをラップするには?
fmt.Errorf を %w とともに使います:return fmt.Errorf("load user %d: %w", id, err)。新しいエラーのメッセージには古いものが含まれ、errors.Unwrap、errors.Is、errors.As で元のエラーにたどり着けます。Go 1.20以降、1つの Errorf 呼び出しで複数の %w を使って複数のエラーをラップできます。
errors.Isとerrors.Asの違いは何ですか?
errors.Is(err, target) は、io.EOF のような番兵について「この特定のエラー値が連鎖のどこかにあるか」に答えます。errors.As(err, &target) は「この型のエラーが連鎖の中にあるか」に答え、あればそれを target に格納するので、フィールドを読めます。
errors.Joinは何をしますか?
errors.Join(errs...)(Go 1.20)は複数のエラーを1つにまとめます。メッセージは個々のメッセージを改行で区切ったもので、nilのエラーは取り除かれ、すべてnilならnilを返します。まとめたエラーのどれかが一致すれば、errors.Is と errors.As は一致します。