hacomono TECH BLOG

フィットネスクラブやスクールなどの顧客管理・予約・決済を行う、業界特化型SaaS「hacomono」を提供する会社のテックブログです!

Go言語で運用品質を向上させるエラーハンドリングライブラリを作った



こんにちは、リアーキテクチャ&イネーブルメント部のjunです。
普段はフレームワーク寄りの機能やライブラリの開発、パフォーマンスチューニング、リファクタリングなどなどプロダクトの土台を支える役割を担当しています。

今回は、運用品質向上のために開発した、Go言語のエラーハンドリングライブラリ「go-errorsx」の設計思想と活用方法をご紹介します。

※ go-errorsxはまだ発展段階のライブラリです。APIの変更や機能追加が行われる可能性があります。また、ValidationErrorは開発中の機能で、将来的にcontribとして別パッケージに分離する可能性があります。

なぜ新しいライブラリが必要だったのか

既存の素晴らしいGoエラーライブラリ(pkg/errorseriscockroachdb/errorsなど)はそれぞれ優れた特徴を持っていますが、運用品質を向上させる機能をもう少し追加したい一方で、大規模で複雑なライブラリを使うほどではないと感じていました。

そこで、AI Agentによるコード生成・メンテナンスが容易になった現在、特定の要件に最適化した軽量なライブラリを自作する方が良いと判断し、以下の機能を実現しました:

go-errorsxの主要機能

go-errorsxは運用品質を向上させる構造化エラーライブラリです。
品質とデバッグ性を高めながら、本番運用での障害対応力を強化します。

運用品質向上で求めていた機能

既存ライブラリでは以下の要件をすべて満たすものがありませんでした:

  • スタックトレースの可読性向上
  • 開発者向けメッセージとユーザー向けメッセージの分離
  • 構造化されたJSON出力
  • HTTPステータス対応
  • 分類可能で外部監視ツールとの親和性
  • リトライ可能性の明示化
スタックトレースの可読性向上
// 従来: ノイズの多いスタックトレース
github.com/gin-gonic/gin.(*Context).Next(...)        ← フレームワーク層(ノイズ)
runtime/debug.Stack(...)                             ← ランタイム層(ノイズ)
main.getUserHandler(...)                             ← アプリケーション層(重要)
// go-errorsx: クリーニング後
main.getUserHandler(...)
main.fetchUserFromDB(...)
main.validateUserInput(...)

開発者から「400系エラーでも必要最低限のスタックトレースは出し、発生箇所は特定したい」という要望に応えるため、RailsのActiveSupport::BacktraceCleanerにインスパイアされた機能を実装しました。
これでフレームワークのノイズを除去し、重要な情報のみを表示します。

開発者向けメッセージとユーザー向けメッセージの分離
err := errorsx.New("user.authentication.failed").
        WithReason("Invalid password hash comparison").  // 開発者向け
        WithMessage("認証に失敗しました。再度お試しください。") // ユーザー向け

技術的な詳細情報(デバッグ用)とユーザー向けの表示メッセージを明確に分離し、適切な情報を適切な対象に提供できます。

構造化されたJSON出力
// 自動的にJSON化可能
jsonData, _ := json.Marshal(err)
// → {"id": "user.not.found", "type": "validation", "message_data": {...}}

slogのような構造化ログとの統合により、監視ツールでの検索・分析・アラートが容易になります。

HTTPステータス対応
err := errorsx.New("user.not.found").WithHTTPStatus(404)
// Web APIハンドラーで自動対応
w.WriteHeader(err.HTTPStatus())

WebアプリケーションでのHTTPステータスとエラーのマッピングを自動化し、一貫したAPIレスポンスを実現します。

分類可能で外部監視ツールとの親和性
err := errorsx.New("database.timeout").WithType(TypeInfrastructure)
// 監視ツールでの自動分類・通知
// TypeInfrastructure → インフラチームに通知
// TypeBusiness → ビジネスチームに通知

エラータイプによる分類により、各企業の監視ツール(PagerDuty、Datadog等)での自動処理・通知が可能になります。

リトライ可能性の明示化
// 一時的な障害:リトライ可能
err := errorsx.New("service.unavailable").WithRetryable()
// 恒久的な障害:リトライ不可
err := errorsx.New("auth.invalid.credentials")
if errorsx.IsRetryable(err) {
    return implementRetryLogic(operation)
}

エラー発生時点でリトライ可能性を明示することで、ハンドリング側での適切な処理判断を支援します。

実践的な使用例

Web APIハンドラーでの活用

