Protocol Buffers for Go
Googleが開発した言語中立・プラットフォーム中立のシリアライゼーションフォーマット。スキーマ定義による型安全なデータ交換を実現。
Protocol Buffers for Go
概要
Protocol Buffers(protobuf)は、Googleが開発した言語中立・プラットフォーム中立のシリアライゼーションフォーマットです。.protoファイルでスキーマを定義し、コードジェネレータを使用して各言語向けのコードを生成します。高効率なバイナリフォーマットと型安全性により、大規模なシステムでのデータ交換に最適です。
重要な注意事項
github.com/golang/protobufはレガシーAPIとなり、現在はgoogle.golang.org/protobuf(新API)に移行することが推奨されています。レガシーAPIは互換性のために維持されていますが、新機能は新APIに追加されます。
インストール
新API(推奨)
go get google.golang.org/protobuf
レガシーAPI(互換性のため)
go get github.com/golang/[email protected]
Protocol Bufferコンパイラ
# protocコンパイラのインストール
# macOS
brew install protobuf
# Linux
sudo apt-get install protobuf-compiler
# Goプラグインのインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
基本的な使い方
1. スキーマ定義(.protoファイル)
syntax = "proto3";
package example;
option go_package = "github.com/yourcompany/yourproject/pb";
import "google/protobuf/timestamp.proto";
message User {
int64 id = 1;
string name = 2;
string email = 3;
repeated string tags = 4;
google.protobuf.Timestamp created_at = 5;
enum Status {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
SUSPENDED = 3;
}
Status status = 6;
map<string, string> metadata = 7;
}
message UserList {
repeated User users = 1;
int32 total_count = 2;
}
2. コード生成
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
user.proto
3. 生成されたコードの使用
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
pb "github.com/yourcompany/yourproject/pb"
)
func main() {
// メッセージの作成
user := &pb.User{
Id: 1,
Name: "田中太郎",
Email: "[email protected]",
Tags: []string{"developer", "golang"},
CreatedAt: timestamppb.Now(),
Status: pb.User_ACTIVE,
Metadata: map[string]string{
"department": "engineering",
"level": "senior",
},
}
// バイナリシリアライゼーション
data, err := proto.Marshal(user)
if err != nil {
log.Fatal("マーシャルエラー:", err)
}
fmt.Printf("シリアライズサイズ: %dバイト\n", len(data))
// デシリアライゼーション
newUser := &pb.User{}
err = proto.Unmarshal(data, newUser)
if err != nil {
log.Fatal("アンマーシャルエラー:", err)
}
fmt.Printf("デコード結果: %+v\n", newUser)
}
高度な機能
JSONシリアライゼーション
import (
"google.golang.org/protobuf/encoding/protojson"
)
func jsonExample(user *pb.User) {
// Protocol Buffers → JSON
jsonData, err := protojson.Marshal(user)
if err != nil {
log.Fatal("JSONマーシャルエラー:", err)
}
fmt.Printf("JSON: %s\n", string(jsonData))
// JSON → Protocol Buffers
newUser := &pb.User{}
err = protojson.Unmarshal(jsonData, newUser)
if err != nil {
log.Fatal("JSONアンマーシャルエラー:", err)
}
// オプション付きJSONマーシャル
marshaler := protojson.MarshalOptions{
Multiline: true, // 整形済みJSON
Indent: " ", // インデント
EmitUnpopulated: true, // ゼロ値を含む
}
prettyJSON, _ := marshaler.Marshal(user)
fmt.Printf("整形済みJSON:\n%s\n", string(prettyJSON))
}
リフレクションAPI
import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
func reflectionExample(user *pb.User) {
// メッセージのリフレクション
m := user.ProtoReflect()
// フィールドの取得
fields := m.Descriptor().Fields()
for i := 0; i < fields.Len(); i++ {
field := fields.Get(i)
value := m.Get(field)
fmt.Printf("フィールド: %s = %v\n", field.Name(), value)
}
// 動的なフィールド設定
nameField := m.Descriptor().Fields().ByName("name")
m.Set(nameField, protoreflect.ValueOfString("新しい名前"))
// 未知フィールドの処理
unknown := m.GetUnknown()
if len(unknown) > 0 {
fmt.Printf("未知フィールド: %dバイト\n", len(unknown))
}
}
Well-Known Typesの使用
import (
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func wellKnownTypesExample() {
// Timestamp
ts := timestamppb.Now()
fmt.Printf("現在時刻: %v\n", ts.AsTime())
// Duration
dur := durationpb.New(5 * time.Minute)
fmt.Printf("期間: %v\n", dur.AsDuration())
// Any(任意のメッセージを格納)
user := &pb.User{Id: 1, Name: "test"}
anyMsg, _ := anypb.New(user)
// Anyからの取得
if anyMsg.MessageIs(user) {
var extractedUser pb.User
anyMsg.UnmarshalTo(&extractedUser)
fmt.Printf("Anyから取得: %+v\n", extractedUser)
}
// Struct(動的JSON構造)
s, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
"age": 30,
"active": true,
"tags": []string{"a", "b"},
})
// Wrappers(null許容型)
nullableInt := wrapperspb.Int32(42)
fmt.Printf("Nullable int: %v\n", nullableInt.GetValue())
}
パフォーマンス最適化
// バッファの再利用
type ProtoBuffer struct {
buf *proto.Buffer
}
func NewProtoBuffer() *ProtoBuffer {
return &ProtoBuffer{
buf: proto.NewBuffer(nil),
}
}
func (pb *ProtoBuffer) Marshal(msg proto.Message) ([]byte, error) {
pb.buf.Reset()
err := pb.buf.Marshal(msg)
return pb.buf.Bytes(), err
}
func (pb *ProtoBuffer) Unmarshal(data []byte, msg proto.Message) error {
pb.buf.SetBuf(data)
return pb.buf.Unmarshal(msg)
}
// メッセージサイズの事前計算
func efficientMarshal(msg proto.Message) ([]byte, error) {
size := proto.Size(msg)
buf := make([]byte, size)
n, err := proto.MarshalOptions{}.MarshalAppend(buf[:0], msg)
if err != nil {
return nil, err
}
return n, nil
}
実践的な使用例
gRPCサービスの定義
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (UserList);
rpc CreateUser(CreateUserRequest) returns (User);
rpc UpdateUser(UpdateUserRequest) returns (User);
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
}
message GetUserRequest {
int64 id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message CreateUserRequest {
User user = 1;
}
message UpdateUserRequest {
User user = 1;
google.protobuf.FieldMask update_mask = 2;
}
message DeleteUserRequest {
int64 id = 1;
}
イベントストリーミング
type EventStream struct {
writer io.Writer
}
func (es *EventStream) WriteEvent(event proto.Message) error {
// メッセージサイズを書き込み(varintエンコーディング)
size := proto.Size(event)
if err := binary.Write(es.writer, binary.LittleEndian, uint32(size)); err != nil {
return err
}
// メッセージを書き込み
data, err := proto.Marshal(event)
if err != nil {
return err
}
_, err = es.writer.Write(data)
return err
}
func (es *EventStream) ReadEvent(msg proto.Message) error {
// サイズを読み込み
var size uint32
if err := binary.Read(es.reader, binary.LittleEndian, &size); err != nil {
return err
}
// メッセージを読み込み
data := make([]byte, size)
if _, err := io.ReadFull(es.reader, data); err != nil {
return err
}
return proto.Unmarshal(data, msg)
}
設定ファイルの管理
type Config struct {
pb *pb.AppConfig
mu sync.RWMutex
path string
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
config := &pb.AppConfig{}
// テキストフォーマットから読み込み
if err := prototext.Unmarshal(data, config); err != nil {
// バイナリフォーマットを試す
if err := proto.Unmarshal(data, config); err != nil {
return nil, err
}
}
return &Config{
pb: config,
path: path,
}, nil
}
func (c *Config) Save() error {
c.mu.RLock()
defer c.mu.RUnlock()
// テキストフォーマットで保存(人間が読める)
data, err := prototext.MarshalOptions{
Multiline: true,
Indent: " ",
}.Marshal(c.pb)
if err != nil {
return err
}
return os.WriteFile(c.path, data, 0644)
}
パフォーマンス特性
バイナリフォーマットの効率性
- Varintエンコーディング: 小さな整数値に少ないバイト数
- フィールドの省略: デフォルト値のフィールドはシリアライズされない
- コンパクトなタグ付け: フィールド番号と型情報を1バイトで表現
比較ベンチマーク
// ベンチマーク結果の例
// BenchmarkProtoMarshal-8 1000000 1050 ns/op 576 B/op 1 allocs/op
// BenchmarkJSONMarshal-8 300000 4250 ns/op 1536 B/op 18 allocs/op
// BenchmarkXMLMarshal-8 100000 15890 ns/op 8672 B/op 99 allocs/op
ベストプラクティス
1. 新APIへの移行
// 旧: github.com/golang/protobuf
import "github.com/golang/protobuf/proto"
// 新: google.golang.org/protobuf
import "google.golang.org/protobuf/proto"
2. スキーマ設計のポイント
// 良い例: 明確なフィールド名と適切な型
message User {
int64 user_id = 1; // 一意識別子
string email = 2; // メールアドレス
repeated string roles = 3; // 複数のロール
}
// 避けるべき: 曖昧なフィールド名
message Data {
string value1 = 1;
string value2 = 2;
}
3. バージョニング
// 後方互換性を保つためのルール
// 1. フィールド番号を変更しない
// 2. 必須フィールドを追加しない(proto3ではすべてoptional)
// 3. フィールドを削除する場合はreservedを使用
message User {
reserved 3, 4, 5; // 削除されたフィールド番号
reserved "old_field", "deprecated_field"; // 削除されたフィールド名
int64 id = 1;
string name = 2;
// int32 old_field = 3; // 削除済み
string email = 6; // 新しいフィールド
}
まとめ
Protocol Buffersは、Googleが開発した強力なシリアライゼーションフォーマットで、特に大規模なシステムやマイクロサービスアーキテクチャで幅広く使用されています。スキーマ定義による型安全性、高効率なバイナリエンコーディング、言語間の互換性などの特徴により、信頼性の高いデータ交換を実現します。新APIへの移行を推奨しますが、レガシーAPIも互換性のために維持されています。