go-error-handling について
go-error-handlingは、Go開発者やコーディングエージェント向けの開発スキルです。エラーの作成、ラップ、伝播、公開のパターンを説明し、文脈、エラーチェーン、システム境界での処理を重視します。
主な機能
- 01センチネルエラーとカスタムエラーのパターン
- 02文脈の追加とエラーチェーンの維持
- 03ログ、エラー返却、panicの規則
- 04境界での変換と安定したメッセージ
- 05go vetとgolangci-lintによる検証
活用シーン
- 01呼び出し側が識別できるエラーの設計
- 02有用な文脈を添えた障害の伝播
- 03API境界でのエラー処理のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-error-handlingインストール後、Claude Code で/go-error-handlingを入力して読み込まれたか確認します。
同梱ファイルreferences/patterns-and-boundaries.md
詳細
Go のエラーハンドリング
Go の明示的なエラーハンドリングは、制約ではなく機能です。 これらのパターンにより、エラーをわかりやすく、対処可能で、適切に伝播できるようにします。
コミュニティのデフォルトです。
go-error-handlingを明示的に 優先するプロジェクトのスキルまたはエージェント設定ルールがある場合は、そちらが優先されます。
詳細なリファレンス資料は、必要に応じて読み込みます。
references/patterns-and-boundaries.md— エラーのチェックパターン、Isメソッド、HTTP 境界でのエラー変換、カーディナリティを抑えたログ記録、 ポインターエラーに対する Go 1.27 の%wの vet チェック。
以下の概要だけでは不十分な場合に限り、リファレンスファイルを読んでください。
1. エラーの判断ツリー
エラーを作成または返すときは、次のツリーに従ってください。
- 単純で、追加のコンテキストが不要ですか? →
errors.New("message") - 既存のエラーにコンテキストを追加する必要がありますか? →
fmt.Errorf("doing X: %w", err) - 呼び出し元がこのエラーを判別する必要がありますか? → センチネル
varまたはカスタム型 - エラーに構造化データが含まれますか? →
errorを実装するカスタム型 - 下流から伝播しますか? →
%wでラップし、コンテキストを追加します
2. センチネルエラー
呼び出し元がチェックする必要のあるエラーには、パッケージレベルの var を使用してください。
// ✅ Good — exported sentinel error
var (
ErrNotFound = errors.New("user: not found")
ErrUnauthorized = errors.New("user: unauthorized")
)
// Naming convention: Err + Description
// Prefix with package context in the message
呼び出し元は errors.Is でチェックします。
if errors.Is(err, user.ErrNotFound) {
// handle not found
}
エラーを == で比較してはいけません。常に errors.Is() を使用してください。
3. カスタムエラー型
エラーに構造化情報を含める必要がある場合は、次のようにします。
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: field %s: %s", e.Field, e.Message)
}
// Callers extract it from the chain — Go 1.26+:
if valErr, ok := errors.AsType[*ValidationError](err); ok {
log.Printf("invalid field: %s", valErr.Field)
}
// Before Go 1.26:
var valErr *ValidationError
if errors.As(err, &valErr) {
log.Printf("invalid field: %s", valErr.Field)
}
原因をラップするカスタム型は、その原因を公開する必要があります。そうしないと、errors.Is/errors.As
はその型で探索を止め、内部のエラーを確認できません。
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return "query " + e.Query + ": " + e.Err.Error() }
func (e *QueryError) Unwrap() error { return e.Err } // keeps the chain walkable
4. エラーのラップ
エラーをスタック上位へ伝播するときは、必ずコンテキストを追加してください。
エラーチェーンを維持するには %w を使用します。
// ✅ Good — context added, chain preserved
func getUser(id int64) (*User, error) {
row, err := db.QueryRow(ctx, query, id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
// ...
}
// ❌ Bad — no context
return nil, err
// ❌ Bad — chain broken, callers can't errors.Is/As
return nil, fmt.Errorf("failed: %v", err)
%w を使用しない場合
呼び出し元が内部実装のエラーに依存しないよう、エラーチェーンを明示的に切断したい場合は、%w ではなく %v を使用します。
// Intentionally hiding internal DB error from public API
return nil, fmt.Errorf("user lookup failed: %v", err)
5. エラーを処理するのは一度だけ
エラーはログに記録するか、返すかのどちらかにしてください。両方行ってはいけません。
// ✅ Good — return the error, let caller decide
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
// ...
}
// ❌ Bad — log AND return (error handled twice)
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
log.Printf("failed to read config: %v", err) // handled once
return nil, err // handled again
}
// ...
}
ルール:エラーへの対処を決定するコンポーネントが、そのエラーのログ記録やメトリクスの記録を行います。それ以外のコンポーネントは、エラーをラップして返します。
6. エラーの命名規則
// Sentinel errors: Err prefix
var ErrNotFound = errors.New("not found")
// Error types: Error suffix
type NotFoundError struct { ... }
type ValidationError struct { ... }
// Error messages: lowercase, no punctuation, no "failed to" prefix
// Include context: "package: action: detail"
errors.New("auth: token expired")
fmt.Errorf("user: get by id %d: %w", id, err)
7. パニックのルール
パニックはエラーハンドリングではありません。パニックを使用するのは、次の場合に限ってください。
- プログラムの初期化に失敗し、処理を続行できない場合(
template.Must、フラグの解析) - 起きてはならないプログラマーのミス(不変条件への違反)
- ランタイムの状況ではなく、バグを示す nil 参照
テストでは panic を使わず、t.Fatal/t.FailNow を使用してください。
HTTP ハンドラーとミドルウェアでは、リクエストによってサーバー全体がクラッシュするのを防ぐため、境界でパニックを回復してください。
8. エラーのチェックパターン
- エラー以外に必要なものがない場合は、
ifのスコープ内にエラーを収めます。if err := do(); err != nil { return fmt.Errorf("do: %w", err) }。 - 名前付きの
err戻り値に対する遅延ラップを使うと、長い関数のすべての戻り経路に同じプレフィックスを追加できます。 - 独立した失敗(検証、並列処理)を収集し、
errors.Join(errs...)を返します。errors.Is/Asは結合された各エラーを確認します。
9. 境界でのエラー
- エラーの公開は契約です。 モジュール内部では
%wでラップします。 公開する境界(エクスポートされたライブラリ API、RPC)では、%wによってラップされたエラーが API の一部になります。%vまたは新しいエラーを使うと、エラーを非公開にできます。 - ユーザーに届く前に変換します。 ハンドラーでドメインエラーをステータスと安定したメッセージにマッピングし、元のエラーをチェーンとともに一度だけログに記録します。レスポンスに
err.Error()を含めると、SQL、パス、ホスト名が漏えいします。 - ログメッセージのカーディナリティを抑えます。 安定したテキストをメッセージにし、ID、パス、件数は構造化属性に含めます。これにより、ログバックエンドで同一の失敗をグループ化できます。
- パニックはゴルーチンを越えません。
recoverが機能するのは、パニックが発生したゴルーチン内だけです。go-concurrency-review を参照してください。 - 型に
Is(target error) boolを実装すると、errors.Isで、あるセンチネルに対してエラーの一群を照合できます。
セクション 8 と 9 の例:references/patterns-and-boundaries.md。
実行可能な検証
エラー処理を記述またはレビューした後、上記のルールを確認するリンターを実行してください (インストールされていない場合はスキップし、その旨を記載してください)。
go vet ./... # includes printf %w misuse
golangci-lint run --enable errcheck,errorlint # unchecked errors, %v-vs-%w,
# == comparisons on errors
検証チェックリスト
_でエラーを破棄していない(コメントで明示的に正当化されている場合を除く)fmt.Errorfによるラップではすべて%wを使用している(または理由を文書化したうえで%vを使用している)- センチネルエラーに
var Err...の命名規則を使用している - カスタムエラー型が
errorインターフェースを実装している - 呼び出し元が
errors.Is/errors.As(Go 1.26 以降ではerrors.AsType)を使用している(==や型アサーションは使用していない) - 原因をラップするカスタム型が
Unwrap() errorを実装している - ログ出力してからエラーを返すパターンがない
- エラーメッセージが小文字で、文脈があり、エラーチェーンに適している
- ユーザーへの応答に
err.Error()のテキストを含めていない。ログメッセージは安定しており、詳細は属性として記録されている