func GetUser(w http.ResponseWriter, r *http.Request) {
    userID, err := strconv.Atoi(mux.Vars(r)["id"])
    if err != nil {
        err := errorsx.New("user.id.invalid").
            WithType(errorsx.TypeValidation).
            WithHTTPStatus(400).
            WithMessage(map[string]any{
                "field": "id",
                "value": mux.Vars(r)["id"],
                "expected": "integer",
            })
        writeErrorResponse(w, err)
        return
    }
    
    user, err := userService.GetByID(userID)
    if err != nil {
        writeErrorResponse(w, err)
        return
    }
    
    json.NewEncoder(w).Encode(user)
}
func writeErrorResponse(w http.ResponseWriter, err error) {
    var xerr *errorsx.Error
    if errors.As(err, &xerr) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(xerr.HTTPStatus())
        json.NewEncoder(w).Encode(xerr)
    }
}


構造化ログとの統合

func logError(err error, ctx map[string]any) {
    var xerr *errorsx.Error
    if errors.As(err, &xerr) {
        slog.Error("Application error occurred",
            "error", xerr,     // 自動的にJSONシリアライゼーション
            "context", ctx,
        )
    }
}


スタックトレース取得の基本ルール

重要な使い分け

go-errorsxでは、エラーの発生源を特定するため以下のルールでスタックトレースを取得します:

  • 元エラーがない場合: WithCallerStack()を使用して明示的にキャプチャ
  • 元エラーがある場合: WithCause()が自動的にスタックトレースをキャプチャ
// 元エラーなし:明示的なキャプチャが必要
err := errorsx.New("user.not.found").WithCallerStack()
// 元エラーあり:WithCauseが自動でキャプチャ
originalErr := db.Query(...)
err := errorsx.New("user.fetch.failed").WithCause(originalErr)


エラーテンプレート化による効率的な設計

// エラーテンプレートを定数として定義
var (
    ErrUserNotFound = errorsx.New("user.not.found").
        WithType(errorsx.TypeNotFound).
        WithHTTPStatus(404)
    
    ErrValidationFailed = errorsx.New("validation.failed").
        WithType(errorsx.TypeValidation).
        WithHTTPStatus(400)
    
    ErrDatabaseTimeout = errorsx.New("database.timeout").
        WithType(errorsx.TypeInfrastructure).
        WithHTTPStatus(503).
        WithRetryable()
)
// 実行時にコンテキストを追加
func getUserByID(id int) (*User, error) {
    user, err := db.GetUser(id)
    if err == sql.ErrNoRows {
        return nil, ErrUserNotFound.
            WithReason("User record not found in database: %d", id).
            WithMessage(map[string]any{"user_id": id}).
            WithCallerStack() // 元エラーがない場合は発生元をキャプチャ
    }
    if isTimeoutError(err) {
        return nil, ErrDatabaseTimeout.
            WithReason("Database connection timeout during user query for ID: %d", id).
            WithCause(err). // 元エラーがある場合はWithCauseのみ
            WithMessage(map[string]any{
                "user_id": id,
                "operation": "SELECT",
                "timeout": "5s",
                "connection_pool": "users_db",
            })
    }
    return user, nil
}



まとめ

go-errorsxは、Go言語における運用品質を重視したエラーハンドリングライブラリです。
既存ライブラリの優れた機能を参考にしながら、現代の運用要件に特化した機能を統合的に提供します。
今後も継続的な改善を進め、より使いやすく価値のあるライブラリを目指していきます。

参考までに・・・

採用を推奨するケース

  • 新規プロジェクト: 統一されたエラーハンドリングを最初から構築
  • Web API・マイクロサービス: HTTP統合やサービス間での統一エラー形式が必要
  • 運用効率化要求: MTTR短縮、自動化推進が必要な組織
  • CLI・バッチアプリケーション: 構造化ログやリトライ制御が必要
  • チーム独自の要件: 既存ライブラリでは柔軟性が足りない

導入時の考慮事項

go-errorsxの導入には一定の学習コストと設計コストがかかります。
しかし、これはエラー設計にきちんと向き合うことの表れでもあります。

  • エラーIDの体系的な設計: 一貫性のある命名規則の策定
  • エラータイプの分類: ビジネスロジック、インフラ、バリデーションなどの適切な分類
  • メッセージデータの設計: 運用に有用な構造化情報の検討
  • チーム内でのルール統一: エラーハンドリングのベストプラクティス共有

このコストは、従来の「とりあえず文字列でエラーメッセージ」から脱却し、運用品質の向上に真剣に取り組む投資と考えることができます。



💁 関連記事