0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goのエラーハンドリングの設計思想を少し理解した

0
Last updated at Posted at 2025-12-01

要約

  • Goのエラーハンドリングでは、ライブラリの責務を「エラーが発生したという事実」と「その原因」を正確に伝えるところまでとし、エラーをどう扱うかはビジネスロジック側に委ねる、という設計方針がしばしば採用されているそう
  • fmt.Errorfはエラーに情報を付与できるがスタックトレースが取れない
  • github.com/cockroachdb/errorsを使うことでスタックトレースを取れる

一人アドベントカレンダー1日目です(夜中滑り込み💦)

背景

社内のコーディングルールで以下のようなものがありました。

- fmt.Errorfは使わない
- 代わりにgithub.com/cockroachdb/errorsを使う
  - ライブラリが返すエラーにはerrors.WithStackを使用する

cockroachdb/errorserrors.WithStackはerrorをラップすることができます。

なぜエラーをラップする必要があるのか?と思いました。

前提としてGoはエラーをthrowするのではなくreturnします。

Goのライブラリが返すエラーでスタックトレースが取れない例

今回はsamber/lovalidate関数で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/errorsWrapメソッドを使います。


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のエラーハンドリングでは、ライブラリの責務を「エラーが発生したという事実」と「その原因」を正確に伝えるところまでとし、エラーをどう扱うかはビジネスロジック側に委ねる、という設計方針がしばしば採用されているそう

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?