要約
- Goのエラーハンドリングでは、ライブラリの責務を「エラーが発生したという事実」と「その原因」を正確に伝えるところまでとし、エラーをどう扱うかはビジネスロジック側に委ねる、という設計方針がしばしば採用されているそう
-
fmt.Errorfはエラーに情報を付与できるがスタックトレースが取れない -
github.com/cockroachdb/errorsを使うことでスタックトレースを取れる
一人アドベントカレンダー1日目です(夜中滑り込み💦)
背景
社内のコーディングルールで以下のようなものがありました。
- fmt.Errorfは使わない
- 代わりにgithub.com/cockroachdb/errorsを使う
- ライブラリが返すエラーにはerrors.WithStackを使用する
cockroachdb/errorsのerrors.WithStackはerrorをラップすることができます。
なぜエラーをラップする必要があるのか?と思いました。
前提としてGoはエラーをthrowするのではなくreturnします。
Goのライブラリが返すエラーでスタックトレースが取れない例
今回はsamber/loのvalidate関数でerrorを返します。
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
str := ""
_, err := validateInput(str)
if err != nil {
fmt.Println("Validation error:", err)
return
}
fmt.Println("Hello, 世界")
}
func validateInput(str string) (bool, error) {
if err := lo.Validate(str != "", "str is required"); err != nil {
return false, err
}
return true, nil
}
これを実行すると以下が表示されます。
Validation error: str is required
エラー内容しかわからないです。
なぜこのようになっているのか見ていきます。
Validateはビルトインのerror型を返します。
ビルトインのerror型は以下のように定義されています。
type error interface {
Error() string //エラーの内容を文字列にする
}
fmt.Errorfの処理を見るとエラーの内容は返していますが、何行目で起きたかは返していません。
また、これだとValidate内の何行目でエラーが起きたか、main.go内の何行目でエラーが起きたかも分かりません。
つまりコードが複雑になった場合、エラーの特定が困難になります(スタックトレースが取れない)。
次にビルトインのerrorをラップしてエラーの発生箇所等の情報を付与します。
エラーをラップしてエラーの情報を付与する
cockroachdb/errorsのWrapメソッドを使います。
import (
"fmt"
"github.com/cockroachdb/errors"
"github.com/samber/lo"
)
func main() {
str := ""
_, err := validateInput(str)
if err != nil {
fmt.Printf("%+v\n", errors.WithStack(err))
return
}
fmt.Println("Hello, 世界")
}
これを実行すると以下が表示されます。
str is required
(1) attached stack trace
-- stack trace:
| main.main
| /Users/yamanetaisei/Desktop/github.com/yamatai12/golang-errors/main.go:14
| runtime.main
| /opt/homebrew/Cellar/go/1.25.3/libexec/src/runtime/proc.go:285
| runtime.goexit
| /opt/homebrew/Cellar/go/1.25.3/libexec/src/runtime/asm_arm64.s:1268
Wraps: (2) str is required
Error types: (1) *withstack.withStack (2) *errors.errorString
これを見るとmain.goの14行目でエラーが起こっていることが分かります。
Goのライブラリはなぜこのようなエラーを返すのか?
Goのエラーハンドリングでは、ライブラリの責務を「エラーが発生したという事実」と「その原因(エラー値そのもの)」を正確に伝えるところまでとし、エラーをどう扱うか(ログに出す、無視する、ユーザー向けメッセージに変換するなど)はビジネスロジック側に委ねる、という設計方針がしばしば採用されているそうです。
以下の記事やdocからそのように推測しました。
In Go, errors are values; they are created by code and consumed by code. Errors can be:
- Converted into diagnostic information for display to humans
- Used by the maintainer
- Interpreted by an end user
トップ(アプリ)レイヤの実装者がどんなログを出力したいのかを決める。
まとめ
-
fmt.Errorfはエラーに情報を付与できるがスタックトレースは作成できない -
github.com/cockroachdb/errorsを使うことでエラーに情報を付与してスタックトレースを作成してくれる - Goのエラーハンドリングでは、ライブラリの責務を「エラーが発生したという事実」と「その原因」を正確に伝えるところまでとし、エラーをどう扱うかはビジネスロジック側に委ねる、という設計方針がしばしば採用されているそう
参考