🐕

go-aspect: Goで横断的関心を合成的に扱うライブラリ

に公開

この記事は ChatGPT を使って書かれた

Go で「横断的関心(cross-cutting concern)」を安全かつシンプルに扱うための軽量ライブラリです。トランザクション、トレース、タイムアウト、監査ログなどを Aspect として合成的に実行できます。


背景

Go の世界では、AOP(Aspect-Oriented Programming)のような構文レベルの仕組みは存在しません。たとえば以下のような課題がよくあります:

  • どの関数でも共通して行いたい前後処理(例:ログ・トレース・Tx 管理)
  • defer の乱立や ctx の伝搬が煩雑
  • 中間層で panic / error の扱いがバラバラ

go-aspect はこの問題を 構文ではなく関数合成で解決する ことを目指しています。


コアアイデア

type Aspect func(ctx context.Context) (context.Context, func(success bool))
  • 各 Aspect は「入場処理」と「退場処理」をセットで提供します。
  • Do() に渡すと、入場処理が順番に適用され、退場処理が逆順で実行されます。
  • panic はすべて recover され、stack trace 付きの error に変換されます。

最小サンプル

err := aspect.Do(ctx, func(ctx context.Context) error {
    tx := aspect.TxFromContext(ctx)
    // ここで業務処理を書く
    return nil
},
    aspect.Trace("CreateUser"),
    aspect.Timeout(2*time.Second),
    aspect.Tx(&runner{}),
    aspect.Audit(store,
        aspect.WithAuditName("CreateUser"),
        aspect.WithAuditAttrs(func(ctx context.Context) map[string]any {
            return map[string]any{"user_id": "1234"}
        }),
    ),
)
  1. Trace が span を開始
  2. Timeout がコンテキストに期限を設定
  3. Tx がトランザクションを開始
  4. Audit がブロック完了時に監査イベントを記録

終了時には、これらの cleanup が 逆順 (LIFO) で確実に呼ばれます。


組み込み Aspect 一覧

Timeout

context.WithTimeout を使ってタイムアウト付きのコンテキストを生成します。

aspect.Timeout(2*time.Second)

Trace

OpenTelemetry を使ったトレースを開始・終了します。

aspect.Trace("OperationName")

Tx

トランザクションを自動で開始・終了します。

aspect.Tx(runner)

オプション指定:

aspect.TxWith(runner, aspect.TxOption{
    PanicOnError: true,
    Logger: log.Printf,
})

Audit

ブロック完了時に監査イベントを記録します。

aspect.Audit(store,
    aspect.WithAuditName("Operation"),
    aspect.WithAuditAttrs(func(ctx context.Context) map[string]any {
        return map[string]any{"tenant": "acme"}
    }),
)

型付きバージョン: DoWith

入力と出力を型で扱いたい場合は DoWith を使います。

res, err := aspect.DoWith(ctx, req, func(ctx context.Context, in Request) (Response, error) {
    // business logic
    return Response{}, nil
},
    aspect.Trace("CreateOrder"),
    aspect.Tx(runner),
)

設計哲学

  • Composable: 関心ごとを小さな関数として合成可能に。
  • Safe: cleanup は必ず呼ばれる(panic-safe, LIFO)。
  • Minimal: 反射もマジックも使わず、純粋な関数合成のみ。
  • Extensible: 新しい Aspect を簡単に追加できる。

カスタム Aspect の例

func Logging() aspect.Aspect {
    return func(ctx context.Context) (context.Context, func(bool)) {
        start := time.Now()
        log.Println("start")
        return ctx, func(success bool) {
            dur := time.Since(start)
            log.Printf("done success=%v dur=%v", success, dur)
        }
    }
}

aspect.Do(ctx, fn, Logging(), aspect.Tx(runner)) のように他と組み合わせて使えます。


実行例

[tx] begin
[business] start
[business] done
[tx] commit
[audit] name=ExampleOperation success=true took=100ms

リンク

Discussion