型とバリデーション

Goaが生成されるコードで型、ポインタ、バリデーションをどのように扱うかを理解する

バリデーションの実施

Goaはバリデーションに対して実用的なアプローチを取り、パフォーマンスと堅牢性の バランスを取ります。フレームワークはシステムの境界でデータを検証し、内部操作は 信頼します:

  • サーバーサイド: 受信リクエストを検証
  • クライアントサイド: 受信レスポンスを検証
  • 内部コード: 不変条件を維持することを信頼

このアプローチにより、内部操作の不要な検証オーバーヘッドを避けながら、コードが 常に有効なデータを受け取ることを保証します。

生成される構造体のフィールドとポインタ

Goaのコード生成アルゴリズムは、構造体のフィールドでポインタをいつ使用するかを 慎重に考慮します。目標は、型の安全性と適切なnull処理を維持しながら、ポインタの 使用を最小限に抑えることです。

プリミティブ型のルール

Goaがコードを生成する際、生成される構造体でフィールドをどのように表現するかに ついて決定を下す必要があります。主要な決定の1つは、プリミティブ型(stringintboolなど)に対してポインタ(*)を使用するか、直接値(-)を使用するか です。

用語の理解

ルールに深入りする前に、主要な用語を明確にしましょう:

  • ペイロード/結果: これらはサービスデザインにおけるメソッドの引数と戻り値です

    • ペイロード: サービスメソッドが受け取るデータ(例: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                // レスポンスボディフィールド
}

ルールは以下によって異なります:

  1. フィールドが必須か、デフォルト値を持っているか
  2. フィールドがどこで使用されているか(ペイロード、リクエスト、レスポンス)
  3. 通信のどちら側にあるか(サーバーまたはクライアント)

詳細な内訳は以下の通りです:

プロパティペイロード/結果リクエストボディ(サーバー)レスポンスボディ(サーバー)リクエストボディ(クライアント)レスポンスボディ(クライアント)
必須またはデフォルト値あり直接値(-)ポインタ(*)直接値(-)直接値(-)ポインタ(*)
必須でない、デフォルト値なしポインタ(*)ポインタ(*)ポインタ(*)ポインタ(*)ポインタ(*)

例を使って説明しましょう:

  1. 必須またはデフォルト値を持つフィールド

    • ほとんどの場合、直接値を使用します(ポインタではない)
    • 例:ペイロードの必須name stringフィールドはName stringとして生成されます
    • 例外:サーバーのリクエストボディとクライアントのレスポンスボディは、より良い null処理のためにポインタを使用します
  2. オプションのフィールド(必須でない、デフォルト値なし)

    • すべてのコンテキストでポインタを使用します
    • 例:オプションのage intフィールドはAge *intとして生成されます
    • これにより、未設定値(nil)とゼロ値(0)を区別できます
  3. 特殊な型

    • オブジェクト(構造体):必須/オプションに関係なく、常にポインタを使用します
    • 配列とマップ:すでに参照型であるため、ポインタは使用しません
    • 例:[]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仕様で明確に文書化します