Goのエラーハンドリング
- 特に呼び出した関数から複数のエラーが返ってくることを前提とする
- 下位で発生したエラーのメッセージを残しつつ上位でもエラーメッセージを付与したり、エラーコードを付与したりしたくなるので、error interfaceをプロパティに含むカスタムエラーを活用するのがベターかもしれない
-
個人的には一番上のカスタムエラーを活用するのが好み- カスタムエラーを活用する
- エラーをWrapする
- エラー毎にエラー構造体を定義する
- エラーは上位の呼び出し元で処理する
カスタムエラーを活用する
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する
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)
}
}
- 呼び出し元でどんなエラーが上がってくるかを把握する必要があってしんどい
- エラーの定義がめんどうくさい
参考
例外について考える
CleanArchitectureベースのAPIサーバにおいて、
- 例外が発生したことにより、正常に処理を終了することができない、あるいは、リクエストに応えられないことをユーザに伝えたい
- Controllerはusecaseを呼び出した際にどのような例外が発生したかを知る必要がある
- usecaseでは複数のエラーが発生する可能性がある
- エラーの種類によってレスポンスを変えたい
- エラーを区別したい
そもそも、
- なぜ例外が必要か?
- 正常に処理が継続できないことを知らせるため
- なぜ例外をハンドリングする必要があるのか?
- 例外によってその後の処理が変わるため
- ユーザ/開発者に伝える
- 無視して構わない内容なので無視する
- 例外によってその後の処理が変わるため
実装より
- ログは決まった場所ではきたい
- echoなら
CustomErrorHandler
を使いたい
- echoなら
- 下位から上位にいくにつれて、エラーの性質が変わる場合、適切な場所でエラーコードを変えるなどエラーの性質変化に対応したい
- 下位から上位にいくにつれて、エラーメッセージを追加で付与したい場合がある
カスタムエラー活用時の各関数の戻り値
func doSomething() error
or
func doSomething() *MyError
errorを返すのがお作法っぽい
とりあえず理解する
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とすると、この問題は発生しません。
より深く理解する
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は以下。
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)
}
}
■参考
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,
}
}
err := New("message", 1111)
参考
以下に記事化してクローズ