カスタムエラーについて理解が浅かったので改めて整理したいと思います。
基本形
仕様を確認
errorはinterfaceのため Error() string
を満たせばいいとあります。
type error interface {
Error() string
}
errosがまさにその実装を踏まえて作られております。
package errors
// errors.New()はその構造体を返す
func New(text string) error {
return &errorString{text}
}
// errorStringというError() stringを満たす構造体を作る
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
実装してみる
同じように作ってみました。 エラー情報として欲しい内容を構造体に定義したら良さそうですね。
type TestError struct {
first, second string
}
func (e TestError) Error() string {
return fmt.Sprintf("%s --- %s", e.first, e.second)
}
func NewErr(first, second string) error {
return &TestError{first, second}
}
func main() {
fmt.Println(last())
}
func last() error {
return NewErr("no.1", "no.2")
}
/*
$ go run main.go
no.1 --- no.2
*/
Wrapの仕組みを調べる
とりあえず試してみます。
func main() {
errA := errors.New("A error")
errB := fmt.Errorf("B error, %w", errA)
errC := fmt.Errorf("C error, %s", errA)
errD := fmt.Errorf("D error, %w", errB)
errE := fmt.Errorf("E error, %w", errC)
fmt.Printf("errB type: %T // string: %s // unwrap: %s\n", errB, errB, errors.Unwrap(errB))
fmt.Printf("errC type: %T // string: %s // unwrap: %s\n", errC, errC, errors.Unwrap(errC))
fmt.Printf("errD type: %T // string: %s // unwrap: %s\n", errD, errD, errors.Unwrap(errD))
fmt.Printf("errE type: %T // string: %s // unwrap: %s\n", errE, errE, errors.Unwrap(errE))
}
/*
errB type: *fmt.wrapError // string: B error, A error // unwrap: A error
errC type: *errors.errorString // string: C error, A error // unwrap: %!s(<nil>)
errD type: *fmt.wrapError // string: D error, B error, A error // unwrap: B error, A error
errE type: *fmt.wrapError // string: E error, C error, A error // unwrap: C error, A error
*/
fmt.Errorf(%w)
を利用することで同じようにエラーをスタックしているように見えますが、errors.Unwrap()
によって底にあるErrを取得できます
仕組みを見ると、Unwrap()
ではまずは、Unwrap()を持っているかアサーションをかけ、入っていたらu.Unwrap()
で取り出しています
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
上記の出力結果を見ると*errors.errorString
には Unwrap()
が最初に確認したように存在しなかったので nil
になるようですね
*fmt.wrapError
をみてみます
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
これを見ると、 wrapError{s, p.wrappedErr}
にて、wrapError.msg
に与えたerrを仕込み、Wrap対象を wrapError.err
に入れていることがわかります
Unwrapの存在を確認
errors.Is()は同じエラーかを確認できる
次の場合は errA
と同じインスタンスがWrapしたerrorに含まれているか確認できます。ただし同じインスタンスであることが条件なのでGlobal変数などで定義したものでないと使い回すのが難しそうです。
func main() {
errA := errors.New("A error")
errB := fmt.Errorf("B error, %w", errA)
errC := fmt.Errorf("C error, %s", errA)
errD := fmt.Errorf("D error, %w", errB)
errE := fmt.Errorf("E error, %w", errC)
fmt.Println(errors.Is(errB, errA)) // true
fmt.Println(errors.Is(errC, errA)) // false
fmt.Println(errors.Is(errD, errA)) // true
fmt.Println(errors.Is(errD, errB)) // true
fmt.Println(errors.Is(errE, errA)) // false
}
erros.As()は同じエラーの型かを確認できる
次のような場合はUnwrapした関数の中に MyError
が入っているかをみています
type MyError struct {
t time.Time
}
func (e MyError) Error() string {
return fmt.Sprintf("This is MyError: %t", e.t)
}
func main() {
errA := MyError{time.Now()}
errB := fmt.Errorf("B error, %w", errA)
errC := fmt.Errorf("C error, %s", errA)
errD := fmt.Errorf("D error, %w", errB)
errE := fmt.Errorf("E error, %w", errC)
fmt.Println(errors.As(errB, &MyError{})) // true
fmt.Println(errors.Is(errC, &MyError{})) // false
fmt.Println(errors.As(errD, &MyError{})) // true
fmt.Println(errors.Is(errD, &MyError{})) // false
fmt.Println(errors.Is(errE, &MyError{})) // false
}
まとめ
以上のことを踏まえると、次のような使い方かと理解しました。
- 深い階層にあるメソッドではWrapでerrを返す
- Handlerに渡すときにカスタムエラーに整理して渡す
- IsやAsなどでエラーハンドリングする