KoderKoder.ai
料金エンタープライズ教育投資家向け
ログインはじめる

プロダクト

料金エンタープライズ投資家向け

リソース

お問い合わせサポート教育ブログ

リーガル

プライバシーポリシー利用規約セキュリティ利用ポリシー不正利用を報告

ソーシャル

LinkedInTwitter
Koder.ai
言語

© 2026 Koder.ai. All rights reserved.

ホーム›ブログ›明確で一貫したレスポンスのための Go API のエラー処理パターン
2025年9月29日·2 分

明確で一貫したレスポンスのための Go API のエラー処理パターン

Go API のエラー処理パターン:型付きエラー、HTTP ステータスのマッピング、request ID、内部を漏らさない安全なメッセージを標準化する方法。

明確で一貫したレスポンスのための Go API のエラー処理パターン

なぜ一貫性のない API エラーはクライアントを苛立たせるのか

各エンドポイントが失敗をバラバラに報告すると、クライアントは API を信用できなくなります。あるルートは { "error": "not found" } を返し、別は { "message": "missing" } を返し、さらに別はプレーンテキストを返す。意味は近くても、クライアント側のコードは何が起きたのかを推測しなければならなくなります。

コストはすぐに現れます。チームは壊れやすいパースロジックを作り、エンドポイントごとに特別扱いを追加します。リトライは「後で再試行すべき」か「入力が間違っている」かを区別できないため危険になります。サポートチケットは増え、クライアントに表示されるメッセージは曖昧で、サーバ側のログ行と一致させるのが難しくなります。

よくあるシナリオ:モバイルアプリがサインアップで3つのエンドポイントを呼ぶとします。最初はフィールドレベルのエラーマップで HTTP 400 を返し、2つ目はスタックトレース文字列付きで HTTP 500 を返し、3つ目は { "ok": false } で HTTP 200 を返す。アプリチームは3つの異なるエラーハンドラを用意し、バックエンドチームには「サインアップが時々失敗する」とだけ報告されて、どこから始めればいいか分かりません。

目標は、すべてのエンドポイントが従う予測可能な契約を作ることです。クライアントは、自分の責任かサーバの問題か、リトライが意味を持つか、サポートに貼り付けるリクエスト ID が分かるべきです。

スコープ注記:ここでは JSON HTTP API(gRPC ではない)に焦点を当てますが、同じ考え方は他のシステムにエラーを返す場合にも当てはまります。

シンプルな目標:すべてのエンドポイントが従う1つの契約

エラーに対して1つの明確な契約を選び、すべてのエンドポイントで守らせてください。「一貫性がある」とは、同じ JSON 形、同じフィールドの意味、どのハンドラが失敗しても同じ動作をすることを意味します。そうすればクライアントは推測をやめてエラーを正しく扱えます。

有用な契約はクライアントが次に何をすべきか判断するのに役立ちます。多くのアプリでは、すべてのエラー応答が次の3つの質問に答えるべきです:

  • 自分の入力を修正できるか?
  • 後で再試行すべきか?
  • サポートに連絡する必要があるか?

実用的なルールセット:

  • すべてのエラーに対して1つのレスポンススキーマを使う。
  • 1つのステータスコードポリシー(同じエラー型は常に同じ HTTP ステータスにマップする)。
  • 1つの安全なメッセージポリシー(ユーザーに見せるものと内部に留めるもの)。
  • 1つの相関フック(サポートが失敗を見つけられるように request ID を返す)。

レスポンスに絶対に出してはいけないものを事前に決めてください。一般的な「絶対出すな」項目には SQL 断片、スタックトレース、内部ホスト名、秘密情報、依存先からの生のエラーメッセージなどがあります。

クリーンな分離を保ってください:短いユーザー向けメッセージ(安全で丁寧、実行可能)と内部詳細(完全なエラー、スタック、コンテキスト)はログに残す。例えば「変更を保存できませんでした。もう一度お試しください。」は安全です。一方で「pq: duplicate key value violates unique constraint users_email_key」は表示すべきではありません。

