階層化 Error パッケージ “xerrors” を試してみる
【2020-11-19 追記】
golang.org/x/xerrors の機能は errors
パッケージおよび fmt
パッケージの Errorf()
関数にほぼ取り込まれている。
今後は errors
を使うことを強くお勧めする。
新しい error パッケージ golang.org/x/xerrors がリリースされたそうな。
これはいわゆる “Go 2 Draft” の “Error Inspection” を実装したもので,なんと Go 1.13 以降で既存の標準 errors
パッケージに組み込む計画があるらしい。
これは朗報! というわけで早速試してみることにした。
Error インスタンスの生成
さっそく簡単なコードを書いてみよう。
package main
import (
"fmt"
"os"
"golang.org/x/xerrors"
)
func foo() error {
return xerrors.New("an error instance")
}
func main() {
if err := foo(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}
これを実行すると
$ go run demo1/demo1.go
an error instance
となる。
ここまでは普通。
ここで fmt.Fprintf()
関数のフォーマット文字列を以下のように書き換えてみる。
package main
import (
"fmt"
"os"
"golang.org/x/xerrors"
)
func foo() error {
return xerrors.New("an error instance")
}
func main() {
if err := foo(); err != nil {
fmt.Fprintf(os.Stderr, "%+v\n", err)
}
}
これ実行すると
$ go run demo1/demo1.go
an error instance:
main.foo
/tmp/xerrors/demo1/demo1.go:11
となり error の発生箇所が表示されるようになった。
ちなみに xerrors
.New()
関数で生成される error インスタンスの構造は以下の通り。
// errorString is a trivial implementation of error.
type errorString struct {
s string
frame Frame
}
// New returns an error that formats as the given text.
//
// The returned error contains a Frame set to the caller's location and
// implements Formatter to show this information when printed with details.
func New(text string) error {
return &errorString{text, Caller(1)}
}
frame
フィールドに xerrors
.New()
関数の呼び出し情報が格納されているのが分かると思う。
これでデバッグ作業がかなり楽になるだろう。
Error の階層化
たとえば以下のようなファイルをオープンするだけの簡単なコードを書いてみる。
package main
import (
"fmt"
"os"
)
func fileOpen(fname string) error {
file, err := os.Open(fname)
if err != nil {
switch e := err.(type) {
case *os.PathError:
return fmt.Errorf("Error in fileOpen(\"%v\"): %v", e.Path, e.Err)
default:
return fmt.Errorf("Error in fileOpen(): %v", err)
}
}
defer file.Close()
return nil
}
func main() {
fmt.Print("Result: ")
if err := fileOpen("null.txt"); err != nil {
fmt.rintf("%+v\n", err)
} else {
fmt.Println("OK")
}
}
このとき null.txt
が存在しないなら実行結果は
$ go run demo2a/demo2a.go
Result: Error in fileOpen("null.txt"): The system cannot find the file specified.
となる。
パッと見は問題なさそうだが fileOpen()
関数が error を返す際に os
.Open()
関数が吐き出した error インスタンスが捨てられてしまうため,エラーの追跡が難しくなる。
そこで xerrors
.Errorf()
関数を使って error のラッピングを行う。
コードはこんな感じ。
package main
import (
"fmt"
"os"
"golang.org/x/xerrors"
)
func fileOpen(fname string) error {
file, err := os.Open(fname)
if err != nil {
switch e := err.(type) {
case * os.PathError:
return xerrors.Errorf("Error in fileOpen(\"%v\"): %w", e.Path, e.Err)
default:
return xerrors.Errorf("Error in fileOpen(): %w", err)
}
}
defer file.Close()
return nil
}
func main() {
fmt.Print("Result: ")
if err := fileOpen("null.txt"); err != nil {
fmt.Printf("%+v\n", err)
} else {
fmt.Println("OK")
}
}
少し解説すると xerrors
.Errorf()
関数の第1引数のフォーマット文字列の末尾が ": %w"
になっていて,かつ対応する値が error インタフェースを備えていれば xerrors
.wrapError
型のインスタンスを返す。
ただの "%w"
では xerrors
.wrapError
型を返さない点に注意1。
このコードを実行すると以下のような結果になる。
$ go run demo2b/demo2b.go
Result: Error in fileOpen("null.txt"):
main.fileOpen
/tmp/xerrors/demo2b/demo2b.go:15
- The system cannot find the file specified.
error が連結され階層構造になっているのが分かると思う。
error を階層化するためには xerrors
.Wrapper
インタフェースを実装する必要がある。
xerrors
.Wrapper
インタフェースの定義は以下の通り。
// A Wrapper provides context around another error.
type Wrapper interface {
// Unwrap returns the next error in the error chain.
// If there is no next error, Unwrap returns nil.
Unwrap() error
}
UML で描くとこんな感じかな。
ちなみに xerrors
.wrapError
型では以下のような実装になっている。
type wrapError struct {
msg string
err error
frame Frame
}
func (e *wrapError) Error() string {
return fmt.Sprint(e)
}
func (e *wrapError) Unwrap() error {
return e.err
}
Error の等値性
上述した xerrors
.Wrapper
インタフェースがなんの役に立つかというと error インスタンスの等値性(equality)をチェックするのに役立つのだ。
error インスタンスの等値性を調べるには xerrors
.Is()
関数を使う。
xerrors
.Is()
関数の中身はこんな感じ。
// Is reports whether any error in err's chain matches target.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
func Is(err, target error) bool {
if target == nil {
return err == target
}
for {
if err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporing target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
return false
}
}
}
err
に対して連結されている error インスタンスを遡っていき target
と等値2 なインスタンスを探している。
連結されている error チェインの中にひとつでも等値な error インスタンスがあるなら対象のインスタンスは等値であるとみなすわけだ。
先程のファイルをオープンするコードを xerrors
.Is()
関数を使って少し書き直してみよう。
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/xerrors"
)
func fileOpen(fname string) error {
file, err := os.Open(fname)
if err != nil {
switch e := err.(type) {
case * os.PathError:
return xerrors.Errorf("Error in fileOpen(\"%v\"): %w", e.Path, e.Err)
default:
return xerrors.Errorf("Error in fileOpen(): %w", err)
}
}
defer file.Close()
return nil
}
func main() {
fmt.Print("Result: ")
if err := fileOpen("null.txt"); err != nil {
if xerrors.Is(err, syscall.ENOENT) {
fmt.Println("ファイルが存在しない。")
} else {
fmt.Println("その他のエラー:", err)
}
} else {
fmt.Println("OK")
}
}
これの実行結果は以下の通り。
$ go run demo3/demo3.go
Result: ファイルが存在しない。
階層化された Error を検索する
xerrors
.As()
関数を使うと階層化された Error の中から指定した型の error インスタンスを抽出できる。
これも xerrors
.Wrapper
インタフェースが実装されていることが前提となる。
// As finds the first error in err's chain that matches the type to which target
// points, and if so, sets the target to its value and returns true. An error
// matches a type if it is assignable to the target type, or if it has a method
// As(interface{}) bool such that As(target) returns true. As will panic if target
// is not a non-nil pointer to a type which implements error or is of interface type.
//
// The As method should set the target to its value and return true if err
// matches the type to which target points.
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for {
if reflect.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflect.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
if err = Unwrap(err); err == nil {
return false
}
}
}
var errorType = reflect.TypeOf((*error)(nil)).Elem()
“Error Inspection” では総称型(Generics)を前提とした実装が提案されていたが,まだ総称型は実現されていないので,ちょっとアレな感じがするのは致し方ない(笑)
xerrors
.As()
関数を使ってコードを書き直してみよう。
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/xerrors"
)
func fileOpen(fname string) error {
file, err := os.Open(fname)
if err != nil {
switch e := err.(type) {
case * os.PathError:
return xerrors.Errorf("Error in fileOpen(\"%v\"): %w", e.Path, e.Err)
default:
return xerrors.Errorf("Error in fileOpen(): %w", err)
}
}
defer file.Close()
return nil
}
func main() {
fmt.Print("Result: ")
if err := fileOpen("null.txt"); err != nil {
var errno syscall.Errno
if xerrors.As(err, &errno) {
switch errno {
case syscall.ENOENT:
fmt.Println("ファイルが存在しない。")
default:
fmt.Println("Errno =", errno)
}
} else {
fmt.Println("その他のエラー:", err)
}
} else {
fmt.Println("OK")
}
}
これの実行結果は以下の通り。
$ go run demo3/demo3.go
Result: ファイルが存在しない。
独自の階層化 error 型を作成する
ここまで分かったので,次は xerrors
互換の階層化 error 型を自作してみよう。
具体的には os
.PathError
をカスタマイズした型を書いてみる。
type CustomPathError struct {
Op string
Path string
Err error
frame xerrors.Frame
}
func (e *CustomPathError) Error() string {
return "error in " + e.Op + " \"" + e.Path + "\""
}
func (e *CustomPathError) Unwrap() error {
return e.Err
}
func (e *CustomPathError) Format(s fmt.State, v rune) {
xerrors.FormatError(e, s, v)
}
func (e *CustomPathError) FormatError(p xerrors.Printer) error {
p.Print(e.Error())
e.frame.Format(p)
return e.Err
}
CustomPathError
が os
.PathError
の階層化 error 版で,これに Error()
, Unwrap()
, Format()
, FormatError()
の各関数が実装されている。
Format()
および FormatError()
関数は xerrors
.Formatter
および fmt
.Formatter
インタフェースの実装で fmt
.Printf()
などの関数で呼び出される。
fmt
.Formatter
は以下のように定義されている。
// Formatter is the interface implemented by values with a custom formatter.
// The implementation of Format may call Sprint(f) or Fprint(f) etc.
// to generate its output.
type Formatter interface {
Format(f State, c rune)
}
また xerrors
.Formatter
は以下のように定義されている。
// A Formatter formats error messages.
type Formatter interface {
error
// FormatError prints the receiver's first error and returns the next error in
// the error chain, if any.
FormatError(p Printer) (next error)
}
CustomPathError
を UML で描くとこんな感じ。
この CustomPathError
を使って os
.Open()
関数の返り値の error をラップする。
func OpenWrapper(name string) (*os.File, error) {
file, err := os.Open(name)
if err != nil {
switch e := err.(type) {
case *os.PathError:
return file, &CustomPathError{Op: e.Op, Path: e.Path, Err: e.Err, frame: xerrors.Caller(0)}
default:
return file, &CustomPathError{Op: "open", Path: name, Err: err, frame: xerrors.Caller(0)}
}
}
return file, nil
}
さらに OpenWrapper()
を使って先程のファイルをオープンするコードを書き直す。
func fileOpen(fname string) error {
file, err := OpenWrapper(fname)
if err != nil {
return xerrors.Errorf("Error in fileOpen(): %w", err)
}
defer file.Close()
return nil
}
func main() {
fmt.Print("Result: ")
if err := fileOpen("null.txt"); err != nil {
fmt.Printf("%v\n", err)
fmt.Printf("%+v\n", err)
if xerrors.Is(err, syscall.ENOENT) {
fmt.Println("ファイルが存在しない。")
} else {
fmt.Println("その他のエラー:", err)
}
} else {
fmt.Println("OK")
}
}
これを実行してみよう。
$ go run demo5/demo5.go
Result: Error in fileOpen(): error in open "null.txt": The system cannot find the file specified.
Error in fileOpen():
main.fileOpen
/tmp/xerrors/demo5/demo5.go:52
- error in open "null.txt":
main.OpenWrapper
/tmp/xerrors/demo5/demo5.go:41
- The system cannot find the file specified.
ファイルが存在しない。
ちゃんと error が連結されていることが分かると思う。
はっきり言って Format()
と FormatError()
の両関数は golang.org/x/xerrors のソースコードからパクっているが,細かいチューニングが必要でないのなら,このまま snippet として使い回せるんじゃないだろうか3。
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。
-
なんでこんなヘンテコな仕様になっているかというと Go 1.13 で
xerrors
.Errorf()
関数をfmt
.Errorf()
関数に統合する予定があるからだそうだ。なおxerrors
.Errorf()
関数では error インスタンスをひとつしかラッピングできない。複数の error インスタンスをまとめてラッピングしたいなら独自の型(クラス)を定義する必要があるだろう。それでも独自型をフルスクラッチで組むよりは簡単だろうけど。 ↩︎ -
Go 言語では
==
は等値演算子(equality operator)だが error インスタンスはポインタ値で表すことが多く,その場合は==
演算子もポインタ値を比較することになり,実質的に error インスタンスの同一性(identity)を調べていることになる。 Go 言語の interface 型は変数がインスタンスそのものかインスタンスへの参照(ポインタ値)かを隠蔽してしまうため,等値か同一かを問題にする際には注意が必要である。 ↩︎ -
ちなみに golang.org/x/xerrors は BSD ライセンス下で利用できる。 ↩︎