NOTE: この記事は Inspecting Errors を翻訳したものです。
原文に従い、 Creative Commons ライセンスで公開します。
error
インターフェイス型の値を返す関数の一般的な契約は、呼び出し側はその error
をチェックする前にそれ以外のいかなる戻り値も利用してはいけないというものです。
多くのケースにおいて、関数が返した error 値は呼び出し元にとって不透明であるべきです。 error が nil かどうかチェックすることで呼び出しが成功したかどうかを知ることができ、そしてそれ以上のことはしません。
少ないケースにおいて、主にネットワークなどプロセス外の世界とやりとりする場合に、呼び出し側がエラーの種別を調べて、操作をリトライするべきかどうかを決める必要があります。
パッケージ作者に対して、呼び出し側がエラーの型を判別して利用できるように返すエラー型を公開して欲しいという要求がよくなされます。しかし、私はこの方法が幾つもの不幸な結果に繋がると信じています:
- 公開されたエラー型はパッケージのAPIの表面積を大きくする
- 実装を新しくした時、インターフェイスを定義したエラー型がもう実装にフィットしていないとしても、その型の値しか返せなくなる
- パッケージを公開した後は、互換性を壊さずにエラー型を変更したり廃止することができない。これは脆いAPIにつながります
呼び出し側は、 error
が特定の型かどうか検査することを、 Error()
メソッドが返す文字列が特定のパターンにマッチするかどうか検査するより便利だと思うべきではありません。
代わりに、パッケージの作者と利用者が、実装と呼び出し側を密結合することなく関心事についてコミュニケーションする方法を提案します。
error を型ではなく振る舞いで検査する
error の値が特定の型であるかどうかを検査する代わりに、その値が特定の振る舞いを実装しているかどうかを検査してください。
この方法は、継承ベースの言語の is a [subtype of] よりも、 Go の暗黙のインターフェイスの has a 文化にうまくフィットします:
func isTimeout(err error) bool {
type timeout interface {
Timeout() bool
}
if te, ok := err.(timeout); ok {
return te.Timeout()
}
return false
}
呼び出し側はこの isTimeout()
を使って、 error が timeout
インターフェイスを持っているか判別し、持っているならそのメソッドに問い合わせることで、その error がタイムアウトであるかどうかを判別することができます。
この方法なら実際の型やその error 値の出自について一切知る必要がありません。
(よくライブラリがエラーの発生したパスを注釈するために行う) エラーのラッピングもこの方法で可能です。
ラップされるエラーが実装しているインターフェイスを、ラップする側にも実装します。
これは非現実的な方法に思えるかもしれません。しかし、実際には一般的に使われるインターフェイスメソッドは比較的少なくて、 Timeout() bool
と Temporary() bool
だけで多くのユースケースをカバーできます。
結論
error を型ではなく振る舞いで検査してください。
パッケージの作者は、もし一時的なエラーを返すのであれば、それに応じたメソッドを返すエラーの型に実装してください。もしエラー値をラップして返すのであれば、ラップするエラー値が実装しているインターフェイスをラッパーにも実装してください。
パッケージのユーザーは、エラーを検査する必要がある場合、エラーの型ではなく、エラーが期待する振る舞いをもっているかをインターフェイスを使って検査してください。パッケージの作者にパブリックなエラー型を追加するように依頼しないでください。かわりに Timeout() や Temporary() メソッドを適切に実装して一般的なインターフェイスに従うように依頼してください。