すべてのエンドポイントが同じ契約に従えば、クライアントは1つのエラーハンドラを作ってどこでも使い回せます。

クライアントが頼れるエラー応答スキーマを定義する

クライアントがエラーをきちんと処理できるのは、すべてのエンドポイントが同じ形で答える場合だけです。1つの JSON 封筒を選び、安定させてください。

実用的なデフォルトは error オブジェクトとトップレベルの request_id です:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields are invalid.",
    "details": {
      "fields": {
        "email": "must be a valid email address"
      }
    }
  },
  "request_id": "req_01HV..."
}

HTTP ステータスは大きなカテゴリ(400, 401, 409, 500)を示します。機械可読な error.code はクライアントが分岐するための具体的なケースを与えます。この分離は重要です。なぜなら多くの異なる問題が同じステータスを共有するからです。モバイルアプリは EMAIL_TAKEN と WEAK_PASSWORD が両方 400 であっても異なる UI を表示するかもしれません。

error.message は安全で人間向けの文にしてください。ユーザーが問題を直せる手助けをしつつ、内部情報(SQL、スタックトレース、プロバイダ名、ファイルパス)を漏らしてはいけません。

オプションフィールドは予測可能であれば便利です:

  • 検証エラー:details.fields をフィールド→メッセージのマップにする。
  • レート制限や一時的な問題:details.retry_after_seconds。
  • 追加の案内:details.docs_hint をプレーンテキストで(URL ではない)。

後方互換性のために、error.code の値を API 契約の一部として扱ってください。古い意味を変えずに新しいコードを追加し、オプションフィールドだけを追加することでクライアントは未認識のフィールドを無視できます。

Go の型付きエラー:ハンドラのためのシンプルなモデル

エラー処理は各ハンドラが独自の失敗シグナルを作ると混乱します。小さな型付きエラーセットがこれを解決します:ハンドラは既知のエラー型を返し、1つのレスポンス層がそれらを一貫した応答に変換します。

実用的なスターターセットは多くのエンドポイントをカバーします:

  • ValidationError(不正な入力)
  • NotFoundError(リソースが見つからない)
  • ConflictError(ユニーク制約、状態不整合)
  • UnauthorizedError(未認証または権限なし)
  • InternalError(それ以外すべて)

重要なのはトップレベルでの安定性です。根本原因が変わっても公開する型は安定させます。低レベルのエラー(SQL、ネットワーク、JSON 解析)をラップしても、ミドルウェアは同じ公開型を検出できます。

type NotFoundError struct {
	Resource string
	ID       string
	Err      error // private cause
}

func (e NotFoundError) Error() string { return "not found" }
func (e NotFoundError) Unwrap() error { return e.Err }

ハンドラ内では、sql.ErrNoRows を直接漏らすのではなく NotFoundError{Resource: "user", ID: id, Err: err} を返してください。

エラーのチェックには errors.As を優先し、シンボルエラーには errors.Is を使ってください。単純なケースにはセンチネルエラー(例:var ErrUnauthorized = errors.New("unauthorized"))が使えますが、公開用の安全なコンテキスト(どのリソースが見つからなかったかなど)が必要な場合はカスタム型が優れています。

添付する情報を厳格に区別してください:

  • 公開(クライアントに安全):短いメッセージ、安定したコード、場合によっては検証フィールド名。
  • 非公開(ログのみ):元の Err、スタック情報、生の SQL エラー、トークン、ユーザーデータ。

その分離により、クライアントを助けつつ内部を晒さずに済みます。

エラー型を HTTP ステータスに一貫してマッピングする

型付きエラーがあれば、次の作業は地味ですが重要です:同じエラー型は常に同じ HTTP ステータスを返すようにします。クライアントはそれに基づいてロジックを構築します。

多くの API に適した実用的なマッピング:

Error type (example)StatusWhen to use it
BadRequest (malformed JSON, missing required query param)400The request is not valid at a basic protocol or format level.
Unauthenticated (no/invalid token)401The client needs to authenticate.
Forbidden (no permission)403Auth is valid, but access is not allowed.
NotFound (resource ID does not exist)404The requested resource is not there (or you choose to hide existence).
Conflict (unique constraint, version mismatch)409The request is well-formed, but it clashes with current state.
ValidationFailed (field rules)422The shape is fine, but business validation fails (email format, min length).
RateLimited429Too many requests in a time window.
Internal (unknown error)500Bug or unexpected failure.
Unavailable (dependency down, timeout, maintenance)503Temporary server-side issue.

混乱を防ぐための2つの区別:

  • 400 と 422:リクエストが確実に解釈できない場合(不正な JSON、型違い)は 400 を使い、解析できるが値が受け入れられない場合は 422 を使う。
  • 409 と 422:422 はフィールドレベルの検証(パスワードが短いなど)に、409 はデータは有効だが状態の衝突(メール既存在、注文がすでに出荷済み、楽観ロック失敗)に使う。

リトライの指針:

  • 一般的に再試行が安全なもの:503、および場合によっては 429(待機後)。
  • 変更なしに再試行しても意味がないもの:400, 401, 403, 404, 409, 422。
  • 操作が冪等であれば(同じボディの PUT、または idempotency key を伴う POST)一時的な失敗に対するリトライはより安全になります。

リクエスト ID:クライアント問題を素早くデバッグする最短路

Build a Go API faster
Describe your Go API and generate handlers that follow one error response shape.
Start Building

リクエスト ID は API 呼び出し一回を一意に識別する短い値です。クライアントが各レスポンスでそれを見られれば、サポートは「リクエスト ID を送ってください」で正確なログと失敗を見つけられます。

この習慣は成功応答と失敗応答の両方で有益です。

生成と伝播のルール

明確なルールを1つ使ってください:クライアントが送ってきた ID はそのまま使う。送ってこなければ新しく作る。

  • 単一のヘッダ名で受け入れる(1つ選んでドキュメント化する。例:X-Request-Id)。
  • ヘッダがなければミドルウェアなどエッジで新しい ID を生成し、リクエストのコンテキストに添付する。
  • リクエスト中に ID を変更しない。下流呼び出し(DB、他サービス)へはコンテキストまたはヘッダ経由で渡す。

リクエスト ID は3か所に入れてください:

  • レスポンスヘッダ(受け入れるのと同じヘッダ名)
  • レスポンスボディ(標準スキーマの request_id)
  • ログ(すべてのログ行に構造化フィールドとして)

バッチや非同期処理

バッチエンドポイントやバックグラウンドジョブでは親リクエスト ID を保ってください。例:クライアントが200行をアップロードして12行が検証で失敗し、処理をキューに入れる場合、全体の呼び出しに対して1つの request_id を返し、各ジョブやアイテムごとのエラーに parent_request_id を含めます。こうすることで、多数にファンアウトしても「1回のアップロード」を追跡できます。

内部を漏らさないログとメトリクス

クライアントは明確で安定したエラー応答を必要とします。あなたのログは煩雑な真実を必要とします。これら2つの世界を分けてください:クライアントには安全なメッセージと公開エラーコードを返し、サーバログには内部の原因、スタック、コンテキストを残します。

エラー応答ごとに構造化されたイベントを1つログに残し、request_id で検索できるようにしてください。

一貫して残すと役立つフィールド:

  • request_id
  • user_id または account_id(認証されている場合)
  • public error code と HTTP ステータス
  • handler/route 名とメソッド
  • internal error detail(ラップされた原因、検証フィールドエラー、上流のタイムアウト)

内部の詳細はサーバログ(または内部のエラーストア)だけに保存してください。クライアントは生のデータベースエラー、クエリテキスト、スタックトレース、プロバイダメッセージを決して見てはいけません。複数サービスを運用しているなら、内部フィールド source(api, db, auth, upstream など)を付けるとトリアージが速くなります。

ノイズの多いエンドポイントやレート制限されたエラーを監視してください。あるエンドポイントが毎分何千回も同じ 429 や 400 を出すなら、ログスパムを避けるためにイベントのサンプリングや重大度を下げつつメトリクスではカウントし続けるなどの対策を取りましょう。

