LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-01-29

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)
}

エラー処理が漏れた場合でもハンドリングしたい

どれだけちゃんとエラー処理をしたとしても、予期せぬエラーが発生する場合があります。
アプリ側の最終手段として、どこかで起こったpanicerrorでもリカバリーできるような実装にしておきたいところです。
バッチ形式の実装と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)
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1