Go中級 #2 エラー処理パターン
基礎#4でerrorの基本を見たなら、今回の記事はその上に実戦的なパターンを載せます — wrapping、検査、カスタム型、そしてpanicが似合う場面まで。
エラーwrapping — %w
#
fmt.Errorfの%w動詞が中核です。元のエラーを内側に挟み込んだ新しいエラーを作ります。
func readConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("config 読み込み失敗 (%s): %w", path, err)
}
// ...
return nil
}この新しいエラーは:
- メッセージがより詳しくなる(
config 読み込み失敗 (...): open ...: no such file...) - 元のエラーを内側に持っている —
errors.Is/errors.Asで取り出せる
%v vs %wの違い
#
err1 := fmt.Errorf("失敗: %v", origErr) // メッセージだけ結合 — 元のエラー情報を失う
err2 := fmt.Errorf("失敗: %w", origErr) // 元のエラーを保存 + メッセージ%vは単純な文字列結合。%wは内側に元のエラーを保存。エラーを上に流すときはほぼ常に%wを使います。
errors.Is — sentinelエラー比較
#
事前に定義されたエラーと比較するとき。
import "errors"
var ErrNotFound = errors.New("not found")
func lookup(key string) (string, error) {
if key == "" {
return "", fmt.Errorf("lookup: %w", ErrNotFound)
}
return "value", nil
}
func main() {
_, err := lookup("")
if errors.Is(err, ErrNotFound) {
fmt.Println("見つからない")
}
}errors.Isはエラーチェーンをたどりながら比較します。wrappingされたエラーの中の元のエラーまで検査します。
err == ErrNotFoundとの違いは — 単純な==はwrapされたエラーの中を見られません。%wで包んだ場合はerrors.Isが正解です。
errors.As — カスタムエラー型を取り出す
#
エラーが特定の型のインスタンスかを確認し、合っていればその型として変数に格納します。
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}func processForm(form Form) error {
if form.Email == "" {
return &ValidationError{Field: "email", Message: "必須です"}
}
return nil
}
func main() {
err := processForm(form)
var verr *ValidationError
if errors.As(err, &verr) {
fmt.Printf("フィールド %s 検証失敗: %s\n", verr.Field, verr.Message)
}
}errors.As(err, &verr)が中核:
- 第1引数 — 検査するエラー
- 第2引数 — 結果を受け取るポインタへのポインタ
チェーンをたどってその型のエラーを見つければ — その値をverrに格納してtrueを返します。その後はverrのフィールド(Field, Message)にアクセス可能です。
カスタムエラー型 — いつ作るか #
次の場面で似合います。
- 追加情報が必要なとき — Field、StatusCode、Retryableのようなメタデータ
- 呼び出し側が異なる処理をしなければならないとき — 検証エラーはユーザーに見せ、DBエラーはロギングして一般メッセージ
type HTTPError struct {
StatusCode int
Message string
URL string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d %s (%s)", e.StatusCode, e.Message, e.URL)
}
// 呼び出し側
var httpErr *HTTPError
if errors.As(err, &httpErr) {
if httpErr.StatusCode == 404 {
// 別の処理
}
}Sentinelエラー — 単純な比較 #
値として定義されたエラー(メタデータなし)。
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
ErrAlreadyExists = errors.New("already exists")
)
if errors.Is(err, ErrNotFound) {
// ...
}標準ライブラリでよく見るパターン — io.EOF、sql.ErrNoRowsなど。
Sentinel vs カスタム型 — どちらをいつ #
| sentinel | カスタム型 | |
|---|---|---|
| 追加情報の必要性 | なし | あり |
| 比較方法 | errors.Is | errors.As |
| 定義の短さ | ✓ | フィールド定義が必要 |
| 外部公開時の互換性 | 単純 | インターフェース/型公開 |
単純な分岐にはsentinel、メタデータが必要ならカスタム型。
エラーをそのまま返すか、wrapするか #
ガイド:
// エラーを上にそのまま → ひとまず wrap (どこで発生したかの手がかりを追加)
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("config 読み込み: %w", err)
}
// または — 呼び出し側に明確なメッセージならそのまま
if err != nil {
return err
}よく使うガイド — 層の境界では wrap、同じ層の中ではそのまま流してもOK。
// 良くない — あまりに多くの箇所で wrap
return fmt.Errorf("foo: %w", err)
return fmt.Errorf("bar: %w", err)
return fmt.Errorf("baz: %w", err)
// 結果: foo: bar: baz: 本物のエラー毎回wrapするとメッセージが長くなり意味がぼやけます。意味のある境界(関数の入り口、パッケージ境界)でだけwrapするのがすっきりします。
エラー比較の罠 — == vs errors.Is
#
var ErrFoo = errors.New("foo")
func wrap() error {
return fmt.Errorf("wrap: %w", ErrFoo)
}
err := wrap()
err == ErrFoo // false ← wrap されているので
errors.Is(err, ErrFoo) // true ← チェーン検査基礎でif err != nilだけ使うときは単純比較で十分でしたが、sentinelエラーを検査するときはほぼ常にerrors.Is。これがモダンな標準です。
errors.Join — 複数のエラーをまとめる
#
Go 1.20+のツール。複数のエラーが同時に発生したとき。
import "errors"
func validate(form Form) error {
var errs []error
if form.Name == "" {
errs = append(errs, fmt.Errorf("name 欠落"))
}
if form.Email == "" {
errs = append(errs, fmt.Errorf("email 欠落"))
}
if len(form.Password) < 8 {
errs = append(errs, fmt.Errorf("password 8文字以上"))
}
return errors.Join(errs...)
}
err := validate(form)
if err != nil {
fmt.Println(err) // すべてのエラーが改行で結合されて出力
}errors.Joinはnilのエラーを自動で除外し、すべてnilならnilを返します。
errors.Isとも動作します — まとまりの中にErrFooがあればerrors.Is(joined, ErrFoo)がtrue。
Panic — 本当に異常な状況だけに #
panicは他の言語のthrowに似ていますが — Goではほとんど使いません。
func divide(a, b int) int {
if b == 0 {
panic("0 で除算")
}
return a / b
}panicが起きると:
- 関数が即座に終了
- deferたちが実行(LIFO)
- 呼び出し側も終了、またその呼び出し側… 結局プログラム全体が終了
Goの慣習 — エラーはほぼ常にerror戻り値。panicは次の場面だけに似合います。
- プログラムが続行してはならない本当に間違った状態 — メモリ不足、前提違反
- 自分のパッケージの中だけで — 外部の呼び出し側には公開しない(recoverで捕捉してerrorに変換)
recover — panicから生き残る
#
defer関数の中でrecoverを呼べばpanicを捕捉できます。
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return divide(a, b), nil
}recover()はpanicが進行中ならその値を返してpanicを止めます。defer関数の中でだけ意味があります。
ライブラリ境界で — 内部panicが外部に漏れないようにする場面に似合います。一般のアプリコードではほとんど出会わないツールです。
errors.Unwrap — 直接ほどく
#
たまにwrapされたエラーを一段だけほどいて見たいとき。
err := fmt.Errorf("outer: %w", fmt.Errorf("middle: %w", io.EOF))
inner := errors.Unwrap(err) // middle: EOF
inner2 := errors.Unwrap(inner) // EOF
inner3 := errors.Unwrap(inner2) // nil (これ以上中はない)直接使う機会は少ないです — errors.Is/Asが内部でやってくれます。デバッグや特殊な場面でだけ。
標準ライブラリでよく出会うエラー #
io.EOF // 入力の終わり
io.ErrUnexpectedEOF // 予期せぬ終わり
sql.ErrNoRows // SQL 結果なし
context.Canceled // コンテキストキャンセル
context.DeadlineExceeded // タイムアウト
os.ErrNotExist // ファイルなし
fs.ErrPermission // 権限なしこれらに出会ったらerrors.Is(err, io.EOF)のように検査。標準ライブラリ関数のエラーは通常こうしたsentinelをwrapして返します。
よく使うパターン #
1) 関数の入り口でコンテキストwrap #
func processOrder(orderID string) error {
if err := validateOrder(orderID); err != nil {
return fmt.Errorf("processOrder: %w", err)
}
if err := saveOrder(orderID); err != nil {
return fmt.Errorf("processOrder: %w", err)
}
return nil
}関数名をprefixに — 呼び出しスタックを追跡しやすく。
2) 即座に返す (early return) #
data, err := fetch()
if err != nil { return err }
parsed, err := parse(data)
if err != nil { return err }
if err := save(parsed); err != nil { return err }深いifではなくフラットに。Goコードのほぼ標準的な形です。
3) 一部のエラーは無視 #
defer file.Close() // 閉じる失敗は通常無視 (成功しても大した意味なし)
f.WriteString("hi") // 戻りエラー無視 (些細なケース)_ = ...で明示的な無視も可能(linter警告回避)。
_ = file.Close()まとめ #
今回の記事で整理した内容:
%wでエラーwrapping — 元のエラーを保存errors.Isでsentinelエラー比較(チェーン検査)errors.Asでカスタム型を取り出し- 追加情報が必要ならカスタム型、単純な分岐はsentinel
- 関数/パッケージ境界でwrap、毎回の過剰なwrapは回避
errors.Joinで複数のエラーをまとめる(Go 1.20+)- panicは本当に異常な状況だけ — 通常のエラーはerror戻り値
recoverはライブラリ境界程度
次の記事(#3 ゴルーチンとチャネル入門)ではGoの最も強力な武器 — 並行性。軽量なゴルーチンとチャネルを最初から整理します。