
こんにちは!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と呼びます)。
type XXX struct { Id string } func (x XXX) GetId() string { return x.Id } func (s *XXXService) CreateXXX( ctx context.Context, req *connect.Request[xxxv1.CreateXXXRequest], ) (*connect.Response[xxxv1.CreateXXXResponse], error) { id := req.Msg.Id id := req.Msg.GetId() }
この方法はシンプルですが、フィールドへの直接アクセスにより API と利用側が密結合になる問題があります。パフォーマンス最適化で構造体のメモリレイアウトを変更すると、直接アクセスしているコードでコンパイルエラーが発生し、破壊的変更となります。
例えば、Protobuf が、PGO(Profile-guided optimization) のような最適化に対応して、使用頻度の低いフィールドを別の内部構造体に移動して生成するようになっても、Getter メソッド(例: GetUrl())が変更を吸収するため、利用側のコード変更は不要です。
type XXX struct { ID string Name string Url string } func (xxx XXX) GetUrl() string { return xxx.Url } func main() { xxx.Url xxx.GetUrl() ... }
type XXX struct { ID string Name string overflow *XXXOverflow } type XXXOverflow struct { Url string } func (xxx XXX) GetUrl() string { return xxx.overflow.Url } func main() { xxx.Url 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