ドメインエラーとトランスポートエラー
Goaにおけるドメインエラーとトランスポートエラーの違い、およびそれらの効果的なマッピング方法について学びます。
Goaは、サービス全体でエラーを効果的に定義、管理、通信できる堅牢なエラー処理システムを提供します。このガイドでは、Goaにおけるエラー処理について知っておくべきすべてを説明します。
Goaはエラー処理に対して「バッテリー同梱」アプローチを取っており、必要に応じて完全にカスタムなエラー型をサポートしながら、最小限の情報(名前のみ)でエラーを定義できます。フレームワークはエラー定義からコードとドキュメントの両方を生成し、API全体での一貫性を確保します。
Goaのエラー処理の主な機能:
エラーはAPIレベルで定義して、再利用可能なエラー定義を作成できます。 サービスレベルのエラーとは異なり、APIレベルのエラーは自動的にすべての メソッドに適用されるわけではありません。代わりに、トランスポートマッピングを 含むエラープロパティを一度定義し、サービスとメソッド間で再利用する方法を 提供します:
var _ = API("calc", func() {
// トランスポートマッピングを持つ再利用可能なエラーを定義
Error("invalid_argument") // デフォルトのErrorResult型を使用
HTTP(func() {
Response("invalid_argument", StatusBadRequest)
})
})
var _ = Service("divider", func() {
// APIレベルのエラーを参照
Error("invalid_argument") // 上で定義したエラーを再利用
// HTTPマッピングを再定義する必要なし
Method("divide", func() {
Payload(DivideRequest)
// カスタム型を持つメソッド固有のエラー
Error("div_by_zero", DivByZero, "ゼロによる除算")
})
})
このアプローチは:
サービスレベルのエラーは、サービス内のすべてのメソッドで利用可能です。 再利用可能な定義を提供するAPIレベルのエラーとは異なり、サービスレベルの エラーは自動的にサービス内のすべてのメソッドに適用されます:
var _ = Service("calc", func() {
// このエラーはこのサービスのどのメソッドからも返すことができます
Error("invalid_arguments", ErrorResult, "無効な引数が提供されました")
Method("divide", func() {
// このメソッドは明示的に宣言しなくてもinvalid_argumentsを返すことができます
Payload(func() {
Field(1, "dividend", Int)
Field(2, "divisor", Int)
Required("dividend", "divisor")
})
// ... その他のメソッド定義
})
Method("multiply", func() {
// このメソッドもinvalid_argumentsを返すことができます
// ... メソッド定義
})
})
サービスレベルでエラーを定義する場合:
メソッド固有のエラーは、特定のメソッドにスコープされます:
var _ = Service("calc", func() {
Method("divide", func() {
Payload(func() {
Field(1, "dividend", Int)
Field(2, "divisor", Int)
Required("dividend", "divisor")
})
Result(func() {
Field(1, "quotient", Int)
Field(2, "reminder", Int)
Required("quotient", "reminder")
})
Error("div_by_zero") // メソッド固有のエラー
})
})
より複雑なエラーシナリオでは、カスタムエラー型を定義できます。カスタム エラー型を使用すると、エラーケースに特有の追加のコンテキスト情報を 含めることができます。
以下は簡単なカスタムエラー型の例です:
var DivByZero = Type("DivByZero", func() {
Description("DivByZeroは、除数として0を使用した場合に返されるエラーです。")
Field(1, "message", String, "ゼロによる除算は無限大になります。")
Required("message")
})
同じメソッドで複数のエラーにカスタムエラー型を使用する場合、Goaはどのフィールドにエラー名が含まれているかを知る必要があります。これは以下の点で重要です:
エラー名フィールドを指定するには、struct:error:name
メタデータを使用します:
var DivByZero = Type("DivByZero", func() {
Description("DivByZeroは、除数として0を使用した場合に返されるエラーです。")
Field(1, "message", String, "ゼロによる除算は無限大になります。")
Field(2, "name", String, "エラーの名前", func() {
Meta("struct:error:name") // このフィールドにエラー名が含まれることをGoaに伝える
})
Required("message", "name")
})
Meta("struct:error:name")
でマークされたフィールドは:
"error_name"
という名前は使用できない(Goaによって予約済み)メソッドが複数の異なるカスタムエラー型を返す可能性がある場合、名前フィールドは特に重要になります。その理由は:
エラー型の解決: 複数のエラー型が可能な場合、Goaは名前フィールドを使用して、 返されている実際のエラーが設計のどのエラー定義に一致するかを判断します。これにより Goaは以下が可能になります:
トランスポート層の処理: 名前フィールドがないと、トランスポート層は異なる ステータスコードで定義された複数のエラー型のうち、どのステータスコードを使用すべきか わかりません:
HTTP(func() {
Response("div_by_zero", StatusBadRequest) // 400
Response("overflow", StatusUnprocessableEntity) // 422
})
クライアント側の型アサーション: 名前フィールドにより、Goaは設計で定義された 各エラーに対して特定のエラー型を生成できます。これらの生成された型により、エラー処理は 型安全になり、すべてのエラーフィールドにアクセスできます:
以下は、設計での名前が実装と一致する必要がある例です:
var _ = Service("calc", func() {
Method("divide", func() {
// これらの名前("div_by_zero"と"overflow")は、エラー型の
// nameフィールドで正確に使用される必要があります
Error("div_by_zero", DivByZero)
Error("overflow", NumericOverflow)
// ... その他のメソッド定義
})
})
// これらのエラーを処理するクライアントコードの例
res, err := client.Divide(ctx, payload)
if err != nil {
switch err := err.(type) {
case *calc.DivideDivByZeroError:
// このエラーは設計のError("div_by_zero", ...)に対応
fmt.Printf("ゼロ除算エラー: %s\n", err.Message)
fmt.Printf("%dをゼロで除算しようとしました\n", err.Dividend)
case *calc.DivideOverflowError:
// このエラーは設計のError("overflow", ...)に対応
fmt.Printf("オーバーフローエラー: %s\n", err.Message)
fmt.Printf("結果値 %d が最大値を超えました\n", err.Value)
case *goa.ServiceError:
// 一般的なサービスエラー(バリデーションなど)の処理
fmt.Printf("サービスエラー: %s\n", err.Message)
default:
// 不明なエラーの処理
fmt.Printf("不明なエラー: %s\n", err.Error())
}
}
設計で定義された各エラーに対して、Goaは以下を生成します:
"div_by_zero"
に対するDivideDivByZeroError
)設計と実装の接続はエラー名を通じて維持されます:
Error("name", ...)
で使用される名前MethodNameError
)エラープロパティは、エラーの性質についてクライアントに通知し、適切な処理戦略を
実装できるようにする重要なフラグです。これらのプロパティはデフォルトのErrorResult
型を
使用する場合にのみ利用可能で、カスタムエラー型では効果がありません。
プロパティはDSL関数を使用して定義されます:
Temporary()
: エラーが一時的であり、同じリクエストを再試行すると成功する可能性があることを示すTimeout()
: デッドラインを超過したためにエラーが発生したことを示すFault()
: サーバーサイドのエラー(バグ、設定の問題など)を示すデフォルトのErrorResult
型を使用する場合、これらのプロパティは生成されたServiceError
構造体の
フィールドに自動的にマッピングされ、洗練されたクライアント側のエラー処理が可能になります:
var _ = Service("calc", func() {
// 一時的なエラーはクライアントに再試行を提案します
Error("invalid_argument", ErrorResult, "無効な引数が提供されました")
Method("divide", func() {
Payload(func() {
Field(1, "dividend", Int)
Field(2, "divisor", Int)
Required("dividend", "divisor")
})
Error("div_by_zero", DivByZero, "ゼロによる除算")
})
})
Error(“rate_limit”, ErrorResult, func() { Description(“APIレート制限を超過しました”) Temporary() // レート制限が解除されたら再試行可能 })
Error(“db_unavailable”, ErrorResult, func() { Description(“データベースが一時的に利用できません”) Temporary() // データベースが復旧したら再試行可能 Fault() // サーバー側の問題 })
Error(“deadline_exceeded”, ErrorResult, func() { Description(“リクエストがタイムアウトしました”) Timeout() // タイムアウトエラー Temporary() // 再試行可能 })
Method(“process”, func() { Payload(func() { Field(1, “input”, String) Required(“input”) }) Result(String) // 上記で定義したエラーを使用可能 })
これらのプロパティを使用することで、クライアントは適切な再試行戦略を実装できます:
```go
res, err := client.Divide(ctx, payload)
if err != nil {
switch e := err.(type) {
case *goa.ServiceError: // ServiceErrorのみがこれらのプロパティを持ちます
if e.Temporary {
// バックオフ付きで再試行を実装
return retry(ctx, func() error {
res, err = client.Divide(ctx, payload)
return err
})
}
if e.Timeout {
// 次のリクエストのタイムアウトを増やすかもしれません
ctx = context.WithTimeout(ctx, 2*time.Second)
return client.Divide(ctx, payload)
}
if e.Fault {
// エラーをログに記録し、管理者に通知
log.Error("サーバーの障害を検出", "error", e)
alertAdmins(e)
}
default:
// カスタムエラー型はこれらのプロパティを持ちません
log.Error("エラーが発生しました", "error", err)
}
}
これらのプロパティにより、クライアントは以下が可能になります:
注意:カスタムエラー型でこれらのプロパティが必要な場合は、カスタム型に 同様のフィールドを実装し、コードで明示的に処理する必要があります。
エラー定義の階層
エラー型の選択
ErrorResult
を使用トランスポートマッピング
エラーメッセージ
クライアント側の処理
Goaのエラー処理システムは、以下を提供することで、堅牢なAPIの構築を支援します:
このシステムを効果的に使用することで:
次のセクションでは、これらの概念を実際のユースケースに適用する方法を見ていきます。
Goaでは、エラーを適切なトランスポート固有のステータスコードにマッピングできます。 このマッピングは、異なるプロトコル間で一貫性のある意味のあるエラーレスポンスを 提供するために重要です。
HTTPトランスポートの場合、エラー状態を最もよく表す標準的なHTTPステータスコードに
エラーをマッピングします。マッピングはHTTP
DSLで定義されます:
var _ = Service("calc", func() {
Method("divide", func() {
// 説明付きで可能性のあるエラーを定義
Error("div_by_zero", ErrorResult, "ゼロによる除算エラー")
Error("overflow", ErrorResult, "数値オーバーフローエラー")
Error("unauthorized", ErrorResult, "認証が必要です")
HTTP(func() {
POST("/")
// 各エラーを適切なHTTPステータスコードにマッピング
Response("div_by_zero", StatusBadRequest)
Response("overflow", StatusUnprocessableEntity)
Response("unauthorized", StatusUnauthorized)
})
})
})
エラーが発生すると、Goaは:
gRPCトランスポートの場合、標準的なgRPCステータスコードにエラーをマッピングします。 マッピングは同様の原則に従いますが、gRPC固有のコードを使用します:
var _ = Service("calc", func() {
Method("divide", func() {
// 説明付きで可能性のあるエラーを定義
Error("div_by_zero", ErrorResult, "ゼロによる除算エラー")
Error("overflow", ErrorResult, "数値オーバーフローエラー")
Error("unauthorized", ErrorResult, "認証が必要です")
GRPC(func() {
// 各エラーを適切なgRPCステータスコードにマッピング
Response("div_by_zero", CodeInvalidArgument)
Response("overflow", CodeOutOfRange)
Response("unauthorized", CodeUnauthenticated)
})
})
})
一般的なgRPCステータスコードのマッピング:
CodeInvalidArgument
: バリデーションエラー用(例:div_by_zero)CodeNotFound
: リソースが見つからないエラー用CodeUnauthenticated
: 認証エラー用CodePermissionDenied
: 認可エラー用CodeDeadlineExceeded
: タイムアウトエラー用CodeInternal
: サーバーサイドの障害用明示的なマッピングが提供されない場合:
CodeUnknown
を使用Goaのエラー処理の基本を理解したところで、以下のトピックを探索して知識を深めましょう:
これらのガイドは、Goaサービスで包括的なエラー処理戦略を実装するのに役立ちます。