Edited at

Goの新しいerrors パッケージ xerrors

先日 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パッケージの仕様を紹介します。


基本

以下のパッケージを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

失敗した場合は okfalse になります。


サンプル

最後にもう少し実践的なサンプルコードを紹介します。

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メソッドでデフォルトの振る舞いを変更可能

エラーを検査する


関連情報