エラー・ハンドリング再考
前回は golang.org/x/xerrors パッケージを紹介かたがた簡単なコードをお試しで書いてみたのだが,大雑把に言って golang.org/x/xerrors パッケージの特徴は以下のようなものだと言えるだろう。
- エラーの発生位置を取得・表示できる
- エラーの等値性(equality)を検証できる
- エラー・チェイン内の指定した型のインスタンスを抽出できる
であるならエラー・ハンドリングの設計もこれらの特徴を活かしたいものである。
errors 標準パッケージからの置き換え
たとえば以下のような
func foo() error {
...
}
func bar() error {
...
}
関数 foo() と bar() から返却される error が等値であれば処理をまとめられる筈である。
今までは errors.New() 関数を使って
var (
ErrInstance1 = errors.New("error instance 1")
ErrInstance2 = errors.New("error instance 2")
ErrInstance3 = errors.New("error instance 3")
)
のように,あらかじめ必要な error インスタンスを作成しておき
func errorHandling(err error) {
switch err {
case ErrInstance1:
fmt.Fprintln(os.Stderr, "Error number 1 was triggered.")
return
case ErrInstance2:
fmt.Fprintln(os.Stderr, "Error number 2 was triggered.")
return
case ErrInstance3:
fmt.Fprintln(os.Stderr, "Error number 3 was triggered.")
return
default:
fmt.Fprintln(os.Stderr, "Unknown error")
return
}
}
みたいな感じにインスタンごとに処理を書いていけばよかった。
しかし errors を xerrors に置き換えた場合,このままでは上手くいかないことが分かる。
実際にコードを書いて確かめてみよう。
errorHandling() 関数ではエラーの発生位置も出力するようにしてみる。
package main
import (
"fmt"
"os"
"golang.org/x/xerrors"
)
var (
ErrInstance1 = xerrors.New("error instance 1")
ErrInstance2 = xerrors.New("error instance 2")
ErrInstance3 = xerrors.New("error instance 3")
)
func errorHandling(err error) {
switch err {
case ErrInstance1:
fmt.Fprintf(os.Stderr, "Error number 1 was triggered.\n%+v\n", err)
return
case ErrInstance2:
fmt.Fprintf(os.Stderr, "Error number 2 was triggered.\n%+v\n", err)
return
case ErrInstance3:
fmt.Fprintf(os.Stderr, "Error number 3 was triggered.\n%+v\n", err)
return
default:
fmt.Fprintf(os.Stderr, "Unknown error.\n%+v\n", err)
return
}
}
func foo() error {
return ErrInstance1
}
func bar() error {
return ErrInstance1
}
func main() {
err1 := foo()
err2 := bar()
fmt.Println("foo() error == bar() error ?", err1 == err2)
errorHandling(err1)
errorHandling(err2)
}
これを実行すると以下のようになる。
$ go run handling1/handling1.go
foo() error == bar() error ? true
Error number 1 was triggered.
error instance 1:
main.init
/tmp/xerrors/handling1/handling1.go:11
Error number 1 was triggered.
error instance 1:
main.init
/tmp/xerrors/handling1/handling1.go:11
これは xerrors.New() 関数を起動した行がエラー発生位置になるためで,これでは本当のエラー発生位置が分からない。
それじゃあ,というので
func foo() error {
return xerrors.New("error instance 1")
}
func bar() error {
return xerrors.New("error instance 1")
}
と書き換えても駄目である。
xerrors.New() 関数はインスタンスへのポインタ値を返すが foo() や bar() 関数で返される error と ErrInstance1 は異なるインスタンスなので Go 言語の等値演算子(equality operator)では(ポインタ値を比較するだけで)等値であることを示せない。
したがって実行結果も
$ go run handling1b/handling1b.go
foo() error == bar() error ? false
Unknown error.
error instance 1:
main.foo
/tmp/xerrors/handling1b/handling1b.go:34
Unknown error.
error instance 1:
main.bar
/tmp/xerrors/handling1b/handling1b.go:37
と “Unknown error” になる。
もちろん Error() 関数で出力される文字列を比較するなんてのは論外である。
Is 関数をカスタマイズする
上述の問題を解決する方法は色々あると思うが,今回はエラーメッセージのメンテンスのしやすさも考慮して,以下の方法を紹介してみる。
まず基本となる error インスタンスを以下のように定義する。
package werror
type Num int
const (
ErrInstance1 Num = iota + 1
ErrInstance2
ErrInstance3
)
var errMessage = map[Num]string{
ErrInstance1: "error instance 1",
ErrInstance2: "error instance 2",
ErrInstance3: "error instance 3",
}
func (n Num) Error() string {
if s, ok := errMessage[n]; ok {
return s
}
return "unknown error"
}
インスタンスの比較を簡単にするために数値型の Num を定義し,値と値に紐づくエラーメッセージを定義する。
次に Num 型を埋め込む形で xerrors 互換の wrapError 型を定義する。
type wrapError struct {
Num
frame xerrors.Frame
}
func New(n Num) error {
return &wrapError{Num: n, frame: xerrors.Caller(1)}
}
func (we *wrapError) Format(s fmt.State, v rune) {
xerrors.FormatError(we, s, v)
}
func (we *wrapError) FormatError(p xerrors.Printer) error {
p.Print(we.Error())
we.frame.Format(p)
return nil
}
Format() および FormatError() 各関数については前回を参照のこと。
これで
return werror.New(werror.ErrInstance1)
などとすれば定義された error 値を使って werror.New() 関数の起動位置で error インスタンスを生成することが出来る。
ただし,このままでは xerrors.Is() 関数によるインスタンスの等値性を正しく検証できない。
そこで Num および wrapError に以下の関数を追加する。
func (n Num) Is(target error) bool {
var t1 *wrapError
if xerrors.As(target, &t1) {
return n == t1.Num
}
var t2 Num
if xerrors.As(target, &t2) {
return n == t2
}
return false
}
func (we *wrapError) Is(target error) bool {
var t1 *wrapError
if xerrors.As(target, &t1) {
return we.Num == t1.Num
}
var t2 Num
if xerrors.As(target, &t2) {
return we.Num == t2
}
return false
}
これで等値演算子 == の代わりに Num.Is() および wrapError.Is() 関数がインスタンスの等値性をチェックしてくれる。
この werror パッケージのクラス図はこんな感じかな。

