🐕
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"}
}),
),
)
-
Traceが span を開始 -
Timeoutがコンテキストに期限を設定 -
Txがトランザクションを開始 -
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
リンク
- GitHub: mickamy/go-aspect
- License: MIT
Discussion