このガイドでは、Goaサービスでインターセプターを実装する際のベストプラクティスとガイドラインについて説明します。
インターセプターは単一責任の原則に従うべきです。各インターセプターは、ロギング、メトリクス、 認証などの特定の横断的関心事を1つだけ処理するようにします。これにより以下の利点が得られます:
例えば、ロギングとメトリクスの両方を処理する1つの大きなインターセプターを作成するのではなく、 必要に応じて一緒に使用できる2つの焦点を絞ったインターセプターを作成します。この関心事の 分離により、より保守性と柔軟性の高いコードが実現できます。
以下は、焦点を絞ったインターセプターと焦点が散漫なインターセプターの違いを示す例です:
// Good: 焦点を絞ったインターセプター
var Auth = Interceptor("Auth", func() {
Description("認証のみを処理")
ReadPayload(func() {
Attribute("token", String)
})
})
var Metrics = Interceptor("Metrics", func() {
Description("メトリクスの収集のみを処理")
ReadResult(func() {
Attribute("status", Int)
})
})
// Bad: 責任が多すぎる
var AuthAndMetrics = Interceptor("AuthAndMetrics", func() {
Description("認証とメトリクスの両方を処理")
// 関心事を混ぜることでインターセプターの保守が難しくなる
})
焦点を絞ったインターセプターは、テスト、保守が容易で、必要に応じて異なる組み合わせで 合成できます。
インターセプターをサービス全体に適用するか、特定のメソッドにのみ適用するかを慎重に 検討してください。サービスレベルのインターセプターは一貫した横断的関心事に適しており、 メソッドレベルのインターセプターは特定の要件に適しています。
以下の例は、異なるスコープでインターセプターを適用する方法を示しています:
var _ = Service("users", func() {
// Good: 認証はすべてのメソッドに適用
ServerInterceptor(Auth)
Method("list", func() {
// Good: メトリクスはlistメソッドにのみ必要
ServerInterceptor(Metrics)
})
})
認証はどこでも必要なためサービス全体に適用され、メトリクス収集は関連するlistメソッドにのみ 適用されています。
明確な命名とドキュメントは、他の開発者がインターセプターの目的と動作を理解するのに 役立ちます。名前はインターセプターの機能を示し、説明はその目的と重要な詳細を説明する 必要があります。
以下の例を比較してください:
// Good: 明確な名前と説明
var RequestValidator = Interceptor("RequestValidator", func() {
Description("ビジネスルールに対して受信リクエストを検証")
ReadPayload(func() {
Attribute("data")
})
})
// Bad: 目的が不明確
var Handler = Interceptor("Handler", func() {
Description("何かを処理")
ReadPayload(func() {
Attribute("data")
})
})
適切に命名されたインターセプターは、その目的を明確にし、有用なドキュメントを提供します。
GoaのエラーDSLを使用して設計時にエラーを定義します。これにより、型安全性とサービス全体での 一貫したエラー処理が保証されます。エラー定義はAPIの契約の一部となり、適切なヘルパー関数が 生成されます。
以下は、エラーを適切に定義して使用する方法です:
// 設計内で
var _ = Service("users", func() {
// サービス固有のエラーを定義
Error("unauthorized", ErrorResult, "認証に失敗しました")
Error("invalid_token", ErrorResult, "無効または不正なトークン")
// インターセプター設計でエラーを使用
var Auth = Interceptor("Auth", func() {
Error("unauthorized")
Error("invalid_token")
ReadPayload(func() {
Attribute("token")
})
})
ServerInterceptor(Auth)
})
// 実装内で
func (i *ServerInterceptors) Auth(ctx context.Context, info *AuthInfo, next goa.Endpoint) (any, error) {
p := info.Payload()
// 設計時のエラーを使用
token := p.Token()
if token == "" {
return nil, genservice.MakeUnauthorized(fmt.Errorf("認証トークンが必要です"))
}
claims, err := validateToken(token)
if err != nil {
return nil, genservice.MakeInvalidToken(err.Error())
}
return next(ctx, info.RawPayload())
}
生成されたMake*
関数は、エラーが設計と一致し、適切なエラーコードとメタデータを含むことを
保証します。このアプローチは、一般的なエラーよりも優れたエラー処理を提供し、APIの一貫性を
維持するのに役立ちます。
インターセプターでコンテキストを扱う際は、コンテキスト値を適切に管理し保持することが 重要です。多くのライブラリやツール(トレーサー、ロガー、認証など)はコンテキストに 情報を格納します。インターセプターは以下を行う必要があります:
以下は、適切なコンテキスト処理の例です:
func (i *ServerInterceptors) Tracer(ctx context.Context, info *TracerInfo, next goa.Endpoint) (any, error) {
// Good: 新しいコンテキストを派生させ、既存の値を保持
ctx, span := tracer.Start(ctx, info.Method())
defer span.End()
return next(ctx, info.RawPayload())
}
このアプローチにより以下が保証されます:
インターセプターはすべてのリクエストで実行されるため、そのパフォーマンスへの影響は サービス全体で倍増します。以下のプラクティスに従うことで、インターセプターが スケールしても効率的に動作することを保証できます。
メモリアロケーションは、特に高負荷時にパフォーマンスに大きな影響を与える可能性が あります。オブジェクトプールを使用し、可能な場合は事前アロケーションを行い、 インターセプターでの不要なアロケーションを避けます。一般的な手法には以下があります:
以下は、効率的なオブジェクト管理の例です:
func (i *ServerInterceptors) Metrics(ctx context.Context, info *MetricsInfo, next goa.Endpoint) (any, error) {
// Good: オブジェクトを再利用
labels := i.getLabelsFromPool()
defer i.putLabelsToPool(labels)
// Bad: 毎回新しいオブジェクトを作成
// labels := make(map[string]string)
return next(ctx, info.RawPayload())
}
このアプローチにより、ガベージコレクションの負荷が軽減され、特に高トラフィック時の 全体的なサービスパフォーマンスが向上します。
キャッシングはパフォーマンスを劇的に改善できますが、慎重に実装する必要があります。 以下を考慮してください:
以下は、効果的なキャッシュ使用の例です:
func (i *ClientInterceptors) Cache(ctx context.Context, info *CacheInfo, next goa.Endpoint) (any, error) {
p := info.Payload()
// Good: 適切なキャッシュ期間を使用
if cached := i.cache.Get(p.CacheKey()); cached != nil {
if !isExpired(cached, p.TTL()) {
return cached, nil
}
}
return next(ctx, info.RawPayload())
}