より安全で効率的な 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と呼びます)。

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 ファイルが生成されます:

  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

参考文献




元の記事を確認する

関連記事