Go言語のエラーハンドリング(2022年版)

半年前にGo言語で開発を行うチームにJoinし、初めてGo言語を書く経験をしました。
自分やチームメンバー向けのメモとして、調べたことをまとめておきます。

# 1. Goのエラーハンドリングを考える上での前提知識（ざっくり）
- Go言語には、他の言語にあるような例外(throw, catch...) の機能が存在しない。
    - Go言語は簡潔さを重視した言語のため、基本的に「例外は使うべきではない」という思想に基づいて作られている
- その代わり、メソッドのエラーハンドリングをしたい場合は、主にメソッドの返り値として `error` 型の変数を返すことが推奨される
    - 主要なライブラリもこのルールに基づいて作られている
    - 例： `json.Marshal` 
        - jsonへのParseに失敗した場合、2番目の返り値としてerrを返す。
        - `err` が `nil` でなければ処理中に問題が起きたと判断し、呼び出し元でエラー対応を行う必要がある
```golang
s, err := json.Marshal(input)
if err != nil {
	panic(err)
}
```
- とはいえ、複雑なエラーハンドリングに対応するには上記だけだと難しいため、Go言語のバージョンが上がるにつれ、「エラーのWrap」などのエラーハンドリング関連機能の強化が行われている
- そのため、Goのバージョンによって推奨される書き方が異なることがある。過去のドキュメントを読む場合は注意が必要。
    - 例 `xerrors` 
        - Go公式がメンテナンスをしていた外部ライブラリ。2019.09のGo v1.13のリリース時に、機能の一部（errors.As, errors.Is, errors.Unwrapなど）が、標準errorsとfmtに取り込まれた。
        

