Goで独自エラー型を定義する

[Goでのエラー判別](https://qiita.com/minoritea/items/94feb33b61c5fdcc6105)という記事で、Goのエラーの判別方法にはだいたい3種類あるという話をしました。

1. どの関数から返されたかで判別
2. 定義済みの変数と比較して判別
3. 独自エラー型を定義してキャストして判別

今回の記事では3の独自エラー型を定義する際のノウハウについて扱います。

※ Go2アドベントカレンダーの[Goのカスタムエラーとその自動生成について](http://akito0107.hatenablog.com/entry/2018/12/14/112717)という記事とかなり内容が被っている気がしますが、ネタかぶりはご容赦ください。
[generr](https://github.com/akito0107/generr)はすごく良いツールだと思います！この記事のコードを自動生成したい場合にご利用ください。

それではいくつかの実装例を紹介します。

# 例1: エラーコードを持たせる
例えばエラーコードを持たせたい場合を考えます。
この場合、エラーコード型にそのまま`error`インターフェースを持たせてしまえばいいでしょう。
Goのエラーはインターフェースなので`Error`メソッドさえ持たせればどんな型もエラーにすることができます。

```go
type ErrorCode uint
const (
    ErrCode1 ErrorCode = 1
    ErrCode2 ErrorCode = 2
    // ...
)

func (e ErrorCode) Error() string {
    // あるいはパッケージ変数にErrorCode => エラーメッセージのMapを定義して引くなどしてもよい。
    switch e {
    case ErrCode1:
        return "error code 1"
    case ErrCode2:
        return "error code 2"
    default:
        return "unknown error code"
    }
}
```

`ErrorCode`型は`error`インターフェースを持っているので`error`を返す関数の戻り値にそのまま使うことができます。

```go
func doSomething() error { return ErrCode1 }
```
エラーコードを取り出すときはキャストします。

```go
    err := doSomething()
    if err != nil {
        code, ok := err.(ErrorCode)
        if ok {
            // この例ではerrがエラーコードでなければデフォルト値をセットしている
            // あるいはErrorCodeでないエラーの場合の処理フローを用意するのもよい
            code = defaultErrorCode
        }
        // ErrorCodeを元になにかのエラーハンドリングを行う
        handleErrorCode(code)
        return
    }
```

# 例2: エラーに追加情報を加える
例1ではリポジトリの内部パッケージで定義した関数が`ErrorCode`型のエラーを返すことを期待していました。
しかし、リポジトリ外のパッケージの関数がエラーを返してきたときに、例えばエラーコードを追加したい場合はどうすればいいでしょうか？

そのような場合は追加情報を保持する構造体を定義してその中に元のエラーを埋め込んでしまえばよいのです。

```go
type ErrorWithCode struct {
    error // オリジナルのエラー
    Code ErrorCode // 追加情報（エラーコード）
}
```

他のエラーが返ってきた場合にErrorWithCode型でラップします。

ちなみにErrorWithCode型は`error`型を埋め込んでいるのでそのまま`error`インターフェースを持っている型として扱えます。

```go
func doAnotherThing() error {
    // externalpackage.DoSomething は error を返す関数
    err := externalpackage.DoSomething()
    if err != nil {
        // 元のエラーを埋め込んで、エラーコードを追加する
        return ErrorWithCode{ error: err, Code: ErrCode2 }
    }
    return nil
}
```

エラーコードを取り出すときはキャストします。

```go
    err := doAnotherThing()
    if err != nil {
        errWithCode, ok := err.(ErrorWithCode)
        if !ok {
            // この例ではErrorWithCode型でなければデフォルト値を使う
            handleErrorCode(defaultErrorCode)
            return
        }
        // ErrorCodeを元になにかのエラーハンドリングを行う
        handleErrorCode(errWithCode.Code)
        return
    }
```

# 例3: パッケージ間の依存性をなくす
例2ではエラーコードを取り出すたびに`ErrorWithCode`型にキャストしていました。
このやり方だと、別パッケージでエラーコードを取り出したいときは、`ErrorWithCode`型を定義してあるパッケージを毎回インポートしなければなりません。

また、複数のパッケージでそれぞれ独立にエラーコードをもたせたエラー型を定義した場合、それらのエラーをハンドリングするには、それぞれのパッケージをインポートしなければなりません。

このような場合、Goではインターフェースを用いて解決します。

例えば`foo`パッケージで`ErrorWithCode`という独自エラー型を定義しているとします。

`foo.DoSomething`は`ErrorWithCode`のインスタンスを`error`型で返す関数です。

```go
package foo

type ErrorWithCode struct {
    error // オリジナルのエラー
    Code uint // エラーコード（分かりやすくするため、この例ではuint型とします）
}

func (e ErrorWithCode) GetErrorCode() uint { return e.Code }

func DoSomething() error { return ErrorWithCode{/* ... */} }
```

別の`bar`パッケージでは中で`foo.DoSomething`を呼んでエラーを返すような関数を定義します。

```go
package bar

import "foo"

func DoAnotherThing() error { return foo.DoSomething() }
```

例えば`main`パッケージで`bar.DoAnotherThing`を呼んだ場合、
もし例2のやり方で、エラーコードを取り出そうとするならば`foo`をインポートして`foo.ErrorWithCode`型にキャストしなければなりません。

しかしインターフェースを使うと以下のように`foo`への依存性をなくすことが出来ます。

```go
package main

// "foo"のimportは不要
import "bar"

// GetErrorCodeを持つインターフェースを定義
type errorCodeHolder interface {
    GetErrorCode() uint
}

func main() {
    err := bar.DoSomething() // 中でfoo.ErrorWithCodeを返している
    if err != nil {
        errCodeHolder, ok := err.(errorCodeHolder)
        if !ok {
            // この例ではerrorCodeHolder型でなければデフォルト値を使う
            handleErrorCode(defaultErrorCode)
            return
        }
        // ErrorCodeを元になにかのエラーハンドリングを行う
        handleErrorCode(errorCodeHolder.GetErrorCode())
        return
    }
}
```

Goのインターフェースは、構造体やそれを返す関数が定義されたパッケージとは、完全に独立して定義することが出来ます。
ですので、このように実体を定義したパッケージと、そこから情報を取り出すパッケージを分離できるのです。

# 例4: 元のエラーを取り出す
追加情報でエラーをラップした場合でも、元のエラーを取り出して処理したいケースが存在します。
例えば、`io.EOF`エラーかどうかを判別する場合、元のエラーでなければ同一判定が出来ません。

そのような場合には[github.com/pkg/errors](https://github.com/pkg/errors)を使いましょう。

`github.com/pkg/errors`はエラーに追加メッセージやスタックトレースを追加するライブラリですが、
元のエラーメッセージを取り出したいときのために`Cause`関数を用意しています。

`github.com/pkg/errors`でラップされたエラーは内部的に`Cause`メソッドを持っており、このメソッドでラップ元のエラーにアクセスすることが出来ます。

この`Cause`メソッドを持ったインターフェースを`causer`と定義しています。

パッケージ関数の方の`Cause`は引数のエラーが`causer`にキャスト出来たらラップ元のエラーを取り出す処理を再帰的に回していて、最終的には元のエラーを取り出せるという仕組みになっています。

もちろん、独自に定義されたエラー型でも`Cause`メソッドを定義すれば、`causer`インターフェースを持ったオブジェクトとして、`Cause`関数にその型のエラーオブジェクトを渡すことが出来ます。

Goのインターフェースはシグネチャーさえマッチしていれば、定義されたパッケージを問わないからです。
ですから、外のパッケージから渡されたオブジェクトでも、中のプライベートなインターフェースにキャストできるわけです。
とても便利ですね！

```go
// ErrorWithCodeの定義は割愛。
// Causeメソッドを定義して元のエラーを取り出せるようにする。
func (e ErrorWithCode) Cause() error { e.error }
// EOFにエラーコードを付与したオブジェクトを返す
func GetEOFWithCode() error {
    return ErrorWithCode{error: io.EOF, code: /* エラーコード */}
}
```

```go
err := GetEOFWithCode()

// Cause関数は元のエラーを取り出す
if errors.Cause(err) == io.EOF {
    // ...
}
```

# 例5: 複数回ラップしたとき、目当ての情報を取り出す
複数の追加情報を別々の場所で追加するケース、例えば、ある関数の中でエラーコード付きエラーを返して、エラー伝搬した呼び出し元の関数でさらにリクエストIDも追加して…、というようなケースの場合、それぞれの追加情報を取り出すにはどのようにすればいいでしょうか。

`github.com/pkg/errors.Cause`だと、元のエラーが取り出されてしまい、追加情報にアクセスすることが出来ません。

そのような場合は`Cause`を参考に独自のヘルパー関数を定義しましょう。

以下の例では、`GetErrorCode`メソッドを持つエラーの場合はエラーコードを取り出し、
それ以外で`causer`にキャストできる場合は、再帰的に元のエラーを取り出して、型チェックを行うことを繰り返しています。

```go
type errorCodeHolder interface { GetErrorCode() uint }
type causer interface { Cause() error }

func GetErrorCode(err error) uint {
EXT: // for文を抜けるため、外側にラベルを定義している
	for err != nil {
		switch cause := err.(type) {
		case errorCodeHolder:
			return cause.GetErrorCode()
		case causer:
			// Cause持ちの場合は元のエラーを取り出してループ継続
			err = cause.Cause()
		default:
			break EXT // forループの外まで抜ける
		}
	}
	// エラーコードを保持したエラーが見つからなかったり、
	// エラーがnilの場合にはデフォルト値を返す
	return defaultErrorCode
}
```

このやり方であれば、さらに`GetRequestID`などと、他の追加情報を取り出すヘルパーを併用しても、
それぞれの追加情報を矛盾なく取り出すことが出来ます。

さらに、`github.com/pkg/errors`とも併用できます。

# 例6: エラー発生時の状態を元にハンドリングする
ときにはエラー発生時の状態を元に、呼び出し元で何らかのハンドリングを行いたいときがあります。
そのような場合は関数オブジェクトをラップしましょう。

```go
type ErrorFunc func()
func (e ErrorFunc) Error() string { return /*エラー情報*/ }

func doSomething() error {
    /* 何らかの処理 */
    return ErrorFunc(func() {
        // エンクロージャ（ErrorFuncの外のスコープ）の情報を参照する処理
    })
}
```

```go
err := doSomething()
if callback, ok := err.(ErrorFunc); ok {
    // ErrorFunc型として定義された関数をコールバックとして受け取るようなエラーハンドラ
    handleErrorWithCallback(callback)
}
```

例ではシンプルに関数型に`error`インターフェースを持たせていますが、
独自エラーの構造体を定義して、元のエラーとそれを扱うコールバック関数をそれぞれ持たせるなどしてもよいでしょう。

# おわりに
ここまで様々な独自エラー型の作り方を紹介してきました。
他にも様々なエラー型が必要に応じて定義できると思います。

僕が強く主張したいのは、Goの`error`型というのはただのインターフェース型でしか無いということです。
なので内部実装はいかようにでも定義できるのです。

個人的には`error`型は`interface{}`型と同様の扱いをしていいと思っています。
好きなオブジェクトをどんどん放り込みたいような変数や引数の型に`interface{}`を使うように、`error`型ももっと柔軟に活用できるはずです。
ただしそれだとエラーとそうでないオブジェクトの判別が難しくなるため、`Error`メソッドを要求しているだけなのです。
つまり`Error`メソッドはエラーかそうでない型かを判別するタグのようなものでしか無いと考えてもいいと思います。

というわけで、皆さんも恐れずにどんどん独自エラー型を定義していきましょう。
ただし、それは安全なエラーハンドリングのための道具であることだけは忘れないでください。