Go言語でコードを書く際によく使うエラーハンドリング方法をまとめます。
エラーハンドリングといったら大げさに聞こえますが、シンプルに普通のエラー処理です。
普段Go言語をいじっている方からすればです。
Goの処理は基本エラーがつきもの
初めてGoを触った時の感想ですが、例外処理はなく常にエラーを持ち回る印象を受けています。
関数はメソッドは正常値とエラーをセットで返していて、コール元はその戻り値よりエラーチェックをしてから、正常処理か異常処理のいずれかを実施する流れがスタンダードになっています。
https://go.dev/play/p/RI6w5doxZZh
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
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
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
は必ずキャッチできるようになります。
拾った後の処理はサンプルです。必要に応じて各自実装してください。
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
は必ずキャッチできるようになります。
拾った後の処理はサンプルです。必要に応じて各自実装してください。
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)
}
}