Go Wiki: Error Values: よくある質問

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

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

新しい機能に対応するために、エラー処理コードをどのように変更すればよいですか?

受け取るエラーがラップされている可能性があることを考慮する必要があります。

  • 現在、==を使用してエラーを比較している場合は、代わりに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のシーケンスとして書き換えてください。

すでにfmt.Errorf%vまたは%sと一緒に使用してエラーのコンテキストを提供しています。いつ%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.Isおよびerrors.As関数に移行すべきです。クライアントがそうしていることを確信できる場合、次のように変更しても互換性を損ねる変更ではありません。

return err

から

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

クライアントのない新しいコードを書いています。返されるエラーをラップすべきですか、すべきではありませんか?

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

  • クライアントコードに基になるエラーへのアクセスを提供することで、クライアントコードが意思決定を行うのに役立ち、より良いソフトウェアにつながる可能性があります。
  • 公開するすべてのエラーはAPIの一部になります。クライアントはそれに依存するようになる可能性があるため、変更することはできません。

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

ただし、エラーの場合、中間的な選択肢があります。コードのエラーメッセージを読む人にエラーの詳細を公開する一方で、クライアントコードにはエラー自体を公開しないという方法です。これを行う1つの方法は、fmt.Errorf%sまたは%vと一緒に使用して文字列に詳細を入れることです。もう1つの方法は、カスタムエラー型を作成し、そのErrorメソッドが返す文字列に詳細を追加し、Unwrapメソッドを定義しないことです。

エラーチェック述語関数をエクスポートするパッケージを管理しています。新しい機能にどのように適応すればよいですか?

あなたのパッケージには、エラーが何らかのプロパティを持っているかどうかを報告する関数またはメソッドIsX(error) boolがあります。自然な考えとしては、渡されたエラーをアンラップし、ラップされたエラーのチェーン内の各エラーのプロパティをチェックするようにIsXを変更することでしょう。しかし、これはお勧めしません。動作の変更によってユーザーが壊れる可能性があります。

あなたの状況は、いくつかのそのような関数を持つ標準のosパッケージの状況に似ています。私たちはそこで採用したアプローチをお勧めします。osパッケージにはいくつかの述語がありますが、そのほとんどを同じように扱いました。具体的には、os.IsExistを見てみましょう。

os.IsExistを変更する代わりに、errors.Is(err, os.ErrExist)Isがアンラップすることを除いて、それと同じように動作するようにしました。(これは、errors.Isのドキュメントに記載されているように、syscall.ErrnoIsメソッドを実装させることによって行いました。)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およびxerrorsIs関数およびAs関数と正しく動作するようになります。

私たちは、標準ライブラリのos.PathErrorや他の同様の型に対してこれを実施しました。

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


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