先日 xerrors パッケージがリリースされました。
このパッケージは、Proposal: Go 2 Error Inspection で提案されているものをGo1向けに外部ライブラリとして試験的に実装したものです。
Goの標準ライブラリではありませんが、Go公式がメンテナンスをしています。
このパッケージができた背景は、今まで多くのGoエンジニアは下位層のエラーの情報を伝播させるために pkg/errors パッケージ などの外部ライブラリを利用していました。この手法が開発者の間で普及したため標準ライブラリで正式に検討を始めることとなりました。
2019/9/4更新
Go 1.13では %w
でのラップや Is
メソッド、 As
メソッドは正式に導入されました。
しかし%+w
や %+v
によるスタックトレースの表示の採用は見送られました。
スタックトレースの表示が必要な場合はxerrors パッケージを利用して、不要な場合には標準ライブラリの errors パッケージを利用してください。
この記事ではxerrorsパッケージの仕様を紹介します。
2022/6/26更新
xerrors packageに更新があり、Is、As、UnwrapはDeprecatedになりました。今後はerrors packageの同名の関数を利用する必要があります。また fmt.Errorf
がスタックトレースを表示できず xerrors.Errorf
の代わりにはならないため xerrors.Errorf
はDeprecatedになっていません。
基本
以下のパッケージをimportします。
import "golang.org/x/xerrors"
フォーマットとラップ
文字列からのエラーを作成する
err := xerrors.New("error in main method")
fmt.Printf("%v\n", err)
error in main method
xerrors.Newで作成したエラーは、%+v
のときにファイル名やメソッド名を表示します。
err := xerrors.New("error in main method")
fmt.Printf("%+v\n", err)
error in main method:
main.main
/Users/sonatard/tmp/xerrors/main.go:9
%#v
では以下のようになります。
err := xerrors.New("error in main method")
fmt.Printf("%#v\n", err)
error in main method
既存のエラーから新規のエラーを作成する
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method %v",baseErr)
fmt.Printf("%+v\n", err)
error in main method base error:
main.main
/Users/sonatard/tmp/xerrors/main.go:10
このやり方だと、baseErrの行数の情報が失われてしまっています。
以下のように %v
の前にコロンとスペースを加えて : %v
とすることで既存のerrorの情報を出力することができます。
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %v", baseErr)
fmt.Printf("%+v\n", err)
error in main method:
main.main
/Users/sonatard/tmp/xerrors/main.go:11
- base error:
main.main
/Users/sonatard/tmp/xerrors/main.go:10
%v
だけではなく %s
でも問題ありません。
エラーをラップする
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %w", baseErr)
fmt.Printf("%+v\n", err)
error in main method:
main.main
/Users/sonatard/tmp/xerrors/main.go:11
- base error:
main.main
/Users/sonatard/tmp/xerrors/main.go:10
: %w
でラップできますが、 %w
では正しくラップできません。
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method %w", baseErr)
fmt.Printf("%+v\n", err)
error in main method %!w(*xerrors.errorString):
main.main
/Users/sonatard/tmp/xerrors/main.go:11
: %w
の理由を知りたい方はこちらをご覧ください。
xerrorsパッケージがWrapメソッドではなく : %w でラップする理由
エラーをアンラップする
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %w", baseErr)
fmt.Printf("%+v\n", xerrors.Unwrap(err))
base error:
main.main
/Users/sonatard/tmp/xerrors/main.go:10
アンラップできるエラーは : %w
でラップしたものだけであり、: %v
や : %s
ではラップされていないためアンラップできません。
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %v", baseErr)
fmt.Printf("%+v\n", xerrors.Unwrap(err))
<nil>
エラーの同一性をチェックする
通常
baseErr := xerrors.New("base error")
fmt.Printf("%v\n", xerrors.Is(baseErr, baseErr))
fmt.Printf("%v\n", baseErr == baseErr)
true
true
ラップした場合
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %w", baseErr)
fmt.Printf("%v\n", xerrors.Is(err, baseErr))
fmt.Printf("%v\n", err == baseErr)
true
false
Is
メソッドを使うことで、errの中のラップされたbaseErrが同一と判断されます。
複数回ラップした場合
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %w", baseErr)
err2 := xerrors.Errorf("error2 in main method: %w", err)
fmt.Printf("%v\n", xerrors.Is(err, baseErr))
fmt.Printf("%v\n", xerrors.Is(err2, baseErr))
fmt.Printf("%v\n", xerrors.Is(err2, err))
true
true
true
すべて true
となります。
Opaqueメソッド実行後の同一性
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %w", baseErr)
err2 := xerrors.Opaque(err)
fmt.Printf("%v\n", xerrors.Is(err, baseErr))
fmt.Printf("%v\n", xerrors.Is(err2, baseErr))
fmt.Printf("%v\n", err)
fmt.Printf("%v\n", err2)
fmt.Printf("%v\n", xerrors.Unwrap(err2))
true
false
error in main method: base error
error in main method: base error
<nil>
Opaque
メソッドを実行すると、同じエラーフォマットの別のエラーが返ってきます。このエラーはIsメソッドで比較すると false
になりますが出力結果は同じになります。また Unwrap
を実行することはできません。
途中のエラーにOpaqueメソッドを適用したエラーの同一性
baseErr := xerrors.New("base error")
err := xerrors.Errorf("error in main method: %w", baseErr)
err2 := xerrors.Errorf("error2 in main method: %w", xerrors.Opaque(err))
err3 := xerrors.Errorf("error3 in main method: %w", err2)
fmt.Printf("%v\n", xerrors.Is(err2, baseErr))
fmt.Printf("%v\n", xerrors.Is(err2, err))
fmt.Printf("%v\n", xerrors.Is(err3, err2))
false
false
true
errにOpaqueを適用してerr2にラップしているため、err2とerrを含んだラップは同一ではないと判断されます。
その後追加でラップしているerr3とerr2は同一と判断されます。
エラーの型変換
以降の説明で登場する独自に定義した型です。
type BaseError struct {
msg string
}
func (e *BaseError) Error() string {
return e.msg
}
通常
var baseErr error = &BaseError{msg: "base error"}
var baseErr2 *BaseError
if ok := xerrors.As(baseErr, &baseErr2); !ok {
fmt.Printf("As failed\n")
}
fmt.Printf("%v\n", baseErr2 == baseErr)
true
As
メソッドの第2引数に渡した型に変換しています。
ラップした型
var baseErr error = &BaseError{msg: "base error"}
err := xerrors.Errorf("error in main method: %w", baseErr)
var baseErr2 *BaseError
if ok := xerrors.As(err, &baseErr2); !ok {
fmt.Printf("As failed\n")
}
fmt.Printf("%v\n", baseErr2 == baseErr)
true
先ほどと同様ですが、ラップした型は一致しないため更に下位層のbaseErrに変換されます。
Asのターゲットの型がerror chainに存在しない場合
var baseErr error = &BaseError{msg: "base error"}
err := xerrors.Errorf("error in main method: %w", baseErr)
var baseErr2 *ABaseError
if ok := xerrors.As(err, &baseErr2); !ok {
fmt.Printf("As failed\n")
}
fmt.Printf("%v\n", baseErr2 == baseErr)
As failed
false
失敗した場合は ok
が false
になります。
サンプル
最後にもう少し実践的なサンプルコードを紹介します。
package main
import (
"fmt"
"golang.org/x/xerrors"
)
var ErrNotFound = &SampleError{
statusCode: 404,
level: "Error",
msg: "not found",
}
type SampleError struct {
level string
statusCode int
msg string
}
func (e *SampleError) Error() string {
return fmt.Sprintf("%s: code=%d, msg=%s", e.level, e.statusCode, e.msg)
}
func main() {
err := func1()
if err != nil {
var sampleErr *SampleError
if xerrors.As(err, &sampleErr) {
switch sampleErr.level {
case "Fatal":
fmt.Printf("Fatal! %v\n", sampleErr)
case "Error":
fmt.Printf("Error! %v\n", sampleErr)
case "Warning":
fmt.Printf("Warning! %v\n", sampleErr)
}
}
fmt.Printf("%+v\n", err)
return
}
fmt.Printf("エラーなし\n")
}
func func1() error {
err := func2()
if err != nil {
return xerrors.Errorf("func1 error: %w", err)
}
return nil
}
func func2() error {
err := func3()
if err != nil {
return xerrors.Errorf("func2 error: %w", err)
}
return nil
}
func func3() error {
return ErrNotFound
}
Error! Error: code=404, msg=not found
func1 error:
main.func1
/Users/sonatard/tmp/xerrors/main.go:45
- func2 error:
main.func2
/Users/sonatard/tmp/xerrors/main.go:53
- Error: code=404, msg=not found
余裕があればこちらも理解することで、より適切な設計ができます。
xerrors - エラー設計の注意点
xerrors パッケージ - 独自に定義したエラー型はIsメソッドとAsメソッドでデフォルトの振る舞いを変更可能
エラーを検査する