LayerX エンジニアブログ

LayerX の エンジニアブログです。

より安全で効率的な Go コードへ:Protocol Buffers Opaque API の導入

こんにちは!LayerX の バクラク事業部でSWEをしております、 2025年4月入社の新卒エンジニア shoyan です!

今回は先日 layerx.go #2 で発表した、Protocol Buffers の Opaque API のメリットと安全な移行方法 について紹介します。

speakerdeck.com

Opaque API とは?

まずは、Protocol Buffers (Protobuf) について簡単に説明します。Protobuf は、Google が開発したデータシリアライゼーション形式です。.protoファイルでスキーマを定義し、コンパイルして各言語のコードを自動生成します。LayerX では、Protobuf と Connect を組み合わせて、gRPC 互換の HTTP API を構築しています。 典型的な .proto ファイルは以下のようになります。

message XXX {
  string id = 1;
  string name = 2;
}

message CreateXXXRequest {
  string name = 1;
}

message CreateXXXResponse {
  XXX xxx = 1;
}

service XXXService {
  rpc CreateXXX(CreateXXXRequest) returns (CreateXXXResponse);
}

従来の「Open Struct API」が抱える課題

従来、protoc で生成された構造体のフィールドは public で、直接アクセス可能でした(本記事ではOpen Struct APIと呼びます)。

// 生成された Go コード (xxx.pb.go)
type XXX struct {
    Id string  // Public フィールド
}

func (x XXX) GetId() string {
    return x.Id
}

// サービス側の実装 (rpc_xxx.go)
func (s *XXXService) CreateXXX(
    ctx context.Context,
    req *connect.Request[xxxv1.CreateXXXRequest],
) (*connect.Response[xxxv1.CreateXXXResponse], error) {
    // フィールドに直接アクセスできる
    id := req.Msg.Id

    // Getter メソッドも利用できる
    id := req.Msg.GetId()
    // ...
}

この方法はシンプルですが、フィールドへの直接アクセスにより API と利用側が密結合になる問題があります。パフォーマンス最適化で構造体のメモリレイアウトを変更すると、直接アクセスしているコードでコンパイルエラーが発生し、破壊的変更となります。 例えば、Protobuf が、PGO(Profile-guided optimization) のような最適化に対応して、使用頻度の低いフィールドを別の内部構造体に移動して生成するようになっても、Getter メソッド(例: GetUrl())が変更を吸収するため、利用側のコード変更は不要です。

  • 最適化前:
  // 生成された Go コード (xxx.pb.go)
  type XXX struct {
    ID       string
    Name     string
    Url string
  }

  func (xxx XXX) GetUrl() string {
    return xxx.Url
  }

  // main.go
  func main() {
    xxx.Url    // フィールドに直接アクセス

    xxx.GetUrl() // Getter methodの利用
    ...
  }
  • 最適化後:
  type XXX struct {
      ID       string
      Name     string
      overflow *XXXOverflow // 使用頻度の低いフィールドを分離
  }
  type XXXOverflow struct {
      Url string
  }

  func (xxx XXX) GetUrl() string {
    return xxx.overflow.Url
  }

  // main.go
  func main() {
    xxx.Url      // ❌ `xxx.Url` への直接アクセスはコンパイルエラーに!

    xxx.GetUrl() // ⭕️ `xxx.GetUrl()` は内部実装の変更を隠蔽し、問題なく動作する
    ...
  }

Opaque API のメリット

Protobuf のedition 2023から導入された Opaque API がこの問題を解決します。フィールドがprivateになり、データ操作はすべてアクセサメソッド(Getter や Setter)経由となります。

それにより以下のようなメリットがあります。

  • 内部実装の変更への耐性: private フィールドにより、内部実装の変更が利用側に影響しません
  • Lazy Decoding(遅延デコード): 必要なフィールドのみを必要なタイミングでデコードすることで、パフォーマンスが向上します
  • メモリの節約: ビットフィールドでフィールドの有無を管理し、メモリ使用量を削減します
  • 安全性の向上: ポインタ比較のミス(メモリアドレスを比較してしまい、常に false になる)を防止します

Opaque API への段階的移行戦略

大規模プロジェクトで数百の.protoファイルが複数のマイクロサービスで共有されている場合、Opaque API への一斉移行はリスクが高く、チーム間調整や後方互換性が課題となります。 そのため、Hybrid APIによる段階的移行が推奨されています。移行を半自動化するopen2opaqueツールも提供されています。 ここからは、公式の Migration Guidesに沿って、 Opaque API への移行を 3 つのステップで進めます。

ステップ 1: Hybrid API を有効にする

最初に、Open Struct API と Opaque API の両方をサポートする Hybrid API を有効にします。

$ open2opaque setapi -api HYBRID ./...

これにより、.proto ファイルに 2 行が追加されます。

edition = "2023";

package xxx.v1;

import "google/protobuf/go_features.proto"; // 追加

option features.(pb.go).api_level = API_HYBRID; // 追加
...

この状態で Protobuf をコンパイルすると、2 つの Go ファイルが生成されます:

  1. xxx.pb.go: 従来の Open Struct API(public フィールド)、デフォルトでコンパイル //go:build !protoopaque
  2. xxx_protoopaque.pb.go: 新しい Opaque API(private フィールド)、protoopaque Tag指定時のみコンパイル //go:build protoopaque

重要な点として、両 API で共通利用できる ビルダーパターン の構造体(XXX_builder)が生成されます。このビルダーが public/private フィールドの違いを吸収し、スムーズな移行を実現します。

ステップ 2: 既存コードをビルダーパターンに書き換える

次に、open2opaqueで既存の構造体初期化コードをビルダーパターンに書き換えます。

$ open2opaque rewrite ./...

コードは自動的に以下のように変換されます:

  • 実行前:
  xxx := &xxxv1.XXX{
      Id: id,
  }
  • 実行後:
  xxx := &xxxv1.XXX_builder{
      Id: id,
  }.Build()

ビルダーは両 API で動作するため、書き換え後も本番環境は問題なく動作し、Build Tags で Opaque API をテスト環境で検証できます。

  • 本番環境(Open Struct API): go build ./...
  • テスト環境(Opaque API): go build -tags=protoopaque ./...

ステップ 3: Opaque API を完全に有効にする

全コードのビルダーパターン変換とテスト完了後、移行を完了させます。

$ open2opaque setapi -api OPAQUE ./...

.protoファイルが更新され、Opaque API コードのみ生成されるようになります。移行完了です!

まとめ

Protocol Buffers の Opaque API は、堅牢で効率的、将来の変更に強い Go コードを実現します。 Opaque API は、フィールドをprivateにし、アクセサメソッド経由でのみ操作を許容する API モデルです。 メモリ最適化と Lazy Decodingによるパフォーマンス向上、ポインタ操作防止による安全性向上がメリットです。 段階的移行では、open2opaqueHybrid APIへの変換を半自動化し、Build Tags により本番環境への影響を防ぎながら新 API を段階的に検証できます。

おわりに

12 月上旬に LayerX 主催の"実践的な"Go 言語の勉強会 layerx.go #3 を開催予定です! 私も主催者兼司会として参加します!

LayerXのTechアカウントの @LayerX_tech をフォローして続報をお待ちください!

カジュアル面談もお待ちしております! layerx.notion.site

参考文献