### 参考記事
- [Go 1.13 Release Notes](https://go.dev/doc/go1.13)
- [xerrors](https://pkg.go.dev/golang.org/x/xerrors)

# 2. Goにおけるerrorとは
- 以下の `interface型` のこと。
```
type error interface {
    Error() string
}
```

- つまり、 `Error()` というメソッドさえ持っていれば、どんな `Struct` であっても `error` 型を満たす値として扱うことができる
- 例えばカスタムエラー `*MyError` は、下記のコードで定義できる
```go
type MyError struct {
    // other fields
}
func (e *MyError) Error() string { return ... }
```

### 参考記事
- [golang error handling (Go1.13)](https://qiita.com/yuukiyuuki327/items/4d31d62f6b70476082f3) <- すごくいい記事でした


# 3. error chain
- 複雑なエラーハンドリングに対応するために、Go 1.13から追加されたのが **error chain（エラーチェーン）** という考え方です。
    - 例えば、孫メソッドでエラーが起きた時、子メソッドはそのエラーをWrapして、親メソッドに返す・・・ということができます。

```go
func 子メソッド() error {
    ...
    if output, err := 孫メソッド(); err == nil {
        return fmt.Errorf("孫メソッドでエラーが起こりました 詳細:%w", err) //型は*fmt.wrapError
    }
    ...
}

func 親メソッド() {
    err := 子メソッド()
    // ここで errors.Is()や errors.As() を用いてエラー判定をする
}
```
- Wrapされたerror（`*fmt.wrapError`）は、 `Unwrap()` を呼び出すことで、元のエラーを呼び出すことができます。
    - `Unwrap`を繰り返し呼び出すことで得られる一連のエラーのことをerror chain（エラーチェーン）と呼びます。

### 参考
- [Source file src/fmt/errors.go](https://go.dev/src/fmt/errors.go)

# 4. エラーの生成
## 4-1. `errors.New()` エラーを新規作成する時の標準関数
### 説明
- [公式ドキュメント errors.New()](https://pkg.go.dev/errors#New) 
    - `func New(text string) error`
    - 与えられたtextをもとに新しい `error` を作成する
- なお、スタックトレースを出力することはできない

### サンプルコード
- 例えば、標準パッケージのoserrorは以下の実装をしている
- [src/internal/oserror/errors.go](src/internal/oserror/errors.go)        
```
package oserror

import "errors"

var (
	ErrInvalid    = errors.New("invalid argument")
	ErrPermission = errors.New("permission denied")
	ErrExist      = errors.New("file already exists")
	ErrNotExist   = errors.New("file does not exist")
	ErrClosed     = errors.New("file already closed")
)
```

## 4-2. `fmt.Errorf()` エラーを生成 or Wrapする時に用いる標準関数
### 説明
- [公式ドキュメント fmt.Errorf](https://pkg.go.dev/fmt#Errorf)
    - フォーマット指定子に従ってフォーマットし、errorを返す
        - フォーマット指定子が `%w` を含む場合、返されたerrorはUnwrapメソッドを実装します。
        - この場合、返り値は`*fmt.wrapError` 型になる
- なお、スタックトレースを出力することはできない

### サンプルコード
- フォーマット指定子を使うところでは `fmt.Errorf` を使っている様子
- [src/database/sql/sql.go](https://cs.opensource.google/go/go/+/master:src/database/sql/sql.go;l=3248?q=%25w&ss=go%2Fgo:src%2Fdatabase%2Fsql%2F)

```go
	if rs.lastcols == nil {
		return errors.New("sql: Scan called without calling Next")
	}
	if len(dest) != len(rs.lastcols) {
		return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
	}
```

## 4-3. 他の手段
- スタックトレースが欲しい場合は、 [pkg/errros](https://github.com/pkg/errors) や [xerrors](https://github.com/golang/xerrors)などの外部ライブラリを使用する必要あり
- 参考
    - [今goのエラーハンドリングを無難にしておく方法（2021.09現在）](https://zenn.dev/nekoshita/articles/097e00c6d3d1c9)


# 5. エラーの判定

## 5-1. `err ！= nil` エラーがあるかないか判定
- エラーの型判定をしたいのでなければ、この書き方で問題ありません。
```golang
val, err := myFunction( args... );
if err != nil {
  // エラーハンドリング
}
```

## 5-2. `errors.Is` エラーを値レベルで比較し、一致判定
### 説明
- [公式ドキュメント errors.Is](https://pkg.go.dev/errors#Is)
    - `func errors.Is(err, target error) bool`
    - error chainを辿り、いずれかのエラーが対象と一致するかどうかを判定
- 値レベルの比較のため、errorの型が同じでも値が異なる場合は一致判定されない
    - 例えば、以下の `APIError` を使用した場合、`HTTPStatusCode` が異なれば一致判定されない
```
type APIError struct {
	HTTPStatusCode int
}

func (e *APIError) Error() string {
	return fmt.Sprintf("...")
}
```
- `errors.New()` で作られたエラーなど、値を持たない場合は問題なく判定できる様子
- カスタムエラー(`APIError`)自体にIsメソッドを持たせることで、一致判定を上書くこともできる                

### サンプルコード
- [errors.Is 公式ドキュメントのサンプルコード](https://pkg.go.dev/errors#example-Is)
- ファイルを開く操作時のエラー原因が「ファイルが存在しない」であれば別途エラーメッセージを表示
```go
if _, err := os.Open("non-existing"); err != nil {
	if errors.Is(err, fs.ErrNotExist) {
		fmt.Println("file does not exist")
	} else {
		fmt.Println(err)
	}
}
```

ちなみに package fs のエラーの実装（[リンク](https://cs.opensource.google/go/go/+/master:src/io/fs/fs.go;l=139)）
```go:src/io/fs/fs.go
var (
	ErrNotExist   = errNotExist()   // "file does not exist"
)
func errNotExist() error   { return oserror.ErrNotExist } // errors.New("file already exists")
```

### 参考記事
- [［Golang］errors.Is() errors.As() 完全ガイド〜使い方と違いをしっかり調査しました〜](https://zenn.dev/kskumgk63/articles/550dc9d42078d968beac)

## 5-3. `errors.As` 比較対象を型レベルで比較し、データを取り出す
### 説明
- [公式ドキュメント errors.As](https://pkg.go.dev/errors#As)
    - `func errors.As(err error, target interface{}) bool`
        - error chainを辿り、target に代入可能な最初のerrを見つける。
            - 見つかった場合、 `target` をそのエラー値に設定して`true`を返す
            - 見つからなかった場合は `false`を返す
        - `target` は `nil` ではない `pointer` である必要あり。
- `errors.Is` とかなり記法が異なるので注意。特に第二引数にpointerを渡す点が最初はつまづきやすい

### サンプルコード
- [errors.As 公式ドキュメントのサンプルコード](https://pkg.go.dev/errors#As)
```
if _, err := os.Open("non-existing"); err != nil {
	var pathError *fs.PathError
	if errors.As(err, &pathError) {
		fmt.Println("Failed at path:", pathError.Path)
	} else {
		fmt.Println(err)
	}
}
```

ちなみに package fs のエラーの実装（[リンク](https://cs.opensource.google/go/go/+/master:src/io/fs/fs.go;l=139)）
```go:src/io/fs/fs.go
type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
```
     
### 参考記事：
- [［Golang］errors.Is() errors.As() 完全ガイド〜使い方と違いをしっかり調査しました〜](https://zenn.dev/kskumgk63/articles/550dc9d42078d968beac)

## 他参考記事
- [Error handling and Go](https://go.dev/blog/error-handling-and-go)
- [Go 1.13時代のエラー実装者のお作法](https://qiita.com/shibukawa/items/e633e426a6e67ea2e830)
- [Go1.17で警告されるようになったerror#Is/As/Unwrap](https://future-architect.github.io/articles/20210819b/)