この werror パッケージを使って最初のコードを書き換えてみよう。
package main
import (
"fmt"
"os"
"demo-xerrors/handling2/werror"
"golang.org/x/xerrors"
)
func errorHandling(err error) {
switch true {
case xerrors.Is(err, werror.ErrInstance1):
fmt.Fprintf(os.Stderr, "Error number 1 was triggered.\n%+v\n", err)
return
case xerrors.Is(err, werror.ErrInstance2):
fmt.Fprintf(os.Stderr, "Error number 2 was triggered.\n%+v\n", err)
return
case xerrors.Is(err, werror.ErrInstance3):
fmt.Fprintf(os.Stderr, "Error number 3 was triggered.\n%+v\n", err)
return
default:
fmt.Fprintf(os.Stderr, "Unknown error.\n%+v\n", err)
return
}
}
func foo() error {
return werror.New(werror.ErrInstance1)
}
func bar() error {
return werror.New(werror.ErrInstance1)
}
func main() {
err1 := foo()
err2 := bar()
fmt.Println("foo() error == bar() error ?", xerrors.Is(err1, err2))
errorHandling(err1)
errorHandling(err2)
}
これを実行すると以下のような結果になる。
$ go run handling2/handling2.go
foo() error == bar() error ? true
Error number 1 was triggered.
error instance 1:
main.foo
/tmp/xerrors/handling2/handling2.go:30
Error number 1 was triggered.
error instance 1:
main.bar
/tmp/xerrors/handling2/handling2.go:33
よーし,うむうむ,よーし。
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!
