mcpField
G
golang-samber-oops

golang-samber-oops

github.com/samber開発3.7k38.6k 回インストール5 日前に更新
リポジトリ

golang-samber-oops について

samber/oops を扱う Go 向けスキルです。構造化エラー処理ライブラリーで、コンテキスト、スタックトレース、エラーコード、公開メッセージを付加し、panic からの復旧も行えます。

主な機能

  1. 01キーと値による構造化コンテキストの付加
  2. 02スタックトレースとエラーコードの記録
  3. 03公開メッセージと技術メッセージの分離
  4. 04エラーのラップ、結合、復旧
  5. 05エラーの JSON シリアライズ

活用シーン

  1. 01サービス境界でドメインコンテキストを追加
  2. 02安定したメッセージによる APM エラーの集約
  3. 03goroutine の panic をエラーに変換
  4. 04リクエストとトレース情報をエラーに付加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-samber-oops

インストール後、Claude Code で/golang-samber-oopsを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/advanced.md

詳細

ペルソナ: エラーを構造化データとして扱う Go エンジニアです。すべてのエラーに、オンコールエンジニアが開発者に問い合わせずに問題を診断できるだけのコンテキスト(ドメイン、属性、トレース)が含まれます。

samber/oops による構造化エラーハンドリング

samber/oops は Go 標準のエラーハンドリングをそのまま置き換え、構造化コンテキスト、スタックトレース、エラーコード、公開メッセージ、パニックリカバリーを追加します。変数データはメッセージ文字列ではなく .With() 属性に格納するため、APM ツール(Datadog、Loki、Sentry)でエラーを適切にグループ化できます。ログ出力時に slog 属性を追加する標準ライブラリの方式とは異なり、oops の属性は呼び出しスタックを通じてエラーとともに引き継がれます。

samber/oops を使う理由

Go 標準のエラーにはコンテキストがありません。connection failed と表示されても、どのユーザーが引き起こしたのか、どのクエリが実行されていたのか、呼び出しスタック全体がどうなっているのかは分かりません。samber/oops は次の機能を提供します。

  • 構造化コンテキスト — あらゆるエラーにキーと値の属性を追加できます
  • スタックトレース — 呼び出しスタックを自動的に取得します
  • エラーコード — 機械可読な識別子を設定できます
  • 公開メッセージ — 技術的な詳細とは分けて、ユーザー向けの安全なメッセージを設定できます
  • カーディナリティの低いメッセージ — 変数データをメッセージ文字列ではなく .With() 属性に格納するため、APM ツールでエラーを適切にグループ化できます

このスキルは網羅的なものではありません。詳しくはライブラリのドキュメントとコード例を参照してください。

  • Go パッケージのドキュメント、シンボル、バージョン、インポーター、既知の脆弱性については、→ samber/cc-skills-golang@golang-pkg-go-dev スキル(godig)を参照してください。Go パッケージに関する情報では、Context7 よりこちらを優先してください。
  • 独自のコードでこのライブラリの使い方(定義、呼び出し箇所、診断情報)を確認するには、→ samber/cc-skills-golang@golang-gopls スキル(gopls)を参照してください。
  • Context7 は、pkg.go.dev にインデックスされていないドキュメントを調べる際のフォールバックとして引き続き利用できます。

基本パターン:エラービルダーチェーン

oops のすべてのエラーは、流れるようなビルダーパターンを使用します。

err := oops.
    In("user-service").           // domain/feature
    Tags("database", "postgres").  // categorization
    Code("network_failure").       // machine-readable identifier
    User("user-123", "email", "[email protected]").  // user context
    With("query", query).          // custom attributes
    Errorf("failed to fetch user: %s", "timeout")

終端メソッド:

  • .Errorf(format, args...) — 新しいエラーを作成します
  • .Wrap(err) — 既存のエラーをラップします
  • .Wrapf(err, format, args...) — メッセージを付けてラップします
  • .Join(err1, err2, ...) — 複数のエラーを結合します
  • .Recover(fn) / .Recoverf(fn, format, args...) — パニックをエラーに変換します
エラービルダーのメソッド
メソッド用途
.With("key", value)カスタムのキーと値の属性を追加します(遅延評価される func() any の値に対応)
.WithContext(ctx, "key1", "key2")Go のコンテキストから値を抽出し、属性に設定します(遅延評価される値に対応)
.In("domain")機能/サービス/ドメインを設定します
.Tags("auth", "sql")分類タグを追加します(err.HasTag("tag") で検索できます)
.Code("iam_authz_missing_permission")機械可読なエラー識別子/スラッグを設定します
.Public("Could not fetch user.")ユーザー向けの安全なメッセージを設定します(技術的な詳細とは別に設定)
.Hint("Runbook: https://doc.acme.org/doc/abcd.md")開発者向けのデバッグヒントを追加します
.Owner("team/slack")担当チーム/オーナーを識別します
.User(id, "k", "v")ユーザー識別子と属性を追加します
.Tenant(id, "k", "v")テナント/組織のコンテキストと属性を追加します
.Trace(id)トレース/相関 ID を追加します(デフォルト:ULID)
.Span(id)作業単位/操作を表すスパン ID を追加します(デフォルト:ULID)
.Time(t)エラーのタイムスタンプを上書きします(デフォルト:time.Now())
.Since(t)t からの経過時間を設定します(err.Duration() で取得できます)
.Duration(d)エラーの経過時間を明示的に設定します
.Request(req, includeBody)*http.Request を添付します(任意でボディを含められます)
.Response(res, includeBody)*http.Response を添付します(任意でボディを含められます)
oops.FromContext(ctx)Go のコンテキストに格納された OopsErrorBuilder を起点にします

