Help us understand the problem. What is going on with this article?

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

関連情報

sonatard
組み込みC言語ネットワークスタック開発者からGoバックエンドエンジニアにジョブチェンジしました。 最近はTypeScript, React(Hooks), GraphQL, SwiftUIに夢中。
https://github.com/sonatard/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした