295
156

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-02-15

先日 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

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

関連情報

295
156
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
295
156

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?