よくあるシナリオ

データベース/リポジトリ層
func (r *UserRepository) FetchUser(id string) (*User, error) {
    query := "SELECT * FROM users WHERE id = $1"
    row, err := r.db.Query(query, id)
    if err != nil {
        return nil, oops.
            In("user-repository").
            Tags("database", "postgres").
            With("query", query).
            With("user_id", id).
            Wrapf(err, "failed to fetch user from database")
    }
    // ...
}
HTTP ハンドラー層
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)

    err := h.service.CreateUser(r.Context(), userID)
    if err != nil {
        err = oops.
            In("http-handler").
            Tags("endpoint", "/users").
            Request(r, false).
            User(userID).
            Wrapf(err, "create user failed")
        http.Error(w, oops.GetPublic(err, "Internal server error"), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusCreated)
}
再利用可能なビルダーを使うサービス層
func (s *UserService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
    builder := oops.
        In("order-service").
        Tags("orders", "checkout").
        Tenant(req.TenantID, "plan", req.Plan).
        User(req.UserID, "email", req.UserEmail)

    product, err := s.catalog.GetProduct(ctx, req.ProductID)
    if err != nil {
        return builder.
            With("product_id", req.ProductID).
            Wrapf(err, "product lookup failed")
    }

    if product.Stock < req.Quantity {
        return builder.
            Code("insufficient_stock").
            Public("Not enough items in stock.").
            With("requested", req.Quantity).
            With("available", product.Stock).
            Errorf("insufficient stock for product %s", req.ProductID)
    }

    return nil
}

エラーをラップする際のベストプラクティス

推奨:nil チェックせずに直接ラップする
// ✓ Good — Wrap returns nil if err is nil
return oops.Wrapf(err, "operation failed")

// ✗ Bad — unnecessary nil check
if err != nil {
    return oops.Wrapf(err, "operation failed")
}
return nil
DO:各レイヤーでコンテキストを追加する

各アーキテクチャレイヤーでは、Wrap/Wrapf を使ってコンテキストを追加してください。パッケージ境界ごとに少なくとも1回追加します(すべての関数呼び出しで追加する必要はありません)。

// ✓ Good — each layer adds relevant context
func Controller() error {
    return oops.In("controller").Trace(traceID).Wrapf(Service(), "user request failed")
}

func Service() error {
    return oops.In("service").With("op", "create_user").Wrapf(Repository(), "db operation failed")
}

func Repository() error {
    return oops.In("repository").Tags("database", "postgres").Errorf("connection timeout")
}
DO:エラーメッセージのカーディナリティを低く保つ

APM で集約するため、エラーメッセージのカーディナリティは低く保つ必要があります。メッセージに変数データを埋め込むと、Datadog、Loki、Sentry でグループ化できなくなります。

// ✗ Bad — high-cardinality, breaks APM grouping
oops.Errorf("failed to process user %s in tenant %s", userID, tenantID)

// ✓ Good — static message + structured attributes
oops.With("user_id", userID).With("tenant_id", tenantID).Errorf("failed to process user")

パニックのリカバリー

ゴルーチンの境界では、oops.Recover() を使用する必要があります。パニックを構造化エラーに変換します。

func ProcessData(data string) (err error) {
    return oops.
        In("data-processor").
        Code("panic_recovered").
        Hint("Check input data format and dependencies").
        With("input_data", data).
        Recover(func() {
            riskyOperation(data)
        })
}

エラー情報へのアクセス

samber/oops のエラーは、標準の error インターフェースを実装しています。追加情報には次のようにアクセスします。

if oopsErr, ok := err.(oops.OopsError); ok {
    fmt.Println("Code:", oopsErr.Code())
    fmt.Println("Domain:", oopsErr.Domain())
    fmt.Println("Tags:", oopsErr.Tags())
    fmt.Println("Context:", oopsErr.Context())
    fmt.Println("Stacktrace:", oopsErr.Stacktrace())
}

// Get public-facing message with fallback
publicMsg := oops.GetPublic(err, "Something went wrong")
出力形式
fmt.Printf("%+v\n", err)       // verbose with stack trace
bytes, _ := json.Marshal(err)  // JSON for logging
slog.Error(err.Error(), slog.Any("error", err))  // slog integration

コンテキストの伝播

Go のコンテキストを通じてエラーコンテキストを引き継ぎます。

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        builder := oops.
            In("http").
            Request(r, false).
            Trace(r.Header.Get("X-Trace-ID"))

        ctx := oops.WithBuilder(r.Context(), builder)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(ctx context.Context) error {
    return oops.FromContext(ctx).Tags("handler", "users").Errorf("something failed")
}

アサーション、設定、その他のロガーの例については、高度なパターンを参照してください。

参考資料

関連項目

  • → 一般的なエラー処理のパターンについては、samber/cc-skills-golang@golang-error-handling スキルを参照してください
  • → ロガーの統合と構造化ログについては、samber/cc-skills-golang@golang-observability スキルを参照してください

コメント