Go 言語用エラーハンドリング・パッケージ

no extension

本パッケージは Go 言語によるプログラミングに於いて標準の errors パッケージを補完し,構造化されたエラーハンドリングを行うことができる。

check vulns lint status GitHub license GitHub release

なお 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 更新)

注意: v1.2.1errs.Cause() 関数を Deprecated とした。 マルチエラーに対応できないため。

標準の 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"
  }
}

などと出力される。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. 可変引数に関数をセットするプログラミング・パターンは “Functional Option Pattern” と呼ばれている。 ↩︎