IT女子 アラ美お疲れ様です!IT業界で働くアライグマです!
「if err != nil の嵐で可読性が下がる」「エラーを握りつぶしてしまって本番障害になった」——Goのエラーハンドリングは言語の最大の特徴でありながら、正しく設計できていないチームが多い領域です。Go 1.13で導入されたerrors.Is・errors.Asとfmt.Errorfの%wを正しく使い分けることで、堅牢かつ保守しやすいエラー設計が実現できます。
Goのエラーハンドリング設計思想と全体像



Goのエラーハンドリングは、他の言語のtry-catchとは根本的に設計思想が異なります。Goではエラーは単なる値(error interface)であり、例外のように制御フローを破壊しません。この設計により、エラーの発生箇所と処理箇所が明確になり、コードの追跡性が格段に向上します。
Go 1.13以降、標準ライブラリにerrors.Is・errors.As・fmt.Errorfの%wが追加され、エラーの比較・抽出・ラップが型安全に行えるようになりました。tRPC・oRPC・Hono RPC・Elysia Eden完全比較ガイドでTypeScriptのエラーハンドリングに触れましたが、Goはそれらとは異なるアプローチで同等以上の堅牢性を実現しています。
エラーハンドリングの3つの柱は以下の通りです。
- errors.Is: 特定のエラー値(sentinel error)と一致するか判定する。エラーチェーンを自動で辿る
- errors.As: エラーチェーンから特定の型のエラーを抽出する。カスタムエラー型のフィールドにアクセスしたいときに使う
- fmt.Errorf + %w: 元のエラーにコンテキスト情報を付加してラップする。呼び出し元でerrors.Is/Asが引き続き機能する



前提条件と環境整理
本記事のコード例は以下の環境を前提としています。
- Go 1.22以上: errors.Is/errors.Asの基本機能はGo 1.13以降で利用可能。本記事ではGo 1.22の改善点も含む
- 標準ライブラリのみ: サードパーティパッケージ(pkg/errors等)は使用しない。Go 1.13以降は標準ライブラリで十分
- プロダクションコード前提: 個人開発ではなく、チームで運用するAPIサーバーやCLIツールを想定
なお、モジュラモノリス完全ガイドで解説したようなモジュール境界をまたぐエラー伝搬でも、ここで紹介するパターンが直接適用できます。



errors.Is・errors.Asの使い分けと実装パターン
ここでは、errors.Isとerrors.Asの具体的な使い分けを、実務で頻出するパターンと共に解説します。
errors.Is:値ベースの比較
errors.Isは、エラーチェーンを辿って特定のsentinel errorと一致するかを判定します。「このエラーが原因で起きたか?」を確認するときに使います。
var ErrNotFound = errors.New("resource not found")
var ErrPermissionDenied = errors.New("permission denied")
func GetUser(id string) (*User, error) {
user, err := repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("GetUser(%s): %w", id, err)
}
return user, nil
}
// 呼び出し側
user, err := GetUser("123")
if errors.Is(err, ErrNotFound) {
// 404を返す
} else if errors.Is(err, ErrPermissionDenied) {
// 403を返す
}
errors.As:型ベースの抽出
errors.Asは、エラーチェーンから特定の型のエラーを抽出します。カスタムエラー型のフィールド(HTTPステータスコード、エラーコード等)にアクセスしたいときに使います。
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}
// 呼び出し側
var valErr *ValidationError
if errors.As(err, &valErr) {
log.Printf("バリデーションエラー: フィールド=%s", valErr.Field)
}
判断基準:IsとAsの選び方
- 「このエラーか?」と判定だけしたい → errors.Is
- エラーの詳細情報を取り出したい → errors.As
- 単純なsentinel error → errors.Is + errors.New
- コンテキスト付きエラー → errors.As + カスタム構造体
エラーの種類に応じて適切なHTTPレスポンスを返す設計は、Cloudflare無料プランでWebサイトを守る完全ガイドで紹介したWAF設定のようなセキュリティ対策と同様に、API開発において重要なパターンです。



