if err != nilがうんざりする理由
一言でいうと
Goはエラーを値として返します。そのためエラー処理を飛ばすことはできず、代わりにコードが長くなります。その長さはコストではなく、明示性の対価です。
なぜ必要なのか
例外のある言語では、どの行が例外を投げうるのかがコードを見てもわかりません。だから「ここで落ちる可能性はあるのか」を確認するには、呼び出しグラフをたどる必要があります。
Goはそれをシグネチャに書きます。
func ReadConfig(path string) (*Config, error)
この関数が失敗しうるという事実が型に含まれています。呼び出す側はそれを無視できません。無視するには_と書く必要があり、それは意図が表に出ます。
エラーのラップ
エラーをそのまま上へ返すと、「どこで起きたか」が失われます。%wでラップします。
if err != nil {
return fmt.Errorf("설정 읽기(%s): %w", path, err)
}
%wは元のエラーを中に抱えます。そのため、上位でerrors.Isやerrors.Asを使って原因を問い合わせられます。
if errors.Is(err, os.ErrNotExist) { ... }
var perr *os.PathError
if errors.As(err, &perr) { fmt.Println(perr.Path) }
%vでラップすると文字列だけが残り、この問い合わせができなくなります。ラップするときは常に%wです。
いつpanicするのか
ほとんどしません。panicは「プログラムが動き続ける理由がない」場合だけです。初期化の失敗や不変条件の違反がそれです。リクエスト処理中のpanicはそのリクエストだけを落とすべきなので、HTTPサーバーはデフォルトで捕捉してくれます。
よくある勘違い
「エラーをログに残し、さらに返す」: そうすると、同じ事故がログに何度も出力されます。処理するか、上へ返すか、どちらか1つだけにします。ログは最終的に処理する場所で1回だけ残します。
「nilのエラーなら結果は有効」: Goの慣例としては正しいですが、強制ではありません。ライブラリによっては、両方とも有効だったり、両方ともnilだったりします。ドキュメントを確認します。
センチネルエラーとカスタム型のどちらを使うのか
呼び出し側が何を知る必要があるかで決めます。
「この場合かどうか」だけわかればよいならセンチネルです。 パッケージレベルで値を1つ宣言し、
errors.Isで比較します。
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) { ... }
追加の情報が必要なら型です。 どのフィールドがなぜ誤っているのかといった情報は、値に入れておく 必要があります。
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Reason)
}
var verr *ValidationError
if errors.As(err, &verr) {
log.Printf("필드 %s 가 문제: %s", verr.Field, verr.Reason)
}
注意すべき落とし穴が1つあります。nilのカスタムエラーをerrorインターフェースに
入れると、err != nilが真になります。
func do() error {
var e *ValidationError // nil
...
return e // ❌ 인터페이스는 (타입, nil) 이라 nil 이 아니다
}
if do() != nil { /* 오류가 없는데 여기로 들어온다 */ }
具象型の変数をそのまま返さず、エラーがなければreturn nilと明示します。
Goでもっとも古い落とし穴で、今でも出てきます。
複数のエラーを一度に
Go 1.20から、errors.Joinで複数のエラーをまとめられます。バリデーションのように「全部集めて一度に
知らせる」必要がある場面に向いています。
var errs error
for _, f := range fields {
if err := f.Validate(); err != nil {
errs = errors.Join(errs, err)
}
}
return errs // 하나도 없으면 nil 이다
fmt.Errorfも%wを複数受け取れます。errors.Isはまとめられたものをすべてたどります。
層ごとに何を付けるのか
エラーをラップするときは、自分が知っていることだけを付けます。下の層がすでに言ったことを 繰り返すと、メッセージが長くなるだけです。
| 層 | 付けるもの | 付けないもの |
|---|---|---|
| リポジトリ | テーブル・キー | SQL全文(ログにのみ) |
| サービス | 何をしようとしたか | リポジトリの詳細 |
| ハンドラー | リクエスト識別子 | 内部構造 |
そして、ユーザーに送るメッセージとログに残すメッセージを分けます。 内部の パスがそのままHTTPレスポンスに出てしまうと、それが情報漏えいです。
if err != nil {
log.Printf("주문 조회 실패: %v", err) // 전체 사슬
http.Error(w, "일시적인 오류입니다", 500) // 사용자에게는 이것만
}
実務で本当に大切なこと
エラーメッセージは、上から下へ読んだときに経路になるように書きます。
사용자 조회(id=42): 설정 읽기(/etc/app.yaml): open /etc/app.yaml: no such file
各層が自分の知っている文脈を1つずつ付けると、このようになります。大文字で始めず、ピリオドで終えないのが慣例です。ほかのメッセージの中に埋め込まれるからです。