アプリ実装者のためのGo1.13からのエラーとの付き合い方


# はじめに

[Go1.13から機能追加されたエラー](https://golang.org/doc/go1.13#error_wrapping)に関してキャッチアップがまだできていないアプリケーション開発者のための記事です。

# TL;DR

アプリケーション実装者は以下をすることで嬉しさを得られます。

* `fmt.Errorf("%w", err)` または `Unwrap() error`を実装したエラー型をカスタムエラーにする。
* `errors.Is`関数、`errors.As`関数を使いエラーハンドリングの受け口を実装する。

Go1.12までのバージョンとの実装方法の比較は[Go 1.13時代のエラー実装者のお作法](https://qiita.com/shibukawa/items/e633e426a6e67ea2e830)の記事が非常に参考になります。

### 環境

```
$ go version
go version go1.13.1 darwin/amd64
```

# ライブラリが提供するエラーとの付き合い方

## ライブラリが返すエラー値を`fmt.Errorf("%w", err)`でラップすれば`errors.Is`関数で中身のライブラリ側エラー値を判定できる

以下のコードをあるGoのライブラリが提供するものだとします。

```go:lib.go
package main // 本来は package lib な形式

import (
	"errors"
	"time"
)

var (
	ErrParse = errors.New("lib: parse error")
)

type LibTime time.Time

func ParseLibFunc(text string) (LibTime, error) {
	t, err := time.Parse(time.RFC3339, text)
	if err != nil {
		return LibTime{}, ErrParse // ライブラリが持つエラー型の値を返す
	}
	return LibTime(t), nil
}
```

このライブラリの関数`ParseLibFunc`を利用するアプリケーションを作っているとします。

ここでは`service`というアプリの関数がライブラリ関数を呼び、ライブラリ関数から返ったエラーをGo1.13からの`fmt.Errorf("%w", err)`の記法でラップしながらエラーを呼び元である`main`関数に返します。ここでは`main`関数はアプリコードのエラーハンドリングを担当する関数とします。

```go:main.go
package main

import (
	"errors"
	"fmt"

	// "XXX/User/lib" // 本来はこのようにライブラリをインポートする
)

func service(text string) error {
	_, err := ParseLibFunc(text)
	if err != nil {
		return fmt.Errorf("service error with '%w'", err) // Unwrapを実装したエラーを返す
	}
	return nil
}

func main() {
	if err := service("2020/02/14"); err != nil {
		// errors.Is 関数を使ってライブラリ提供のエラーかを判定
		if errors.Is(err, ErrParse) {
			// handle error
		}

		// 以下検証用

		fmt.Println(err) // service error with 'lib: parse error'

		// errors.Unwrap 関数でライブラリ関数を取り出す()
		wrappedErr := errors.Unwrap(err)
		fmt.Println(wrappedErr) // lib: parse error

		fmt.Println(errors.Is(wrappedErr, ErrParse)) // true
		fmt.Println(errors.Is(err, ErrParse))        // true
	}
}
```

`service`関数で新たなエラー型の値にしていますが`fmt.Errorf("%w", err)`とラップしているので、`errors.Is`関数を使うことでライブラリのエラー値の判定が可能になっています。

また、`errors.Unwrap`関数によりラップされた1つ分内側のエラー値を得ることができます。`errors.Is`関数は内部で`errors.Unwrap`を繰り返し呼び出すことで判定を実現しています。

## ライブラリが独自の公開エラー型の値を返すときは、ラップしても`errors.As`関数で本来のエラー値を得られる

```go:lib.go
package main // 本来は package lib な形式

import (
	"errors"
	"time"
)

// LibError はライブラリの独自エラー型
type LibError struct {
	kind     string
	orgError error
}

func (l *LibError) Error() string {
	return "error occured in Lib"
}

func (l *LibError) Kind() string {
	return l.kind
}

type LibTime time.Time

func ParseLibFunc(text string) (LibTime, error) {
	t, err := time.Parse(time.RFC3339, text)
	if err != nil {
		return LibTime{}, &LibError{kind: "Parse", orgError: err} // 独自エラー型の値を返す
	}
	return LibTime(t), nil
}
```

```go:main.go
package main

import (
	"errors"
	"fmt"
)

func service(text string) error {
	_, err := ParseLibFunc(text)
	if err != nil {
		return fmt.Errorf("service error with '%w'", err) // Unwrapを実装したエラーを返す
	}
	fmt.Println("service finished successfully")
	return err
}

func main() {
	if err := service("2020/02/14"); err != nil {
		var e *LibError
		// errors.As 関数を使うことでライブラリの独自エラー型の本来の値を得ることができる
		if errors.As(err, e) {
			fmt.Println(e)        // error occurred in Lib
			fmt.Println(e.Kind()) // Parse
		}
	}
}
```

`errors.As`関数を使うことでライブラリ独自エラー型の値が実装する`Kind()`メソッドを実行することができています。

# アプリコード内のカスタムエラーは`Unwrap() error`を実装しよう

上記の例では`service`関数は`fmt.Errorf("%w", err)`でUnwrapが実装されたエラー値を返していましたがアプリ内での独自エラー型を実装する場合はGo1.13時代以降はUnwrapメソッドを実装することが理想になります。

```go:main.goの一部

// AppError はアプリコード側の独自エラー型
type AppError struct {
	orgErr error
	code   string
}

func (e AppError) Error() string {
	return fmt.Sprintf("code: %s, msg: app error occurred", e.code)
}

func (e AppError) Unwrap() error {
	return e.orgErr
}

func service(text string) error {
	_, err := ParseLibFunc(text)
	if err != nil {
		return AppError{orgErr: err, code: "00A"}
	}
	return err
}

func main() {
	if err := service("2020/02/14"); err != nil {
		fmt.Printf("%T\n", err)           // main.AppError
		fmt.Println(err)                  // code: 00A, msg: app error occurred
		wrappedErr := errors.Unwrap(err)
		fmt.Printf("%T\n", err)           // *main.LibError
		fmt.Println(wrappedErr)           // error occured in Lib

		fmt.Println(errors.Is(err, ErrParse)) // true
}
```

上記のように`Unwrap() error`のメソッドを実装すればアプリケーションのエラーハンドリングにてIs関数とAs関数をライブラリ提供のエラー値に対して適用することができます。

# 参考

* https://qiita.com/shibukawa/items/e633e426a6e67ea2e830
* https://qiita.com/nayuneko/items/dea02377b797c2a52053
* https://cipepser.hatenablog.com/entry/go1.13-error-wrapping
* https://qiita.com/cia_rana/items/72a91175eadc1bffe9b0#fnref1