メトリクスはログより早く問題を検出します。HTTP ステータスとエラーコードごとのカウントを追跡し、急増をアラートにしてください。デプロイ後に RATE_LIMITED が 10 倍になれば、ログがサンプリングされていてもすぐに気づけます。

ステップバイステップ:Go で一貫したエラーパイプラインを実装する

Own the backend code
Keep full control by exporting the generated Go source code anytime.
Export Source

エラーを一貫させる最も簡単な方法は、あちこちで扱うのをやめて小さなパイプラインに集約することです。そのパイプラインがクライアントに見せる内容とログ用に残す内容を決めます。

パイプラインの実務的な5ステップ

まずクライアントが依存できる小さなエラーコードセット(例:INVALID_ARGUMENT, NOT_FOUND, UNAUTHORIZED, CONFLICT, INTERNAL)から始めます。これらを、公開可能な安全なフィールド(code、safe message、場合によってはどのフィールドが間違っているか)だけを露出する型付きエラーでラップします。内部の原因は非公開にします。

次に任意のエラーを (statusCode, responseBody) に変換する1つのトランスレータ関数を実装します。ここで型付きエラーを HTTP ステータスにマッピングし、不明なエラーは安全な 500 応答にします。

次にミドルウェアを追加します:

  • すべてのリクエストに request_id を確実に付与する
  • panic をリカバーする

panic がクライアントにスタックトレースを投げてはいけません。通常の 500 応答を返し、同じ request_id で完全な panic をログに残してください。

最後にハンドラを変更して、直接レスポンスを書き込むのではなく error を返すようにします。ラッパーがハンドラを呼び、トランスレータを実行して標準フォーマットの JSON を書き込むようにします。

簡潔なチェックリスト:

  • 公開用の安全なフィールドと安定したコードを持つ型付きエラーを定義する。
  • エラーをステータスとレスポンス JSON に1箇所で翻訳する。
  • request ID と panic 回復のミドルウェアを追加する。
  • ハンドラはエラーを返し、レスポンスは共通のラッパーで処理する。
  • トランスレータとラッパーに対するゴールデンテストを追加する。

ゴールデンテストは契約を固定するため重要です。誰かが後でメッセージやステータスコードを変えると、テストで失敗してクライアントが驚く前に検出できます。

例:1つのエンドポイント、3つの失敗、予測可能な応答

顧客レコードを作るエンドポイントを想像してください。

POST /v1/customers に { "email": "[email protected]", "name": "Pat" } のような JSON を送ると、サーバは常に同じエラー形を返し、常に request_id を含めます。

1) 検証エラー(400)

メールがないかフォーマットが不正で、クライアントはフィールドをハイライトできます。

{
  "request_id": "req_01HV9N2K6Q7A3W1J9K8B",
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields need attention.",
    "details": {
      "fields": {
        "email": "must be a valid email address"
      }
    }
  }
}

2) 衝突(409)

メールが既に存在する場合。クライアントはサインインを提案するか別のメールを選ぶよう案内できます。

{
  "request_id": "req_01HV9N3C2D0F0M3Q7Z9R",
  "error": {
    "code": "ALREADY_EXISTS",
    "message": "A customer with this email already exists."
  }
}

3) 一時的な障害(503)

依存先がダウンしている場合。クライアントはバックオフして再試行を促し、落ち着いたメッセージを表示できます。

{
  "request_id": "req_01HV9N3X8P2J7T4N6C1D",
  "error": {
    "code": "TEMPORARILY_UNAVAILABLE",
    "message": "We could not save your request right now. Please try again."
  }
}

1つの契約があればクライアントは一貫して反応できます:

  • 400:details.fields を使ってフィールドをマークする
  • 409:ユーザーに安全な次の手順を案内する
  • 503:リトライを促し、request_id をサポート ID として表示する

サポートでは同じ request_id が内部ログで本当の原因を見つける最速パスになります。内部のスタックトレースや DB エラーを露出せずに済みます。

エラー処理を悪化させるよくある落とし穴

