Go Wiki: エラー値:よくある質問
Go 2のエラー値提案は、Go 1.13の標準ライブラリのerrors
パッケージとfmt
パッケージに機能を追加します。また、以前のバージョンのGo向けの互換性パッケージgolang.org/x/xerrors
もあります。
下位互換性のためにxerrors
パッケージを使用することをお勧めします。Go 1.13より前のバージョンをサポートしなくなった場合は、対応する標準ライブラリ関数を使用してください。このFAQでは、Go 1.13のerrors
とfmt
パッケージを使用しています。
新しい機能に対応するために、エラー処理コードをどのように変更する必要がありますか?
取得するエラーがラップされている可能性があることを覚悟する必要があります。
-
現在
==
を使用してエラーを比較している場合は、代わりにerrors.Is
を使用してください。例if err == io.ErrUnexpectedEOF
は
if errors.Is(err, io.ErrUnexpectedEOF)
- になります。
if err != nil
形式のチェックは変更する必要はありません。 io.EOF
との比較は変更する必要はありません。io.EOF
はラップされるべきではないためです。
- になります。
-
型アサーションまたは型スイッチを使用してエラー型をチェックする場合は、代わりに
errors.As
を使用してください。例if e, ok := err.(*os.PathError); ok
は
var e *os.PathError if errors.As(err, &e)
- また、このパターンを使用して、エラーがインターフェースを実装しているかどうかを確認します。(これは、インターフェースへのポインタが適切であるというまれなケースの1つです。)
- 型スイッチをif-elseのシーケンスとして書き直します。
すでに%v
または%s
を使用してfmt.Errorf
でエラーのコンテキストを提供しています。いつ%w
に切り替えるべきですか?
次のようなコードがよく見られます。
if err := frob(thing); err != nil {
return fmt.Errorf("while frobbing: %v", err)
}
新しいエラー機能を使用しても、そのコードは以前とまったく同じように動作し、err
のテキストを含む文字列を構築します。%v
から%w
に変更してもその文字列は変わりませんが、err
をラップするため、呼び出し元はerrors.Unwrap
、errors.Is
、またはerrors.As
を使用してアクセスできます。
そのため、呼び出し元に基礎となるエラーを公開したい場合は%w
を使用してください。そうすることで、コードの進化を制限する可能性のある実装の詳細を公開する可能性があることに注意してください。呼び出し元は、ラップしているエラーの型と値に依存できるため、そのエラーを変更すると、呼び出し元が壊れる可能性があります。たとえば、パッケージpkg
のAccessDatabase
関数がGoのdatabase/sql
パッケージを使用している場合、sql.ErrTxDone
エラーが発生する可能性があります。fmt.Errorf("accessing DB: %v", err)
でそのエラーを返す場合、呼び出し元はsql.ErrTxtDone
が返されたエラーの一部であることがわかりません。しかし、代わりにfmt.Errorf("accessing DB: %w", err)
を返す場合、呼び出し元は合理的に次のように記述できます。
err := pkg.AccessDatabase(...)
if errors.Is(err, sql.ErrTxDone) ...
その時点で、別のデータベースパッケージに切り替えたとしても、クライアントを壊したくない場合は、常にsql.ErrTxDone
を返す必要があります。
クライアントを壊すことなく、すでに返しているエラーにコンテキストを追加するにはどうすればよいですか?
現在のコードが次のようになっているとします。
return err
そして、返す前にerr
により多くの情報を追加することにしたとします。次のように記述すると
return fmt.Errorf("more info: %v", err)
クライアントが壊れる可能性があります。err
の同一性が失われ、メッセージだけが残るためです。
代わりに、%w
を使用してエラーをラップし、次のように記述できます。
return fmt.Errorf("more info: %w", err)
これでも、==
または型アサーションを使用してエラーをテストするクライアントは壊れます。しかし、このFAQの最初の質問で説明したように、エラーのコンシューマーはerrors.Is
とerrors.As
関数に移行する必要があります。クライアントがそうしたと確信できる場合は、次のように切り替えることは破壊的な変更ではありません。
return err
から
return fmt.Errorf("more info: %w", err)
クライアントのない新しいコードを書いています。返されたエラーをラップする必要がありますか?
クライアントがないため、下位互換性によって制約されることはありません。しかし、それでも2つの相反する考慮事項をバランスさせる必要があります。
- クライアントコードに基礎となるエラーへのアクセスを提供すると、クライアントコードが決定を下せるようになり、より良いソフトウェアにつながる可能性があります。
- 公開するすべてのエラーはAPIの一部になります。クライアントはそれに依存するようになる可能性があるため、変更できません。
返す各エラーについて、クライアントにとって重要な機能と実装の詳細のどちらを重視するかを検討する必要があります。もちろん、この選択はエラーに固有のものではありません。パッケージの作成者として、コードの機能がクライアントにとって知る必要があるかどうか、実装の詳細かどうかについて多くの決定を下します。
ただし、エラーの場合、中間的な選択肢があります。クライアントコードにエラー自体を公開せずに、コードのエラーメッセージを読む人にエラーの詳細を公開できます。その1つの方法は、%s
または%v
を使用してfmt.Errorf
で詳細を文字列に入れることです。もう1つの方法は、カスタムエラー型を作成し、そのError
メソッドによって返される文字列に詳細を追加し、Unwrap
メソッドを定義しないことです。
エラーチェック述語関数をエクスポートするパッケージを保守しています。新しい機能にどのように適応する必要がありますか?
パッケージには、エラーに何らかのプロパティがあるかどうかを報告する関数またはメソッドIsX(error) bool
があります。当然のことながら、渡されたエラーをアンラップし、ラップされたエラーのチェーンの各エラーのプロパティをチェックするようにIsX
を変更することを考えます。これはお勧めしません。動作の変更により、ユーザーが壊れる可能性があります。
あなたの状況は、そのような関数をいくつか持つ標準のos
パッケージのようになっています。そこで行ったアプローチをお勧めします。os
パッケージにはいくつかの述語がありますが、ほとんどを同じように扱いました。具体的には、os.IsExist
を見てみましょう。
os.IsExist
を変更する代わりに、errors.Is(err, os.ErrExist)
がアンラップする点を除いて同じように動作するようにしました。(これは、errors.Is
のドキュメントで説明されているように、syscall.Errno
にIs
メソッドを実装させることによって行いました。)errors.Is
を使用すると、Goバージョン1.13以降でのみ存在するため、常に正しく動作します。古いバージョンのGoの場合、基礎となる各エラーに対してos.IsExist
を呼び出し、自分でエラーを再帰的にアンラップする必要があります。
このテクニックは、ラップされているエラーを制御し、それらにIs
メソッドを追加できる場合にのみ機能します。その場合、次のことをお勧めします。
IsX(error) bool
関数を変更しないでください。アンラップしないことを明確にするために、ドキュメントを変更してください。- まだない場合は、関数がテストする条件を表す、
error
を実装する型のグローバル変数を追加します。var ErrX = errors.New("has property X")
IsX
がtrueを返す型にIs
メソッドを追加します。Is
メソッドは、引数がErrX
と等しい場合にtrueを返す必要があります。
プロパティXを持つ可能性のあるすべてのエラーを制御できない場合は、アンラップしながらプロパティをテストする別の関数を追加することを検討してください。たとえば、
func IsXUnwrap(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if IsX(e) {
return true
}
}
return false
}
または、現状のままにして、ユーザーが自分でアンラップできるようにすることもできます。どちらの場合も、IsX
のドキュメントを変更して、アンラップしないことを明確にする必要があります。
error
を実装し、ネストされたエラーを保持する型を持っています。新しい機能にどのように適応する必要がありますか?
型がすでにエラーを公開している場合は、Unwrap
メソッドを記述します。
たとえば、型が次のようになっているとします。
type MyError struct {
Err error
// other fields
}
func (e *MyError) Error() string { return ... }
次に、次を追加する必要があります。
func (e *MyError) Unwrap() error { return e.Err }
これで、型はerrors
とxerrors
のIs
関数とAs
関数で正しく動作します。
これは、標準ライブラリのos.PathError
やその他の同様の型に対して行われています。
ネストされたエラーがエクスポートされている場合、またはUnwrap
のようなメソッドを介してパッケージ外のコードに表示されている場合は、Unwrap
メソッドを記述するのが正しい選択であることは明らかです。しかし、ネストされたエラーが外部コードに公開されていない場合は、おそらくそのようにしておく必要があります。Unwrap
から返すことによってエラーを可視化すると、クライアントがネストされたエラーの型に依存できるようになり、実装の詳細を公開し、パッケージの進化を制限する可能性があります。詳細は上記の%w
の議論を参照してください。
このコンテンツはGo Wikiの一部です。