Go 言語用エラーハンドリング・パッケージ
本パッケージは Go 言語によるプログラミングに於いて標準の errors
パッケージを補完し,構造化されたエラーハンドリングを行うことができる。
なお errs
パッケージは Go 1.13 以上を要求する。
ご注意を。
インポート
import "github.com/goark/errs"
簡単な使い方
error インスタンスの生成
まず errs
.New()
関数は標準の errors
.New()
を置き換え可能である。
err := errs.New("file open error")
その上で errs
.WithContext()
関数を使ってコンテキスト情報を付加することができる。
err := errs.New(
"file open error",
errs.WithContext("path", path),
)
errs
.WithContext()
関数は errs
.New()
関数の引数として複数セット可能で1,付加されたコンテキスト情報は map[string]interface{}
形式の連想配列に格納される。
更に errs
.WithCause()
関数を使って原因エラーを付加することもできる。
err := errs.New(
"file open error",
errs.WithContext("path", path),
errs.WithCause(cause),
)
errs
.WithCause()
関数も errs
.New()
関数の引数として複数セットできるが,最後にセットしたインスタンスのみが有効となる。
なお,複数の原因エラーがある場合は errors
.Join()
関数を使うといいだろう(Go 1.20 以降)。
err := errs.New(
"file open error",
errs.WithContext("path", path),
errs.WithCause(errors.Join(cause1, cause2)),
)
以上を踏まえて,ファイルをオープンするだけの関数を考えてみよう。 こんな感じ。
func checkFileOpen(path string) error {
file, err := os.Open(path)
if err != nil {
return errs.New(
"file open error",
errs.WithContext("path", path),
errs.WithCause(err),
)
}
defer file.Close()
return nil
}
この checkFileOpen()
関数の返り値を評価する。
最初は簡単に
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
fmt.Printf("%v\n", err)
}
}
とする。 これを実行すると
$ go run sample1a.go
file open error: open not-exist.txt: no such file or directory
と,生成したエラーのメッセージと原因エラーのメッセージが連結されて表示される。
ここで書式 %#v
を使ってエラー内容を表示してみる。
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
fmt.Printf("%#v\n", err)
}
}
これを実行すると
$ go run sample1b.go
*errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}}
と,エラーの内部構造を出力してくれる。
更に 書式 %+v
を使って
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
fmt.Printf("%+v\n", err)
}
}
とすると,実行結果は
$ go run sample1c.go | jq .
{
"Type": "*errs.Error",
"Err": {
"Type": "*errors.errorString",
"Msg": "file open error"
},
"Context": {
"function": "main.checkFileOpen",
"path": "not-exist.txt"
},
"Cause": {
"Type": "*fs.PathError",
"Msg": "open not-exist.txt: no such file or directory",
"Cause": {
"Type": "syscall.Errno",
"Msg": "no such file or directory"
}
}
}
と JSON 形式で出力される。
この出力を見ると分かるように errs
パッケージでは error
インスタンス生成時にエラーが発生した関数をコンテキスト情報として自動的に付加している。
書式を使い分けて上手く利用して欲しい。
error インスタンスのラッピング
errs
.Wrap()
関数を使って,他の関数・メソッドが出力した error
インスタンスにコンテキスト情報を付加することもできる。
先程の checkFileOpen()
関数であれば
func checkFileOpen(path string) error {
file, err := os.Open(path)
if err != nil {
return errs.Wrap(
err,
errs.WithContext("path", path),
)
}
defer file.Close()
return nil
}
のように書ける。
この checkFileOpen()
関数の返り値を書式 %v
で出力すると
$ go run sample2a.go
open not-exist.txt: no such file or directory
と,元の error
インスタンスと同じ結果になるが,書式 %+v
で出力すると
$ go run sample2c.go | jq .
{
"Type": "*errs.Error",
"Err": {
"Type": "*fs.PathError",
"Msg": "open not-exist.txt: no such file or directory",
"Cause": {
"Type": "syscall.Errno",
"Msg": "no such file or directory"
}
},
"Context": {
"function": "main.checkFileOpen",
"path": "not-exist.txt"
}
}
と,コンテキスト情報が付加されているのが分かる。
errs
.Wrap()
関数にも errs
.WithCause()
関数を使って原因エラーを付加することができる。
たとえば
var ErrCheckFileOpen = errors.New("file open error")
などと,あらかじめ error
インスタンス ErrCheckFileOpen
を定義しておいて
func checkFileOpen(path string) error {
file, err := os.Open(path)
if err != nil {
return errs.Wrap(
ErrCheckFileOpen,
errs.WithCause(err),
errs.WithContext("path", path),
)
}
defer file.Close()
return nil
}
と ErrCheckFileOpen
をラップし原因エラーやコンテキスト情報を付加することができる。
これを使えば
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
if errors.Is(err, ErrCheckFileOpen) {
fmt.Printf("%+v\n", err)
} else {
fmt.Printf("Other: %v\n", err)
}
return
}
}
と errors
.Is()
関数等を使って比較的簡単にエラーハンドリングを行うことができる。
その他のハンドリング関数(2023-02-07 更新)
標準の errors
.As()
, errors
.Is()
, errors
.Unwrap()
各関数の互換となる errs
.As()
, errs
.Is()
, errs
.Unwrap()
関数も用意した。
まぁ,内部で errors
の各関数を呼び出しているだけだけど。
でも,これで標準の errors
パッケージを errs
パッケージに置き換えて使うことができると思う。
さらに,複数の原因エラーを []error
型で返す errs
.Unwraps()
関数を用意した。
こんな感じに使える。
func main() {
err := errors.Join(os.ErrInvalid, io.EOF)
for _, e := range errs.Unwraps(err) {
fmt.Println(e)
}
//Output:
//invalid argument
//EOF
}
なお errs
.Unwraps()
関数は原因エラーがひとつの場合でも,要素数1の []error
型で返す。
func main() {
err := errs.Wrap(os.ErrInvalid)
for _, e := range errs.Unwraps(err) {
fmt.Println(e)
}
//Output:
//invalid argument
}
さらにさらに, errs
.EncodeJSON()
関数を使うと,通常の error
インスタンスでも可能な限り構造を辿って JSON 形式で出力する。
たとえば
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
var pathError *fs.PathError
if errs.As(err, &pathError) {
fmt.Printf("%v\n", errs.EncodeJSON(pathError))
} else {
fmt.Println(err)
}
return
}
}
のように書けば
$ go run sample/sample2d.go | jq .
{
"Type": "*fs.PathError",
"Msg": "open not-exist.txt: no such file or directory",
"Cause": {
"Type": "syscall.Errno",
"Msg": "no such file or directory"
}
}
などと出力される。
ブックマーク
- Go 1.13 のエラー・ハンドリング
- 書式 %v のカスタマイズ
- 構造化エラーをログ出力する
- Go のエラーハンドリング : Zenn 本書きました
参考図書
- プログラミング言語Go
- アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
- 丸善出版 2016-06-20 (Release 2021-07-13)
- Kindle版
- B099928SJD (ASIN)
- 評価
Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら。
-
可変引数に関数をセットするプログラミング・パターンは “Functional Option Pattern” と呼ばれている。 ↩︎