
こんにちは!LayerX の バクラク事業部でSWEをしております、 2025年4月入社の新卒エンジニア shoyan です!
今回は先日 layerx.go #2 で発表した、Protocol Buffers の Opaque API のメリットと安全な移行方法 について紹介します。
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 ファイルが生成されます:
xxx.pb.go: 従来の Open Struct API(public フィールド)、デフォルトでコンパイル//go:build !protoopaquexxx_protoopaque.pb.go: 新しい Opaque API(private フィールド)、protoopaqueTag指定時のみコンパイル//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によるパフォーマンス向上、ポインタ操作防止による安全性向上がメリットです。 段階的移行では、open2opaqueでHybrid APIへの変換を半自動化し、Build Tags により本番環境への影響を防ぎながら新 API を段階的に検証できます。
おわりに
12 月上旬に LayerX 主催の"実践的な"Go 言語の勉強会 layerx.go #3 を開催予定です! 私も主催者兼司会として参加します!
LayerXのTechアカウントの @LayerX_tech をフォローして続報をお待ちください!
カジュアル面談もお待ちしております! layerx.notion.site
参考文献
- https://go.dev/blog/protobuf-opaque
- https://protobuf.dev/reference/go/opaque-migration/
- https://protobuf.dev/reference/go/opaque-faq
- https://protobuf.dev/
- https://connectrpc.com/
- https://go.dev/doc/pgo
- https://ja.wikipedia.org/wiki/Builder_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
- https://speakerdeck.com/shwatanap/yorian-quan-dexiao-lu-de-na-go-kodohe-protocol-buffers-opaque-api-nodao-ru