Goのエラーハンドリング設計パターン:errors.Is・errors.Asとカスタムエラー型の実務活用

当ページのリンクには広告が含まれています。
IT女子 アラ美
🚀 Go案件で高単価を狙いなさい!フリーランスで勝負するならここよ
ITフリーランスエンジニアの案件探しなら【techadapt】
この記事の結論
Goのエラーハンドリングでは、errors.Isで値ベースの比較、errors.Asで型ベースの抽出、カスタムエラー型でドメイン固有のエラー情報を表現するのが最適解です。fmt.Errorfの%wによるエラーラップを徹底すれば、デバッグ時間を大幅に短縮できます。本記事では、実務で頻出する設計パターンとアンチパターンを具体的なコード付きで解説します。

お疲れ様です!IT業界で働くアライグマです!

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

目次

Goのエラーハンドリング設計思想と全体像

IT女子 アラ美
💡 Go案件で年収アップしたいでしょ!
社内SEならGoの技術選定を自分の裁量で決められるわよ
社内SEを目指す方必見!IT・Webエンジニアの転職なら【社内SE転職ナビ】

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が引き続き機能する

IT女子 アラ美
try-catchに慣れていると、毎回 if err != nil を書くのが冗長に感じるのですが。

ITアライグマ
冗長に見えるのは最初だけです。エラーの発生箇所が明確になるため、デバッグ効率は上がります!

前提条件と環境整理

本記事のコード例は以下の環境を前提としています。

  • Go 1.22以上: errors.Is/errors.Asの基本機能はGo 1.13以降で利用可能。本記事ではGo 1.22の改善点も含む
  • 標準ライブラリのみ: サードパーティパッケージ(pkg/errors等)は使用しない。Go 1.13以降は標準ライブラリで十分
  • プロダクションコード前提: 個人開発ではなく、チームで運用するAPIサーバーやCLIツールを想定

なお、モジュラモノリス完全ガイドで解説したようなモジュール境界をまたぐエラー伝搬でも、ここで紹介するパターンが直接適用できます。

IT女子 アラ美
pkg/errorsは使わなくていいんですか?以前のプロジェクトでは必須でした。

ITアライグマ
Go 1.13以降は標準ライブラリで十分です。pkg/errorsはメンテナンス終了状態なので移行推奨です!

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開発において重要なパターンです。

IT女子 アラ美
errors.Isとerrors.Asの使い分けがまだ曖昧です。具体的な判断基準はありますか?

ITアライグマ
シンプルに「エラーの中身を見るか見ないか」で判断してください。中身不要ならIs、必要ならAsです!

カスタムエラー型の設計と実務でのアンチパターン

カスタムエラー型を適切に設計することで、エラーにドメイン固有の情報を持たせられます。ただし、設計を誤ると保守性が大幅に低下します。

推奨パターン:ドメインエラー型

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選

  1. エラーの文字列比較: err.Error() == “not found” のようなパターンはリファクタリングで壊れる。必ずerrors.Is/Asを使う
  2. エラーの握りつぶし: _ = someFunc() はログにも残らず原因追跡が不可能になる。最低限ログ出力する
  3. 過剰なラップ: 全関数でfmt.Errorfすると同じ情報が重複する。コンテキストが新たに加わる層でのみラップする

エラーメッセージに機密情報を含めないことも重要です。AI時代の.envシークレット管理術で解説した通り、データベース接続文字列やAPIキーをエラーメッセージに埋め込むと、ログ経由で漏洩するリスクがあります。

IT女子 アラ美
カスタムエラー型を作りすぎると管理が大変になりませんか?目安を教えてください。

ITアライグマ
ドメイン境界ごとに1つが目安です。マイクロサービスなら1サービス1〜2型で十分ですよ!

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

IT女子 アラ美
💡 エラー設計できるGoエンジニア、市場で引っ張りだこよ!
体系的にGoスキルを磨くなら、プロの指導で効率的に学びなさい
資格と仕事に強い!個人レッスンのプログラミングスクール【Winスクール】

ここでは、Goのエラーハンドリングを再設計して成果を上げた事例を紹介します。

人物像と状況(Before)

田中さん(31歳・バックエンドエンジニア・Go経験3年)は、従業員80名のSaaS企業でAPIサーバーの開発を担当していました。チームメンバー5名で運用するGoのAPIサーバーは約200エンドポイントを持ち、月間リクエスト数は約500万件。

エラーハンドリングの課題は深刻でした。エラーの文字列比較(err.Error() == “…”)が47箇所に散在し、エラーメッセージを変更するたびにテストが壊れる状態。さらに、エラーのラップが統一されておらず、本番障害時に「どの層で何が起きたか」をログから追跡するのに平均2.5時間かかっていました。

きっかけと行動(Action)

きっかけは、決済処理でエラーの握りつぶしが原因の障害が発生したことでした。田中さんはチームに提案し、以下の3ステップでリファクタリングを実施しました。

  1. sentinel errorの定義: ドメイン層にErrNotFound、ErrConflict等の共通エラーを定義
  2. AppError型の導入: HTTPステータスコードとユーザー向けメッセージを持つカスタムエラー型を設計
  3. ハンドラー層のエラー変換を統一: ミドルウェアで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万円を稼ぐ実践ロードマップでも触れている通り、コードレビューや技術顧問の副業案件でも高く評価されるスキルです。

IT女子 アラ美
リファクタリングの2週間、通常の開発を止める必要がありましたか?

ITアライグマ
通常開発と並行して進めたそうです。モジュール単位で段階的に置換する方法がおすすめです!

よくある質問

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開発
難易度 初心者◎個人レッスン形式 中級者〜コード記述あり
補助金・給付金 最大70%還元教育訓練給付金対象 最大70%還元教育訓練給付金対象
おすすめ度 S幅広くITスキルを学ぶなら AAIエンジニアになるなら
公式サイト 詳細を見る
IT女子 アラ美
AIスキルを身につけたいけど、どのスクールを選べばいいかわからないです…
ITアライグマ
現場で即・ITスキルを身につけたいならWinスクールがおすすめです!個人レッスン形式で初心者でも取り組みやすいですよ。

まとめ

Goのエラーハンドリングは、errors.Is・errors.As・fmt.Errorfの%wという3つの標準ツールを正しく使い分けることで、堅牢かつ保守しやすい設計が実現できます。

  • errors.Isは値ベースの比較、errors.Asは型ベースの抽出。「中身を見るか見ないか」で使い分ける
  • カスタムエラー型はドメイン境界ごとに1つが目安。Unwrap()を実装してエラーチェーンを維持する
  • アンチパターン(文字列比較・握りつぶし・過剰ラップ)を避けるだけで、デバッグ効率は大幅に改善する

まずは既存コードのerr.Error()による文字列比較をerrors.Is/Asに置き換えるところから始めてみてください。小さな改善がチーム全体のコード品質を引き上げます。

IT女子 アラ美
Goのエラーハンドリングを改善するなら、まず何から手をつけるべきですか?

ITアライグマ
既存コードの文字列比較を洗い出すことから始めてください。grepで一発で見つかりますよ!

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次