型とバリデーション
バリデーションの実施
Goaはバリデーションに対して実用的なアプローチを取り、パフォーマンスと堅牢性の バランスを取ります。フレームワークはシステムの境界でデータを検証し、内部操作は 信頼します:
- サーバーサイド: 受信リクエストを検証
- クライアントサイド: 受信レスポンスを検証
- 内部コード: 不変条件を維持することを信頼
このアプローチにより、内部操作の不要な検証オーバーヘッドを避けながら、コードが 常に有効なデータを受け取ることを保証します。
生成される構造体のフィールドとポインタ
Goaのコード生成アルゴリズムは、構造体のフィールドでポインタをいつ使用するかを 慎重に考慮します。目標は、型の安全性と適切なnull処理を維持しながら、ポインタの 使用を最小限に抑えることです。
プリミティブ型のルール
Goaがコードを生成する際、生成される構造体でフィールドをどのように表現するかに
ついて決定を下す必要があります。主要な決定の1つは、プリミティブ型(string、
int、boolなど)に対してポインタ(*)を使用するか、直接値(-)を使用するか
です。
用語の理解
ルールに深入りする前に、主要な用語を明確にしましょう:
ペイロード/結果: これらはサービスデザインにおけるメソッドの引数と戻り値です
- ペイロード: サービスメソッドが受け取るデータ(例:
method (payload *CreateUserPayload)) - 結果: サービスメソッドが返すデータ(例:
returns (UserResult))
- ペイロード: サービスメソッドが受け取るデータ(例:
リクエスト/レスポンスボディ: これらはHTTPまたはgRPCのトランスポートレベルの構造です
- リクエストボディ: 受信HTTP/gRPCリクエストデータを運ぶデータ構造
- レスポンスボディ: 送信HTTP/gRPCレスポンスデータを運ぶデータ構造
例えば、REST APIでは:
// デザインで:
var _ = Service("users", func() {
Method("create", func() {
Payload(func() {
Field(1, "name", String) // これはペイロードフィールド
Required("name")
})
Result(func() {
Field(1, "id", Int) // これは結果フィールド
})
HTTP(func() {
POST("/users")
Response(StatusOK)
})
})
})
// Goaが生成:
type CreatePayload struct {
Name string // ペイロードフィールド
}
type CreateRequestBody struct {
Name *string // リクエストボディフィールド
}
type CreateResult struct {
ID int // 結果フィールド
}
type CreateResponseBody struct {
ID int // レスポンスボディフィールド
}
ルールは以下によって異なります:
- フィールドが必須か、デフォルト値を持っているか
- フィールドがどこで使用されているか(ペイロード、リクエスト、レスポンス)
- 通信のどちら側にあるか(サーバーまたはクライアント)
詳細な内訳は以下の通りです:
| プロパティ | ペイロード/結果 | リクエストボディ(サーバー) | レスポンスボディ(サーバー) | リクエストボディ(クライアント) | レスポンスボディ(クライアント) |
|---|---|---|---|---|---|
| 必須またはデフォルト値あり | 直接値(-) | ポインタ(*) | 直接値(-) | 直接値(-) | ポインタ(*) |
| 必須でない、デフォルト値なし | ポインタ(*) | ポインタ(*) | ポインタ(*) | ポインタ(*) | ポインタ(*) |
例を使って説明しましょう:
必須またはデフォルト値を持つフィールド:
- ほとんどの場合、直接値を使用します(ポインタではない)
- 例:ペイロードの必須
name stringフィールドはName stringとして生成されます - 例外:サーバーのリクエストボディとクライアントのレスポンスボディは、より良い null処理のためにポインタを使用します
オプションのフィールド(必須でない、デフォルト値なし):
- すべてのコンテキストでポインタを使用します
- 例:オプションの
age intフィールドはAge *intとして生成されます - これにより、未設定値(nil)とゼロ値(0)を区別できます
特殊な型:
- オブジェクト(構造体):必須/オプションに関係なく、常にポインタを使用します
- 配列とマップ:すでに参照型であるため、ポインタは使用しません
- 例:
[]stringまたはmap[string]int(*[]stringや*map[string]intではない)
これらのルールの理由:
- ポインタはオプションのフィールドに対して明示的なnil値を可能にします
- 値が常に存在することがわかっている場合、直接値の方が効率的です
- リクエスト/レスポンス処理の非対称性は、適切なシリアライゼーションと バリデーションに役立ちます
例のシナリオ:
// 以下のフィールドを持つデザインの場合:
// - name: string(必須)
// - age: int(オプション)
// - hobbies: []string
// - metadata: map[string]string
// サービスパッケージで生成される構造体は以下のようになります:
type Person struct {
Name string // 必須、直接値
Age *int // オプション、ポインタ
Hobbies []string // 配列、ポインタなし
Metadata map[string]string // マップ、ポインタなし
}
デフォルト値の処理
デザインで指定されたデフォルト値は、2つの主要なシナリオで使用されます:
1. マーシャリング時(送信データ)
出力用にデータをマーシャリングする際、デフォルト値はnil値の処理で重要な役割を 果たします。nilである配列とマップフィールドの場合、デザインで指定された デフォルト値を使用して初期化されます。ただし、これはプリミティブフィールドには 適用されません - プリミティブフィールドは常にゼロ値(数値の場合は0、文字列の 場合は"“など)を持つためです。
2. アンマーシャリング時(受信データ)
受信データをアンマーシャリングする際、デフォルト値は入力に存在しないオプション フィールドにのみ適用されます。必須フィールドが欠けている場合、デフォルト値を 適用する代わりにバリデーションエラーが発生します。gRPCの場合、アンマーシャリング 時のデフォルト値の特別な処理があります - 詳細についてはgRPCアンマーシャリング セクションを参照してください。
ベストプラクティス
- オプションフィールドに対して、適切なフォールバックを提供するためにデフォルト値を 使用します
- デフォルト値を変更する際は、APIバージョニングへの影響を考慮します
- デフォルト値をAPI仕様で明確に文書化します