カスタムエラー型の設計と実務でのアンチパターン
カスタムエラー型を適切に設計することで、エラーにドメイン固有の情報を持たせられます。ただし、設計を誤ると保守性が大幅に低下します。
推奨パターン:ドメインエラー型
APIサーバーでは、ドメイン層のエラーをHTTPレスポンスに変換する必要があります。以下のパターンなら、ハンドラー層でerrors.Asを使って統一的に変換できます。
type AppError struct {
Code int // HTTPステータスコード
Message string // ユーザー向けメッセージ
Err error // 元のエラー(ラップ用)
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 使用例
func NewNotFound(resource string, err error) *AppError {
return &AppError{
Code: 404,
Message: fmt.Sprintf("%s が見つかりません", resource),
Err: err,
}
}
アンチパターン3選
- エラーの文字列比較: err.Error() == “not found” のようなパターンはリファクタリングで壊れる。必ずerrors.Is/Asを使う
- エラーの握りつぶし: _ = someFunc() はログにも残らず原因追跡が不可能になる。最低限ログ出力する
- 過剰なラップ: 全関数でfmt.Errorfすると同じ情報が重複する。コンテキストが新たに加わる層でのみラップする
エラーメッセージに機密情報を含めないことも重要です。AI時代の.envシークレット管理術で解説した通り、データベース接続文字列やAPIキーをエラーメッセージに埋め込むと、ログ経由で漏洩するリスクがあります。



ケーススタディ:APIサーバーのエラー設計リファクタリングでデバッグ時間を40%削減



ここでは、Goのエラーハンドリングを再設計して成果を上げた事例を紹介します。
人物像と状況(Before)
田中さん(31歳・バックエンドエンジニア・Go経験3年)は、従業員80名のSaaS企業でAPIサーバーの開発を担当していました。チームメンバー5名で運用するGoのAPIサーバーは約200エンドポイントを持ち、月間リクエスト数は約500万件。
エラーハンドリングの課題は深刻でした。エラーの文字列比較(err.Error() == “…”)が47箇所に散在し、エラーメッセージを変更するたびにテストが壊れる状態。さらに、エラーのラップが統一されておらず、本番障害時に「どの層で何が起きたか」をログから追跡するのに平均2.5時間かかっていました。
きっかけと行動(Action)
きっかけは、決済処理でエラーの握りつぶしが原因の障害が発生したことでした。田中さんはチームに提案し、以下の3ステップでリファクタリングを実施しました。
- sentinel errorの定義: ドメイン層にErrNotFound、ErrConflict等の共通エラーを定義
- AppError型の導入: HTTPステータスコードとユーザー向けメッセージを持つカスタムエラー型を設計
- ハンドラー層のエラー変換を統一: ミドルウェアでerrors.Asを使いAppErrorを抽出し、レスポンスに自動変換
リファクタリングは2週間のスプリントで完了。文字列比較の47箇所をすべてerrors.Is/Asに置換し、fmt.Errorfの%wによるエラーラップを全層に適用しました。
結果(After)
- 障害時のデバッグ時間: 平均2.5時間 → 平均1.5時間(40%削減)
- エラー起因のテスト失敗: 月12件 → 月2件(83%削減)
- エラーレスポンスの統一率: 60% → 98%
- チームの新メンバーオンボーディング: エラー設計の説明が30分で完了(以前は2時間)
振り返り・教訓
田中さんは振り返ります。「一番の学びは、最初にsentinel errorとカスタムエラー型のルールをドキュメント化したことです。ルールがあるとレビューの基準が明確になり、チーム全体の設計品質が一気に上がりました。逆に、やらなくてよかったのは全エラーのカスタム型化です。sentinel errorで十分な箇所は無理に型を作らない方がシンプルに保てます」。
このような設計判断力は、エンジニアが副業で月10万円を稼ぐ実践ロードマップでも触れている通り、コードレビューや技術顧問の副業案件でも高く評価されるスキルです。



よくある質問
Q. errors.Isとerrors.Asはどちらを先に覚えるべきですか?
errors.Isから始めるのがおすすめです。sentinel errorの定義と比較はシンプルで、すぐに実務で使えます。errors.Asはカスタムエラー型を設計する段階で必要になるため、基本を押さえてから学ぶと理解が深まります。
Q. fmt.Errorfの%wと%vの違いは何ですか?
%wはエラーをラップし、errors.Is/Asでチェーンを辿れるようにします。%vは単にエラーメッセージを文字列として埋め込むだけで、チェーンが切れます。原則として%wを使い、意図的にチェーンを切りたい場合のみ%vを使ってください。
Q. panicとerrorはどう使い分けるべきですか?
panicはプログラムの継続が不可能な状態(初期化の失敗、不変条件の違反等)にのみ使います。ビジネスロジックのエラー(ユーザー入力の不正、リソースの不在等)はerrorで返すのがGoの慣習です。
Q. Go 1.22以降で変わったことはありますか?
エラーハンドリングの基本APIに大きな変更はありません。ただし、slogパッケージの安定化により、構造化ログとエラー情報を組み合わせた運用が標準的になりました。slog.Error(“failed”, “err”, err)のように、エラーをログフィールドとして渡す設計が推奨されています。
自分のスキルアップに合った学習環境を選ぶ際は、バックエンド言語の設計パターンを実践的に学べるカリキュラムを重視してみてください。
本記事で解説したようなAI技術を、基礎から体系的に身につけたい方は、以下のスクールも検討してみてください。
| 比較項目 | Winスクール | Aidemy Premium |
|---|---|---|
| 目的・ゴール | 資格取得・スキルアップ初心者〜社会人向け | エンジニア転身・E資格Python/AI開発 |
| 難易度 | 個人レッスン形式 | コード記述あり |
| 補助金・給付金 | 教育訓練給付金対象 | 教育訓練給付金対象 |
| おすすめ度 | 幅広くITスキルを学ぶなら | AIエンジニアになるなら |
| 公式サイト | 詳細を見る | − |



まとめ
Goのエラーハンドリングは、errors.Is・errors.As・fmt.Errorfの%wという3つの標準ツールを正しく使い分けることで、堅牢かつ保守しやすい設計が実現できます。
- errors.Isは値ベースの比較、errors.Asは型ベースの抽出。「中身を見るか見ないか」で使い分ける
- カスタムエラー型はドメイン境界ごとに1つが目安。Unwrap()を実装してエラーチェーンを維持する
- アンチパターン(文字列比較・握りつぶし・過剰ラップ)を避けるだけで、デバッグ効率は大幅に改善する
まずは既存コードのerr.Error()による文字列比較をerrors.Is/Asに置き換えるところから始めてみてください。小さな改善がチーム全体のコード品質を引き上げます。














