Go Wiki: エラー値:よくある質問

Go 2のエラー値提案は、Go 1.13の標準ライブラリのerrorsパッケージとfmtパッケージに機能を追加します。また、以前のバージョンのGo向けの互換性パッケージgolang.org/x/xerrorsもあります。

下位互換性のためにxerrorsパッケージを使用することをお勧めします。Go 1.13より前のバージョンをサポートしなくなった場合は、対応する標準ライブラリ関数を使用してください。このFAQでは、Go 1.13のerrorsfmtパッケージを使用しています。

新しい機能に対応するために、エラー処理コードをどのように変更する必要がありますか?

取得するエラーがラップされている可能性があることを覚悟する必要があります。

すでに%vまたは%sを使用してfmt.Errorfでエラーのコンテキストを提供しています。いつ%wに切り替えるべきですか?

次のようなコードがよく見られます。

if err := frob(thing); err != nil {
    return fmt.Errorf("while frobbing: %v", err)
}

新しいエラー機能を使用しても、そのコードは以前とまったく同じように動作し、errのテキストを含む文字列を構築します。%vから%wに変更してもその文字列は変わりませんが、errをラップするため、呼び出し元はerrors.Unwraperrors.Is、またはerrors.Asを使用してアクセスできます。

そのため、呼び出し元に基礎となるエラーを公開したい場合は%wを使用してください。そうすることで、コードの進化を制限する可能性のある実装の詳細を公開する可能性があることに注意してください。呼び出し元は、ラップしているエラーの型と値に依存できるため、そのエラーを変更すると、呼び出し元が壊れる可能性があります。たとえば、パッケージpkgAccessDatabase関数が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.Iserrors.As関数に移行する必要があります。クライアントがそうしたと確信できる場合は、次のように切り替えることは破壊的な変更ではありません。

return err

から

return fmt.Errorf("more info: %w", err)

クライアントのない新しいコードを書いています。返されたエラーをラップする必要がありますか?

クライアントがないため、下位互換性によって制約されることはありません。しかし、それでも2つの相反する考慮事項をバランスさせる必要があります。

返す各エラーについて、クライアントにとって重要な機能と実装の詳細のどちらを重視するかを検討する必要があります。もちろん、この選択はエラーに固有のものではありません。パッケージの作成者として、コードの機能がクライアントにとって知る必要があるかどうか、実装の詳細かどうかについて多くの決定を下します。

ただし、エラーの場合、中間的な選択肢があります。クライアントコードにエラー自体を公開せずに、コードのエラーメッセージを読む人にエラーの詳細を公開できます。その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.ErrnoIsメソッドを実装させることによって行いました。)errors.Isを使用すると、Goバージョン1.13以降でのみ存在するため、常に正しく動作します。古いバージョンのGoの場合、基礎となる各エラーに対してos.IsExistを呼び出し、自分でエラーを再帰的にアンラップする必要があります。

このテクニックは、ラップされているエラーを制御し、それらにIsメソッドを追加できる場合にのみ機能します。その場合、次のことをお勧めします。

プロパティ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 }

これで、型はerrorsxerrorsIs関数とAs関数で正しく動作します。

これは、標準ライブラリのos.PathErrorやその他の同様の型に対して行われています。

ネストされたエラーがエクスポートされている場合、またはUnwrapのようなメソッドを介してパッケージ外のコードに表示されている場合は、Unwrapメソッドを記述するのが正しい選択であることは明らかです。しかし、ネストされたエラーが外部コードに公開されていない場合は、おそらくそのようにしておく必要があります。Unwrapから返すことによってエラーを可視化すると、クライアントがネストされたエラーの型に依存できるようになり、実装の詳細を公開し、パッケージの進化を制限する可能性があります。詳細は上記の%wの議論を参照してください。


このコンテンツはGo Wikiの一部です。