Go言語でのエラーハンドリング方法

Go言語でコードを書く際によく使うエラーハンドリング方法をまとめます。

エラーハンドリングといったら大げさに聞こえますが、シンプルに普通のエラー処理です。
普段Go言語をいじっている方からすればです。

## Goの処理は基本エラーがつきもの
初めてGoを触った時の感想ですが、例外処理はなく常にエラーを持ち回る印象を受けています。
関数はメソッドは正常値とエラーをセットで返していて、コール元はその戻り値よりエラーチェックをしてから、正常処理か異常処理のいずれかを実施する流れがスタンダードになっています。
https://go.dev/play/p/RI6w5doxZZh

```go
package main

import (
	"errors"
	"log"
)

func main() {
	s, err := doSomething()
	if err != nil {
		doErrorProcess(err)
		return
	}
	doSuccessProcess(s)
}

func doSomething() (string, error) {
	// return "test string", nil
	return "", errors.New("test error")
}

func doSuccessProcess(s string) {
	log.Println(s)
}

func doErrorProcess(err error) {
	log.Println(err)
}
```

## エラーを記録しておいて無視する
関数やメソッドから返ってくるエラーは、記録として残しておいてエラー処理をしないことも可能です。
次の例では、エラーログが出力されますが、その後正常処理も実行されますね。
https://go.dev/play/p/Jwwgz-zsArX

```go
package main

import (
	"errors"
	"log"
)

func main() {
	s, err := doSomething()
	if err != nil {
		log.Println(err)
	}
	doSuccessProcess(s)
}

func doSomething() (string, error) {
	// return "test string", nil
	return "", errors.New("test error")
}

func doSuccessProcess(s string) {
	if s == "" {
		s = "s is empty"
	}
	log.Println(s)
}
```

## エラーを完全無視する
次に、返ってくるエラーを完全無視することも可能です。
よっぽど自信がある場合を除いて、この方法はおすすめできません。
どこでエラーが起こったのか、追うことができなくなるからです。
https://go.dev/play/p/BCaDQ7dOSgO

```go
package main

import (
	"errors"
	"log"
)

func main() {
	s, _ := doSomething()
	doSuccessProcess(s)
}

func doSomething() (string, error) {
	// return "test string", nil
	return "", errors.New("test error")
}

func doSuccessProcess(s string) {
	if s == "" {
		s = "s is empty"
	}
	log.Println(s)
}
```
## エラー処理が漏れた場合でもハンドリングしたい
どれだけちゃんとエラー処理をしたとしても、予期せぬエラーが発生する場合があります。
アプリ側の最終手段として、どこかで起こった`panic`や`error`でもリカバリーできるような実装にしておきたいところです。
バッチ形式の実装とAPIサーバ形式の実装で、`panic`の拾い方が微妙に違います。
それぞれご紹介しましょう。
### バッチ処理のケース
`main`処理の一行目で`panic`を拾うための処理を`defer`で書いてしまいましょう。
すると、以降の処理で起こった`panic`は必ずキャッチできるようになります。
拾った後の処理はサンプルです。必要に応じて各自実装してください。

```go
package main

import (
	"fmt"
	"log"
	"os"
	"runtime"
)

func main() {
	defer recoverPanic()
	doSomething("", 0)
}

func doSomething(s string, i int) (string, error) {
	panic("test panic") // this is a test panic
}

func recoverPanic() {
	if r := recover(); r != nil {
		err, ok := r.(error)
		if !ok {
			err = fmt.Errorf("%v", r)
		}
		stackTrace := make([]byte, 2048)
		runtime.Stack(stackTrace, true)
		msg := fmt.Sprintf(`[Err]panic in main %v %s`, err, string(stackTrace))
		log.Println(msg)
		os.Exit(1)
	}
}
```

### APIサーバのケース
APIのハンドラを`panicInterceptor`で囲む必要があります。
囲むというのは、`panicInterceptor`の引数にAPIのハンドラをセットすることです。
これでAPI処理中に起こった`panic`は必ずキャッチできるようになります。
拾った後の処理はサンプルです。必要に応じて各自実装してください。

```go
package main

import (
	"fmt"
	"log"
	"runtime"
)

func loadServer() {
	panicInterceptor(doSomethingAPI)
}

func doSomethingAPI(s string, i int) {
	panic("test panic") // this is a test panic
}

func panicInterceptor(f func(string, int)) func(string, int) {
	return func(s string, i int) {
		defer func() {
			if r := recover(); r != nil {
				err, ok := r.(error)
				if !ok {
					err = fmt.Errorf("%v", r)
				}
				stackTrace := make([]byte, 2048)
				len := runtime.Stack(stackTrace, true)
				msg := fmt.Sprintf(`[Err]panic %v %s`, err, string(stackTrace[:len]))
				log.Println(msg)
				// errro response
			}
		}()
		f(s, i)
	}
}
```