Golangのエラーハンドリングの基本

## 想定している読者

Goのエラーハンドリングについて体系だった記事が見つからなかったので、色々調べたことを整理して備忘録も兼ねて記事にしました。
以下のような方を読者対象に記事を書いています。

- Goのエラーハンドリングの基本的なやり方を知りたい
- スタックトレースを標準エラー出力したい
- エラーの種類に応じてステータスコードを変えたい

まずデファクトになっているエラーパッケージ[pkg/errors](https://golang.org/pkg/errors/)の使い方を確認して、実際のWebアプリケーションで追加で実装しないといけないことを説明していきます。

## そもそもerrorとは？

goのエラーは以下のようなエラーメッセージを返す関数を実装していれば満たせるインターフェースです。

```go
// cf. https://golang.org/pkg/builtin/#error
type error interface {
  Error() string
}
```

実際には返されたエラーがnilかどうかで条件分岐して、nilでない場合は`error.Error()`でエラー内容を出力するような使われ方をします。
例として、引数で与えられたファイルを開いて、それをJSONとして扱って、`map[string]interface{}`型の値（`jsonMap`）に変換してみます。

```go
import (
	"encoding/json"
	"fmt"
	"io/ioutil"
)

func main() {
  // 中身が空のファイルをJSONとして扱うとjson.Unmarshalでエラーになる
	if _, err := unmarshalToMap("src.json"); err != nil {
    // err.Error()の結果が出力される
    fmt.Println(err)
    // unexpected end of JSON input
  }
}

func unmarshalToMap(src string) (map[string]interface{}, error) {
	jsonMap := map[string]interface{}{}
  // ファイルパスからファイルを読み込む
	data, err := ioutil.ReadFile(src)
	if err != nil {
		return jsonMap, err
	}
  // ファイルの内容をJSONとみなして、key, valueの対応をmap[string]interface{}型の値にする
	if err := json.Unmarshal(data, &jsonMap); err != nil {
		return nil, err
	}
	return jsonMap, nil
}
```

しかし、引数で渡されたファイル`src.json`は何も書かれていないので（JSONの書式に沿っていないので）エラーが返ってきます。

```
$ go run main.go
unexpected end of JSON input
```

このように、`Error() string`さえ実装してエラーメッセージを返せれば`error`型を満たすことができます。

一見、上記のような方法でも問題がなさそうですが、実際のアプリケーションは実装が多くなり、複雑になりがちにも関わらず、`unexpected end of JSON input` というエラー内容だけだと、どのファイルの内容を`json.Umarshal`した時に起こったのか分からないとデバッグが困難になります。そこで重要なのが、返ってきたエラーに「何を」、「どこで」、「どんな処理で」起こったのかコンテキスト情報を付与することです。


## コンテキスト情報をエラー内容に含める

返ってきたエラーにコンテキスト情報を付与する一番簡単な方法は[`fmt.Error`](https://golang.org/pkg/fmt/#Errorf)を利用することです。
これは、指定したフォーマットにしたがって、第二引数以降をフォーマットし、新しいエラーメッセージを持つエラーを作って返すことができます。

先ほどの実装で`fmt.Errorf`を使ってみます。

```go
func unmarshalToMap(src string) (map[string]interface{}, error) {
	jsonMap := map[string]interface{}{}
    // ファイルパスからファイルを読み込む
	data, err := ioutil.ReadFile(src)
	if err != nil {
		return jsonMap, err
	}
	if err := json.Unmarshal(data, &jsonMap); err != nil {
		return nil, fmt.Errorf("read %s, %s", src, err) // ここをfmt.Errorfに置き換えた
	}
	return jsonMap, nil
}
```

これによって以下のようにどのファイルを読み込んでエラーがでたのかメッセージに含めることができます。

```
$ go run main.go
read src.json, unexpected end of JSON input
```

このように`fmt.Errorf`を使うことで「どこで」、「どんな処理で」エラーが起こったのか知ることができます。

## fmt.Errorfの問題点

しかし、これで問題なしかというとそうではありません。理由は`fmt.Errorf`は元のerrorインターフェースを実装するある型と値を消失させるからです。
`fmt.Errorf`の実装を見ると、内部で`errors.New`を呼び出していることがわかります。

```go
// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a ...interface{}) error {
	return errors.New(Sprintf(format, a...))
}
```

`errors.New`とは、`error`インターフェースを実装したエラーメッセージだけを持つ構造体を返します。

```go
// cf. https://golang.org/pkg/errors/#New

// New returns an error that formats as the given text.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}
```

しかし、ライブラリによって、独自の`error`インターフェースを実装した構造体を定義して、エラーメッセージ以外の情報を付与していることがよくあります。これによって、受け取った（errorインターフェース型に抽象化された）エラーを型アサーションして元の型に戻して、付加された値を取り出すことができます。`fmt.Errorf`は上記の`errorString`構造体に作り直してしまうのでこれをできなくしてしまいます。

例えば、JSONのKey, Valueの対応はSliceで表現することはできないのでUnmarshalするとエラーになりますが、そのエラーはJSONのバイト列におけるエラーが起こった位置やUnmarshalしようとした型の名前をエラーメッセージ以外を持っています。

具体的には、以下の[UnmarshalTypeError](https://golang.org/pkg/encoding/json/#UnmarshalTypeError)がerrorインターフェースを実装しており、それが返ってきます。

```go
// cf. https://golang.org/pkg/encoding/json/#UnmarshalTypeError

// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
	Value  string       // description of JSON value - "bool", "array", "number -5"
	Type   reflect.Type // type of Go value it could not be assigned to
	Offset int64        // error occurred after reading Offset bytes
	Struct string       // name of the struct type containing the field
	Field  string       // name of the field holding the Go value
}

func (e *UnmarshalTypeError) Error() string {
	if e.Struct != "" || e.Field != "" {
		return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
	}
	return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}
```

実際に、対応していない型にUnmarshalするとUnmarshalTypeError型に型アサーションして、エラーメッセージ以外を取り出してみます。

```go
import (
	"encoding/json"
	"fmt"
)

var jsonData = []byte(`
{
	"name": "user",
	"password": "pass"
}
`)

func main() {
  // []string型はJSONのKey, Valueの内容を持つことができないのでUnmarshalするとエラーになる
	valueOfInvalidType := make([]string, 0)
	err := json.Unmarshal(jsonData, &valueOfInvalidType)
	switch err := err.(type) {
	case *json.UnmarshalTypeError:
		fmt.Printf("type: %s\n", err.Type)
		fmt.Printf("offet: %d\n", err.Offset)
		fmt.Printf("Error(): %s\n", err)
	default:
		fmt.Println(err)
	}
}
```

```
$ go run main.go
type: []string
offet: 2
Error(): json: cannot unmarshal object into Go value of type []string
```

このようにしたくとも、`fmt.Errorf`が既存の`Error()`の結果以外の情報を切り捨ててしまうので、以下のような問題に直面します。

- errorインターフェースの元の型に応じて条件分岐ができなくなる
- errorインターフェースの元の型の持つコンテキスト情報が消失する

これらの問題を解決する手段でかつ、現在（2019/01/22）でデファクトとなっているエラーパッケージとして、`pkg/errors`があります。

## pkg/errorsを使う

では、具体的に`pkg/errors`で上記問題を解決できるのかというと、以下の`Wrap`と`Cause`を用います。

```
func Wrap(err error, message string) error
```

```
func Cause(err error) error
```

まず、`Wrap`を使うことで元のerrorインターフェースを実装した型と値を保持して、エラーメッセージだけコンテキスト情報を追加した新しいものにできます。

```go
if err := json.Unmarshal(data, &jsonMap); err != nil {
    // failed to unmarshal src.json: unexpected end of JSON input
		return nil, errors.Wrap(err, "failed to unmarshal src.json")
}
```

この結果だけ見れば、`fmt.Errorf("failed to unmarshal scr.json: %s", err)`した結果と同じですが、`%+v`でフォーマットするとStackTraceを出力することもできます。

```go
// cf. https://godoc.org/github.com/pkg/errors#hdr-Formatted_printing_of_errors
if err := json.Unmarshal(data, &jsonMap); err != nil {
    fmt.Printf("%+v", err)
}
```

```
main.unmarshalToMap
        /go/src/github.com/shoichiimamura/error-handling-example/main.go:30
main.main
        /go/src/github.com/shoichiimamura/error-handling-example/main.go:18
runtime.main
        /usr/local/go/src/runtime/proc.go:198
runtime.goexit
```

さらに`Cause`を使うことで`Wrap`されたエラーから元のerrorインターフェースを実装した型と値を取り出すことができます。
より厳密に言うと、`causer`インターフェースを実装していない一番最後の`error`インターフェース型を取り出します。

```go
// cf. https://github.com/pkg/errors/blob/master/errors.go#L269
func Cause(err error) error {
	type causer interface {
		Cause() error
	}

	for err != nil {
		cause, ok := err.(causer)
		if !ok {
			break
		}
		err = cause.Cause()
	}
	return err
}
```

これを使うと、以下のように元のエラーの型に応じた条件分岐が可能になります。

```go
switch err := errors.Cause(err).(type) {
case *json.UnmarshalTypeError:
  fmt.Println(err.Offset)
case *json.InvalidUnmarshalError:
  fmt.Println(err.Type)
default:
  fmt.Println(err)
}
```

この`Cause`でエラーの型に応じた条件分岐ができることがわかりました。

しかし、実際のAPIを持つアプリケーションサーバーの開発では、エラーの種類に応じてHTTPステータスコードを変えたいことがよくあります。これを実現するためには、`pkg/errors`を拡張したエラーパッケージを作る必要があります。次は`pkg/errors`を利用して、エラーの種類の応じたHTTPステータスコードを返す実装をしてみます。

## エラーに応じてステータスコードを選ぶ

`pkg/errors`を使った以下のようなerrorインターフェースを実装した構造体を作ることで、`Wrap`や`Cause`を使えつつ、エラータイプを取得できるようになります。`errors.Wrapf`などは説明を簡単にするためにラップしていませんが、同じような要領で実装することもできます。

```go
import (
	"github.com/pkg/errors"
)

// ErrorType エラーの種類
type ErrorType uint

const (
	Unknown ErrorType = iota
	InvalidArgument
	Unauthorized
	ConnectionFailed
)

// ErrorTypeを返すインターフェース
type typeGetter interface {
	Type() ErrorType
}

// ErrorTypeを持つ構造体
type customError struct {
	errorType     ErrorType
	originalError error
}

// New 指定したErrorTypeを持つcustomErrorを返す
func (et ErrorType) New(message string) error {
	return customError{errorType: et, originalError: errors.New(message)}
}

// Wrap 指定したErrorTypeと与えられたメッセージを持つcustomErrorにWrapする
func (et ErrorType) Wrap(err error, message string) error {
	return customError{errorType: et, originalError: errors.Wrap(err, message)}
}

// Error errorインターフェースを実装する
func (e customError) Error() string {
	return e.originalError.Error()
}

// Type typeGetterインターフェースを実装する
func (e customError) Type() ErrorType {
	return e.errorType
}

// Wrap 受け取ったerrorがErrorTypeを持つ場合はそれを引き継いで与えられたエラーメッセージを持つcustomErrorにWrapする
func Wrap(err error, message string) error {
	we := errors.Wrap(err, message)
	if ce, ok := err.(typeGetter); ok {
		return customError{errorType: ce.Type(), originalError: we}
	}
	return customError{errorType: Unknown, originalError: we}
}

// Cause errors.CauseのWrapper
func Cause(err error) error {
	return errors.Cause(err)
}

// GetType ErrorTypeを持つ場合はそれを返し、無ければUnknownを返す
func GetType(err error) ErrorType {
	for {
		if e, ok := err.(typeGetter); ok {
			return e.Type()
		}
		break
	}
	return Unknown
}
```

これによって、任意のErrorTypeを持つエラーを作って、Controller層でそれを取り出し、対応するステータスコードを選ぶことができます。

```go
func main() {
  err := Unauthorized.New("ある認証の処理内で返されたエラー")
  fmt.Println(statusCode(err)) // 401
}

func statusCode(err error) int {
  switch GetType(err) {
  case ConnectionFailed:
    return http.StatusInternalServerError // 500
  case Unauthorized:
    return http.StatusUnauthorized // 401
  default:
    return http.StatusBadRequest // 400
  }
}
```

## Panicでアプリケーションをクラッシュさせない

panicとは関数呼び出し元の処理を連続的に中断するgoの組み込み関数のことです。
明示的に呼び出すこともできますし、他にはnilポインタにメソッド呼び出しをした時などでも起きます。

```go
type User struct {
  Name string
}
func (u *User) Name() string {
  return u.Name
}

var user *models.User
user.Name() // panic
```

panicが起こるとdefer内でrecover（後述）しないと、プログラムはCrashしてしまうので、そうさせないようにPanicが起きた旨をエラーにして返してあげるようにします。そのためには、まず、deferとrecoverについて概要を押さえる必要があります。

deferは、関数を登録することができ、その定義元の関数がreturnされた後に呼び出され、panicが起こった場合も呼びされます。
そのため以下の実装は、returnされた値（`i`）をdeferでインクリメントして出力しているので、結果は`2`になります。

```go
func a() (i int) {
	defer func() {
		i++
		fmt.Printf("%d\n", i)
	}()
	return 1
}
```

また、deferに登録した関数は後入れ先出し（Last in First out）で呼び出されるので、以下の実装は`3210`を出力します。

```go
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
```

そして、recoverはpanicが起こったgoroutineを再び制御する組み込み関数で、panicが起こった後にdeferが呼ばれ、その中でpanicの伝播を止める役割を担います。defer外でrecoverしてもpanic時には呼び出されず、nilを返すだけなので、この使われ方以外はなさそうです。

以下は、panicが起こった時にdefer内でrecoverを呼び出し、panicの伝播を止めてエラーを返しています。

```go
import (
	"fmt"
	"github.com/pkg/errors"
)

func panicAndRecover() (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = errors.New(fmt.Sprintf("recovered: %v\n", r))
		}
	}()
	panic("panic at panicAndRecover")
	return
}

func main() {
  err := panicAndRecover()
  fmt.Println(err)
  // recovered: panic at panicAndRecover
}
```

こうすることで、panicが起こってもエラーとして扱うことができます。

## 参考
- [Golang — handling errors gracefully](https://hackernoon.com/golang-handling-errors-gracefully-8e27f1db729f)
- [pkg/errors](https://godoc.org/github.com/pkg/errors)
- [Defer, Panic, and Recover](https://blog.golang.org/defer-panic-and-recover)