クライアントを苛立たせる最速の方法は推測を強いることです。あるエンドポイントが { "error": "..." } を返し、別が { "message": "..." } を返すと、クライアントは特例の山になり、バグが何週間も隠れます。

繰り返し出る間違い:

  • 成功を示す 200 とエラーを同じにして、エラーをボディ内に隠す。エンドポイント間でエラースキーマを切り替える。
  • ユーザーメッセージに内部情報を出す(SQL エラー、スタックトレース、IP、依存先ホスト名、ファイルパス)。
  • 安定した code の代わりに人間向けテキストだけを識別子として使う。
  • エラーコードを軽率に変更したり、異なる問題に同じコードを再利用してクライアントを壊す。
  • 失敗時だけ request_id を付ける(成功時に付けない)ので相関ができない。

内部を漏らすのは一番簡単に陥る罠です。ハンドラが手っ取り早さから err.Error() を返すと、制約名やサードパーティのメッセージが本番に露出してしまいます。クライアント向けのメッセージは安全で短く、詳細はログに残してください。

テキストのみで依存するのも徐々に問題になります。クライアントが “email already exists” のような英語文章を解析していると、文言を変えただけで動作が壊れます。安定したエラーコードを使えばメッセージは自由に変更・翻訳できます。

エラーコードは公開契約の一部として扱ってください。変更が必要なら新しいコードを追加し、古いコードはしばらく動作し続けるようにして互換性を保ちます。

最後に、成功でも失敗でも同じ request_id フィールドを含めてください。ユーザーが「動いていたのに壊れた」と言ったとき、その ID が1時間の推測を節約してくれます。

出荷前の簡易チェックリスト

Centralize error handling
Turn your typed error model into a shared mapper layer across endpoints.
Create Backend

リリース前に一貫性のために簡単に確認してください:

  • 1つのエラー形を全てに適用している。すべてのエンドポイントが同じ JSON フィールド(例:error.code, error.message, request_id)を返す。
  • 安定したエラーコードをカバーしている。コードは短く無難に(VALIDATION_FAILED, NOT_FOUND, CONFLICT, UNAUTHORIZED)。ハンドラが未知のコードを返さないようにテストを用意する。
  • 1つのステータスマッピングルールブック。各エラー型がどの HTTP ステータスにマップするかを共有の場所で決める。
  • リクエスト ID を双方向で扱う。すべてのリクエストに request_id を返し、タイムアウトや panic でもログに記録する。
  • デフォルトで安全なメッセージ。ユーザー向けメッセージは短く明確で実行可能、スタックトレースや SQL エラー、ベンダー名は含めない。

その後、いくつかのエンドポイントを手動でサンプルチェックしてください。検証エラー、レコードの欠如、予期しない失敗をトリガーします。もしエンドポイント間で応答が異なる(フィールドが変わる、ステータスがずれる、メッセージが内部を漏らす)なら、機能を増やす前に共有パイプラインを修正してください。

実用的なルール:メッセージが攻撃者を助けたり通常ユーザーを混乱させるなら、それはレスポンスではなくログに置いてください。

次のステップ:今標準化し、その後も一貫性を保つ

API が既に稼働していても、すべてのエンドポイントが従うべきエラー契約(ステータス、安定したエラーコード、安全なメッセージ、request_id)を書き出してください。これがクライアントにとってエラーを予測可能にする最速の方法です。

その後、段階的に移行してください。既存ハンドラを残しつつ、失敗を1つのマッパーに通して公開用の応答形に変換してください。これにより大きなリライトをせずとも一貫性を高め、新しいエンドポイントが別の形式を生み出すのを防げます。

小さなエラーコードカタログを作り、それを API の一部として扱ってください。新しいコードを追加する際はレビューをして:本当に新しいか、名前は分かりやすいか、適切な HTTP ステータスにマップしているかを確認します。

ドリフトを検出するための数件のテストを追加してください:

  • すべてのエラー応答に request_id が含まれている。
  • ステータスコードがエラー型に一致している(テキストではない)。
  • error.code が存在し、カタログから来ている。
  • error.message は安全で内部の詳細を含まない。
  • 不明なエラーはジェネリックな 500 にフォールバックする。

