はじめに
こんにちは。最近業務でGoを書いているハセガワカンタです。
先日pkg/errorsのCause()メソッドで少しハマったので書き残しておきます。
errors.Cause()の注意点
errors.Cause()はエラーの連鎖を遡って根本的な原因を探る関数ですが、すべてのエラーを辿ることができるわけではありません。エラーがcauserインターフェースを満たしていない場合、errors.Cause()の再帰的な処理が中断されます。この場合、errors.Cause()だけでは最後までエラーを辿ることができません。
// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
// type causer interface {
// Cause() error
// }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}
実際のケース
url.Errorからcontext.Canceledを取り出したかった
実際にハマったのは、 net/httpパッケージのClient.Doメソッドを使用したときです。Doメソッドがcontext canceledされた場合、Doメソッドはurl.Errorを返し、その内部のErrフィールドにcontext.Canceledエラーを格納します。問題はurl.Errorがcauserインターフェースを実装しておらず、errors.Cause()ではcontext.Canceledを取り出すことができない点です。
開発していたプロジェクトではSentryで通知するエラーを判別するためにerrors.Cause()を使用していましたが、この方法ではcontext.Canceledを特定できず、適切な処理ができませんでした。
switch errors.Cause(err).(type) {
case context.Canceled:
// Sentry通知に関する処理
...
}
対処法
今回行った対処としては、Unwrap()を使用して内部エラーを取り出し、errors.Wrap()で再度ラップする方法を取りました。これにより、errors.Cause()を使用すると最後のエラーがcontext.Canceledになります。
var urlErr *url.Error
if errors.As(err, &urlErr) {
return nil, errors.Wrap(urlErr.Unwrap(), urlErr.Error())
}
ただラップし直すことで実際のエラー階層構造と異なってしまうデメリットがあります。
cockroachdb/errors
ちなみに今回はpkg/errorsの話をしていましたが、cockroachdb/errorsではUnwrapとCauseの両方に対応したメソッドがあるのでそちらを使えるのであればベストです。
// UnwrapOnce accesses the direct cause of the error if any, otherwise
// returns nil.
//
// It supports both errors implementing causer (`Cause()` method, from
// github.com/pkg/errors) and `Wrapper` (`Unwrap()` method, from the
// Go 2 error proposal).
//
// UnwrapOnce treats multi-errors (those implementing the
// `Unwrap() []error` interface as leaf-nodes since they cannot
// reasonably be iterated through to a single cause. These errors
// are typically constructed as a result of `fmt.Errorf` which results
// in a `wrapErrors` instance that contains an interpolated error
// string along with a list of causes.
//
// The go stdlib does not define output on `Unwrap()` for a multi-cause
// error, so we default to nil here.
func UnwrapOnce(err error) (cause error) {
switch e := err.(type) {
case interface{ Cause() error }:
return e.Cause()
case interface{ Unwrap() error }:
return e.Unwrap()
}
return nil
}
// UnwrapAll accesses the root cause object of the error.
// If the error has no cause (leaf error), it is returned directly.
// UnwrapAll treats multi-errors as leaf nodes.
func UnwrapAll(err error) error {
for {
if cause := UnwrapOnce(err); cause != nil {
err = cause
continue
}
break
}
return err
}