Closed6

Goのエラーハンドリング

  • 特に呼び出した関数から複数のエラーが返ってくることを前提とする
  • 下位で発生したエラーのメッセージを残しつつ上位でもエラーメッセージを付与したり、エラーコードを付与したりしたくなるので、error interfaceをプロパティに含むカスタムエラーを活用するのがベターかもしれない
  • 個人的には一番上のカスタムエラーを活用するのが好み
    • カスタムエラーを活用する
    • エラーをWrapする
    • エラー毎にエラー構造体を定義する
  • エラーは上位の呼び出し元で処理する

カスタムエラーを活用する

https://qiita.com/nayuneko/items/3c0b3c0de9e8b27c9548#errorインタフェースを実装した構造体を返却する
package main

import (
    "fmt"
    "os"
)

// エラー処理用の構造体
type MyError struct {
    Msg string
    Code int
}
// MyError構造体にerrorインタフェースのError関数を実装
func (err *MyError) Error() string {
    return fmt.Sprintf("err %s [code=%d]", err.Msg, err.Code)
}

func main() {
    if err := doError(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("(o・∇・o)終わりだよ~") // ここにはこない
}

func doError() error {
    return &MyError{Msg: "(*>△<)<ナーンナーンっっ", Code: 19}
}

呼び出し元はcodeを基にエラーを区別できる。
エラーコードとかを定義しているPJでは有効。

エラーをWrapする

https://qiita.com/nayuneko/items/dea02377b797c2a52053#エラーをwrapする

Wrapを繰り返していると関数の呼び出しが二層、三層にもなってしまうと、エラー1: エラー2: エラー3のように若干冗長気味になってしまうので注意が必要です。

  • エラーメッセージが冗長になる
  • wrapを繰り返すと、どれが重要なエラーかわからなくなる
  • 単純に取り回しが面倒に思える

エラー毎にエラー構造体を定義する

エラーの型でエラーを区別する

type HogeError struct {
    Msg string
}

func (e *HogeError) Error() string {
    return "hogehoge"
}

type FooError struct {
    Msg  string}

func (e *FooError) Error() string {
    return "foofoo"
}

err := func() // 何か処理する関数
if err != nil {
    switch e := err.(type) {
    case *HogeError:
        fmt.Println("HogeError")
    case *FooError:
        fmt.Println("FooError")
    default:
        fmt.Println("その他のエラー", err)
    }
}
  • 呼び出し元でどんなエラーが上がってくるかを把握する必要があってしんどい
  • エラーの定義がめんどうくさい

参考

https://zenn.dev/koduki/articles/2840dab22efc68
https://text.baldanders.info/golang/error-handling/
https://go.dev/blog/errors-are-values

例外について考える

CleanArchitectureベースのAPIサーバにおいて、

  • 例外が発生したことにより、正常に処理を終了することができない、あるいは、リクエストに応えられないことをユーザに伝えたい
  • Controllerはusecaseを呼び出した際にどのような例外が発生したかを知る必要がある
  • usecaseでは複数のエラーが発生する可能性がある
  • エラーの種類によってレスポンスを変えたい
  • エラーを区別したい

そもそも、

  • なぜ例外が必要か?
    • 正常に処理が継続できないことを知らせるため
  • なぜ例外をハンドリングする必要があるのか?
    • 例外によってその後の処理が変わるため
      • ユーザ/開発者に伝える
      • 無視して構わない内容なので無視する

実装より

  • ログは決まった場所ではきたい
    • echoならCustomErrorHandlerを使いたい
  • 下位から上位にいくにつれて、エラーの性質が変わる場合、適切な場所でエラーコードを変えるなどエラーの性質変化に対応したい
  • 下位から上位にいくにつれて、エラーメッセージを追加で付与したい場合がある

カスタムエラー活用時の各関数の戻り値

func doSomething() error

or

func doSomething() *MyError

errorを返すのがお作法っぽい

とりあえず理解する

https://qiita.com/niusounds/items/0f458be1ecce8a955a64
func fn() (int, *MyError) {
    return 42, nil
}

func main() {
    result, err := fn()
    if err != nil {
        fmt.Println("This should not be called")
    }
    fmt.Println(result, err)
}

返却するエラーの型を*MyErrorではなくerrorとすると、この問題は発生しません。

より深く理解する

https://go.dev/blog/error-handling-and-go

It’s usually a mistake to pass back the concrete type of an error rather than error, for reasons discussed in the Go FAQ

上記のFAQは以下。

https://golang.org/doc/faq#nil_error

It's a good idea for functions that return errors always to use the error type in their signature (as we did above) rather than a concrete type such as *MyError, to help guarantee the error is created correctly. As an example, os.Open returns an error even though, if not nil, it's always of concrete type *os.PathError.

理由

上記記事より引用

interfaces are implemented as two elements, a type T and a value V. V is a concrete value such as an int, struct or pointer, never an interface itself, and has type T. For instance, if we store the int value 3 in an interface, the resulting interface value has, schematically, (T=int, V=3)

An interface value is nil only if the V and T are both unset, (T=nil, V is not set)

func returnsError() error {
	var p *MyError = nil
	if bad() {
		p = ErrBad
	}
	return p // Will always return a non-nil error.
}

If all goes well, the function returns a nil p, so the return value is an error interface value holding (T=*MyError, V=nil). This means that if the caller compares the returned error to nil, it will always look as if there was an error even if nothing bad happened.

カスタムエラー活用時のコンストラクタの返り値

error型(interface{}型)? or *MyError型

type MyError struct {
	Msg  string
	Code int
}

func (err *MyError) Error() string {
	return fmt.Sprintf("err %s [code=%d]", err.Msg, err.Code)
}

// New コンストラクタ
func New(msg string, code int) *MyError {
	return &MyError{
		Msg:  msg,
		Code: code,
	}
}

*MyErrorが良さそう。
error型で返すと以下のような型判定ができない。

if me, ok := err.(*MyError); ok {
        doSomething(me)
}

以下のような場合はerrors.Is()が使える。

import (
    "fmt"
    "errors"
)

ErrFoo := errors.New("foo error")

func main() {
    wrapped := fmt.Errorf("wrapped woo: %w", ErrFoo)
    if errors.Is(wrapped, ErrFoo) {
        fmt.Println("this error is caused by %v", ErrFoo)
    }
}

■参考

https://qiita.com/hiro_o918/items/fb01014e51354b8bb49f

Stack Trace

  • 標準のerrorsパッケージではstacktraceの格納はできない

独自Error構造体でStackTraceを出力する

zapを使っている場合、こんな感じとか?

type HogeError struct {
	Msg        string
	Code       int
	StackTrace string
}

func (he *HogeError) Error() string {
	return fmt.Sprintf("error: code[%d], message[%s]", pe.Code, pe.Msg)
}

// New コンストラクタ
func New(msg string, code int) *HogeError{
	// stack traceを取得
	// zap.takeStacktrace()を呼び出せればStack()を呼ぶ必要はないが、
	// unexportedなので間接的に呼び出す
	stack := zap.Stack("").String
	return &HogeError{
		Msg:        msg,
		Code:       code,
		StackTrace: stack,
	}
}
errorを吐きたい場所.go
err := New("message", 1111)

参考

https://qiita.com/roba4coding/items/769ddb220bc61cd19df1
https://zenn.dev/nekoshita/articles/097e00c6d3d1c9
このスクラップは14日前にクローズされました
ログインするとコメントできます