もし Go でバックエンドを一から作るなら、契約を早期に固めるのは有益です。例えば、Koder.ai (koder.ai) は計画モードでエラースキーマやコードカタログのような規約を先に定義でき、API が成長してもハンドラを整合させ続けられます。

よくある質問

What should a “consistent error response” look like?

1つの JSON 形を全てのエラー応答で使ってください。実用的なデフォルトは、トップレベルに request_id を置き、code、message、任意の details を持つ error オブジェクトを返す形です。これによりクライアントは確実に解析して適切に処理できます。

How do I avoid leaking internal details in API errors?

error.message は短くユーザー向けに安全な文にして、実際の原因はサーバーログに残してください。生のデータベースエラー、スタックトレース、内部ホスト名、依存先のメッセージは返さないでください。開発中でも同様に扱うべきです。

Do I really need an error code if I already have HTTP status codes?

機械的な分岐には安定した error.code を使い、HTTP ステータスは大まかなカテゴリを示すために使います。クライアントは ALREADY_EXISTS のような error.code を基準に振る舞いを決め、ステータスは補助情報として扱ってください。

When should I use HTTP 400 vs 422?

リクエストが解析できない(不正な JSON、型が間違っている)場合は 400 を使ってください。リクエスト自体は構文的に正しいがビジネスルールに反する場合(無効なメール形式、パスワードの短さなど)は 422 を使います。

When should I use HTTP 409 vs 422?

入力自体は有効だが現在の状態と衝突する(メールが既に使われている、バージョン不一致など)場合は 409 を使います。フィールドレベルの検証エラーで、値を変えれば解決する場合は 422 を使ってください。

How do typed errors in Go help keep responses consistent?

Go では小さなセットの型付きエラー(validation, not found, conflict, unauthorized, internal など)を作り、ハンドラはそれらを返すようにします。共通のトランスレータを使って型をステータスと標準 JSON 形に変換すると、応答が一貫します。

How should I generate and return request IDs?

全てのレスポンス(成功・失敗ともに)に request_id を返し、サーバの全ログ行にも記録してください。クライアントが問題を報告するとき、その ID があればログ上で正確な実行を追跡できます。

Why is returning HTTP 200 with `{ "ok": false }` a bad idea?

操作が成功したときにだけ 200 を返し、エラーに対しては適切な 4xx/5xx を返してください。200 の中に { "ok": false } のように隠すと、クライアントはボディを解析しなければならず、一貫性が失われます。

Which errors should clients retry, and which should they not?

一般的には 400, 401, 403, 404, 409, 422 はリトライしてもうまくいかないことが多いのでリトライ不可とし、503(および場合によっては 429 の待機後)はリトライを許容します。POST に対しては冪等キーをサポートすると一時的な失敗時のリトライが安全になります。

How do I prevent error responses from drifting as the API evolves?

“ゴールデン”テストを使って契約を固定してください:ステータス、error.code、request_id の有無を検査します。新しいコードを追加する場合は既存の意味を壊さないようにし、オプションフィールドのみを追加して古いクライアントが無視できるようにします。

目次
なぜ一貫性のない API エラーはクライアントを苛立たせるのかシンプルな目標:すべてのエンドポイントが従う1つの契約クライアントが頼れるエラー応答スキーマを定義するGo の型付きエラー:ハンドラのためのシンプルなモデルエラー型を HTTP ステータスに一貫してマッピングするリクエスト ID:クライアント問題を素早くデバッグする最短路内部を漏らさないログとメトリクスステップバイステップ:Go で一貫したエラーパイプラインを実装する例:1つのエンドポイント、3つの失敗、予測可能な応答エラー処理を悪化させるよくある落とし穴出荷前の簡易チェックリスト次のステップ:今標準化し、その後も一貫性を保つよくある質問
共有
Koder.ai
Koderで自分のアプリを作ろう 今すぐ!

Koderの力を理解する最良の方法は、自分で体験することです。

無料で始めるデモを予約