🤔
Goにおけるerrors.Newとfmt.Errorfの違い
アドベントカレンダーの記事になります!
今回、レビューでコメントいただき個人的にも気になったため改めて調べてみました。
Go には代表的なエラー生成手段として errors.New と fmt.Errorf が存在します。
どちらも error を返しますが、どのような違いがあるのか自分の理解をまとめます。
1. errors.New:最軽量な固定エラー
内部実装(簡略)
func New(text string) error {
return &errorString{text}
}
- 単なる文字列を保持する構造体
- エラー原因(cause)を持たない
- ラップ不可
- 非常に軽量
特徴
- メモリ割り当てが最小
- フォーマット処理なし
- インライン化されやすい
- 高パフォーマンス
主な用途
- Sentinel error(比較可能な固定エラー)
- 静的なバリデーションエラー
- ホットパスでのエラー生成
例:
var ErrNotFound = errors.New("not found")
2. fmt.Errorf:文脈付きエラー生成とラップ
fmt.Errorf には 2つの役割があります。
1. フォーマット付きエラー生成
fmt.Errorf("invalid id: %d", id)
- 動的な値を含められる
- 内部的には文字列エラー(errorString)と同等
- ただしフォーマット処理分コストは増加
2. エラーラップ(Go1.13+)
fmt.Errorf("failed to save user: %w", err)
- 内部に元エラーを保持
- errors.Unwrap / Is / As が使用可能
- 文脈を積み重ねられる
これは Goにおける「スタックトレースの代替」 として機能します。
3. 技術的比較
| 観点 | errors.New | fmt.Errorf |
|---|---|---|
| 固定メッセージ | ✅ | ✅ |
| 動的メッセージ | ❌ | ✅ |
| エラーラップ | ❌ | ✅(%w) |
| cause チェーン | ❌ | ✅ |
| errors.Is / As | 限定的 | ✅ |
| パフォーマンス | ◎ 非常に高速 | △ フォーマット分遅い |
| メモリ使用量 | 最小 | 多め |
| Sentinel error | ◎ | ❌ |
4. パフォーマンス観点(参考)
ベンチマーク結果
| ベンチマーク | 実行回数 | 実行時間 | メモリ | アロケーション |
|---|---|---|---|---|
| errors.New | 121,718,304 | 9.28 ns/op | 16 B/op | 1 allocs/op |
| fmt.Errorf (静的) | 41,594,394 | 28.88 ns/op | 40 B/op | 2 allocs/op |
| fmt.Errorf (%d) | 24,589,932 | 48.46 ns/op | 56 B/op | 3 allocs/op |
| fmt.Errorf (%s) | 37,033,606 | 32.70 ns/op | 40 B/op | 2 allocs/op |
| fmt.Errorf (%w) | 24,662,010 | 48.38 ns/op | 56 B/op | 2 allocs/op |
%sと%dで実行時間に差分があった
-
%s(文字列)の処理
文字列 → そのままコピー
- 文字列はすでにバイト列なので、メモリをコピーするだけ
-
%d(整数)の処理
整数 → 各桁を計算 → 文字に変換 → 文字列を構築
- 例: 12345 → '1','2','3','4','5' に分解が必要
- 割り算とモジュロ演算を繰り返す
アロケーション数の違い
フォーマット アロケーション 理由 %s 2 allocs エラー構造体 + 最終文字列 %d 3 allocs 上記 + 整数→文字列変換用バッファ この変換処理の差が違いになっていると思われます。
5. 使い分け
errors.Newを使うべきケース
- 固定メッセージ
- Sentinel error
- バリデーションエラー
- 高頻度・高性能が求められる箇所
return errors.New("userId is required")
fmt.Errorfを使うべきケース
- 動的な情報を含めたい
- エラーの文脈を追加したい
- 原因エラーを保持したい(%w)
return fmt.Errorf("failed to create task: %w", err)
6. Goのエラー設計思想との整合性
Goは以下を意図的に採用しています:
- 例外・スタックトレースを持たない
- error は軽量な「値」
- 文脈は 呼び出し側が明示的に積む
その結果生まれたのが:
- %w による 明示的な文脈チェーン
- errors.Is / As による 構造化エラー処理
これは「暗黙の例外」ではなく
明示的で追跡可能なエラーフローを実現します。
7. まとめ
普段の開発では、そこまでシビアなパフォーマンスを意識する場面も少なく、エラーを動的に生成する必要があるケースも限定的です。
そのため、自分の環境ではerrors.Newでもfmt.Errorfでも大きな違いは感じませんでした。
とはいえ、エラーの意味や責務を明確にしたい場面では、適切に使い分ける意識は持っておきたいと感じました。
今回、調べてみようと思ったきっかけはレビューの中での指摘だったので、個人的にひっかかったポイントやレビューで指摘あったポイントなど、普段の小さなきっかけから気になるポイントを見つけて調べていく意識を持ちたいと思いました。
参考リンク
Discussion