こんにちは、daihaseです。
今回はGoとProtocol Buffersを使った記事を書きたいと思います。 まずProtocol Buffersですが、こちらはGoogleによって2008年に開発されたもので、一言でいうと言語やプラットフォームに依存せずに構造化されたデータをシリアライズ可能な仕組みといった感じでしょうか。
.protoというファイルに記載されたインターフェース定義言語(IDL) によって、クライアント/サーバーが共通で使用する構造を定義でき、これによってXMLやJSONと同じようにAPI通信の際に用いることが出来ます。
恐らく多くの方が使い慣れているJSONなどと比較してのメリットは、データサイズがとても小さいことや、軽量で高速、またプログラム上から簡単に使用出来るデータアクセスクラスを生成する、など色々ありますが、まずは簡単なサンプルにて使ってこちらを実感してみましょう。
※事前にGoの開発環境は構築済みとします。
Protocol Buffers コンパイラのインストール
まずProtocol Buffersを各種言語にコンパイルするために、コンパイラとターゲット言語用のランタイムが必要となります。
ターミナルより、下記にてコンパイラをインストールします。
$ brew install protobuf
次にGoを使って構造体をProtocol Buffersバイナリー形式にシリアライズするための、専用のGo protoドライバーをインストールする必要があります。
$ go get github.com/golang/protobuf/proto
これで下準備は完了です。
.protoファイルの作成
早速プロジェクトファイルを作っていきます。 GOPATHの通った場所にprotobufsSampleというフォルダを作成します。
mkdir $GOPATH/src/github.com/"name"/protobufsSample
次に.protoファイルを格納するためのprotofilesフォルダを作成します。
mkdir $GOPATH/src/github.com/"name"/protifiles
では作ったprotofilesフォルダにperson.protoファイルを作成し、以下のように記述します。
syntax = "proto3"; package protofiles; message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; } message AddressBook { repeated Person people = 1; }
XMLやJSONに慣れていれば、何となく雰囲気で構造が理解出来きそうですね。
まずメッセージタイプを定義します。上記でいうとPersonですね。PhoneNumberのように他のメッセージ型の中にメッセージタイプを定義することも出来ます。
また見てわかるようにメッセージ定義の各フィールドにはユニークな数値タグがあります。数値は1から始まり、これを元にフィールドを区別するため、基本的に一度決めたタグは変更はしないほうがいいでしょう。
なおenumで定義されているPhoneTypeは最初の定数は 0 にマップされており、enum定義は全て0から始める必要があると覚えておきましょう。
他には、他のメッセージ型をフィールドの型として使うことも出来ます。上記の場合は、Personメッセージ中にPhoneNumberメッセージを含めたいので、フィールドphonesの型をPhoneNumberとしています。
最後に、repeatedは後続する型の配列であることを宣言するためのものになります。
ざっくりとこんな感じでしょうか。
.protoファイルのコンパイル
作った.protoはそのままではGoから使うことができないので、これをコンパイルする必要があります。.protoを作成したprotifilesフォルダに移動し、以下コマンドを叩いてください。
$ protoc --go_out=. *.proto
すると.protoファイルと同じ名前のperson.pb.goというファイルが生成されているかと思います。中は以下のようになっています。
// Code generated by protoc-gen-go. DO NOT EDIT. // source: person.proto package protofiles import proto "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Person_PhoneType int32 const ( Person_MOBILE Person_PhoneType = 0 Person_HOME Person_PhoneType = 1 Person_WORK Person_PhoneType = 2 ) var Person_PhoneType_name = map[int32]string{ 0: "MOBILE", 1: "HOME", 2: "WORK", } var Person_PhoneType_value = map[string]int32{ "MOBILE": 0, "HOME": 1, "WORK": 2, } func (x Person_PhoneType) String() string { return proto.EnumName(Person_PhoneType_name, int32(x)) } func (Person_PhoneType) EnumDescriptor() ([]byte, []int) { return fileDescriptor_4c9e10cf24b1156d, []int{0, 0} } type Person struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Person) Reset() { *m = Person{} } func (m *Person) String() string { return proto.CompactTextString(m) } func (*Person) ProtoMessage() {} func (*Person) Descriptor() ([]byte, []int) { return fileDescriptor_4c9e10cf24b1156d, []int{0} } func (m *Person) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Person.Unmarshal(m, b) } func (m *Person) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Person.Marshal(b, m, deterministic) } func (m *Person) XXX_Merge(src proto.Message) { xxx_messageInfo_Person.Merge(m, src) } func (m *Person) XXX_Size() int { return xxx_messageInfo_Person.Size(m) } func (m *Person) XXX_DiscardUnknown() { xxx_messageInfo_Person.DiscardUnknown(m) } var xxx_messageInfo_Person proto.InternalMessageInfo func (m *Person) GetName() string { if m != nil { return m.Name } return "" } func (m *Person) GetId() int32 { if m != nil { return m.Id } return 0 } func (m *Person) GetEmail() string { if m != nil { return m.Email } return "" } func (m *Person) GetPhones() []*Person_PhoneNumber { if m != nil { return m.Phones } return nil } type Person_PhoneNumber struct { Number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"` Type Person_PhoneType `protobuf:"varint,2,opt,name=type,proto3,enum=protofiles.Person_PhoneType" json:"type,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Person_PhoneNumber) Reset() { *m = Person_PhoneNumber{} } func (m *Person_PhoneNumber) String() string { return proto.CompactTextString(m) } func (*Person_PhoneNumber) ProtoMessage() {} func (*Person_PhoneNumber) Descriptor() ([]byte, []int) { return fileDescriptor_4c9e10cf24b1156d, []int{0, 0} } func (m *Person_PhoneNumber) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Person_PhoneNumber.Unmarshal(m, b) } func (m *Person_PhoneNumber) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Person_PhoneNumber.Marshal(b, m, deterministic) } func (m *Person_PhoneNumber) XXX_Merge(src proto.Message) { xxx_messageInfo_Person_PhoneNumber.Merge(m, src) } func (m *Person_PhoneNumber) XXX_Size() int { return xxx_messageInfo_Person_PhoneNumber.Size(m) } func (m *Person_PhoneNumber) XXX_DiscardUnknown() { xxx_messageInfo_Person_PhoneNumber.DiscardUnknown(m) } var xxx_messageInfo_Person_PhoneNumber proto.InternalMessageInfo func (m *Person_PhoneNumber) GetNumber() string { if m != nil { return m.Number } return "" } func (m *Person_PhoneNumber) GetType() Person_PhoneType { if m != nil { return m.Type } return Person_MOBILE } type AddressBook struct { People []*Person `protobuf:"bytes,1,rep,name=people,proto3" json:"people,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *AddressBook) Reset() { *m = AddressBook{} } func (m *AddressBook) String() string { return proto.CompactTextString(m) } func (*AddressBook) ProtoMessage() {} func (*AddressBook) Descriptor() ([]byte, []int) { return fileDescriptor_4c9e10cf24b1156d, []int{1} } func (m *AddressBook) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_AddressBook.Unmarshal(m, b) } func (m *AddressBook) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_AddressBook.Marshal(b, m, deterministic) } func (m *AddressBook) XXX_Merge(src proto.Message) { xxx_messageInfo_AddressBook.Merge(m, src) } func (m *AddressBook) XXX_Size() int { return xxx_messageInfo_AddressBook.Size(m) } func (m *AddressBook) XXX_DiscardUnknown() { xxx_messageInfo_AddressBook.DiscardUnknown(m) } var xxx_messageInfo_AddressBook proto.InternalMessageInfo func (m *AddressBook) GetPeople() []*Person { if m != nil { return m.People } return nil } func init() { proto.RegisterType((*Person)(nil), "protofiles.Person") proto.RegisterType((*Person_PhoneNumber)(nil), "protofiles.Person.PhoneNumber") proto.RegisterType((*AddressBook)(nil), "protofiles.AddressBook") proto.RegisterEnum("protofiles.Person_PhoneType", Person_PhoneType_name, Person_PhoneType_value) } func init() { proto.RegisterFile("person.proto", fileDescriptor_4c9e10cf24b1156d) } var fileDescriptor_4c9e10cf24b1156d = []byte{ // 251 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x90, 0x41, 0x4b, 0xc3, 0x40, 0x10, 0x85, 0xcd, 0x36, 0x5d, 0xec, 0x8b, 0x94, 0x30, 0x88, 0x04, 0x11, 0x09, 0x39, 0x05, 0x85, 0x20, 0x15, 0x04, 0x8f, 0x16, 0x0a, 0x8a, 0xd6, 0x94, 0x45, 0xe8, 0xb9, 0x25, 0x23, 0x06, 0x93, 0xec, 0x92, 0xad, 0x87, 0xde, 0xfc, 0xe9, 0x92, 0x4d, 0x50, 0x41, 0x3c, 0xed, 0x9b, 0x99, 0x8f, 0x79, 0x6f, 0x07, 0x47, 0x86, 0x5b, 0xab, 0x9b, 0xcc, 0xb4, 0x7a, 0xa7, 0x09, 0xee, 0x79, 0x2d, 0x2b, 0xb6, 0xc9, 0xa7, 0x80, 0x5c, 0xb9, 0x21, 0x11, 0xfc, 0x66, 0x53, 0x73, 0xe4, 0xc5, 0x5e, 0x3a, 0x51, 0x4e, 0xd3, 0x14, 0xa2, 0x2c, 0x22, 0x11, 0x7b, 0xe9, 0x58, 0x89, 0xb2, 0xa0, 0x63, 0x8c, 0xb9, 0xde, 0x94, 0x55, 0x34, 0x72, 0x50, 0x5f, 0xd0, 0x0d, 0xa4, 0x79, 0xd3, 0x0d, 0xdb, 0xc8, 0x8f, 0x47, 0x69, 0x30, 0x3b, 0xcf, 0x7e, 0x1c, 0xb2, 0x7e, 0x7b, 0xb6, 0xea, 0x80, 0xe7, 0x8f, 0x7a, 0xcb, 0xad, 0x1a, 0xe8, 0xd3, 0x35, 0x82, 0x5f, 0x6d, 0x3a, 0x81, 0x6c, 0x9c, 0x1a, 0x22, 0x0c, 0x15, 0x5d, 0xc1, 0xdf, 0xed, 0x0d, 0xbb, 0x18, 0xd3, 0xd9, 0xd9, 0x7f, 0xcb, 0x5f, 0xf6, 0x86, 0x95, 0x23, 0x93, 0x4b, 0x4c, 0xbe, 0x5b, 0x04, 0xc8, 0x65, 0x3e, 0x7f, 0x78, 0x5a, 0x84, 0x07, 0x74, 0x08, 0xff, 0x3e, 0x5f, 0x2e, 0x42, 0xaf, 0x53, 0xeb, 0x5c, 0x3d, 0x86, 0x22, 0xb9, 0x45, 0x70, 0x57, 0x14, 0x2d, 0x5b, 0x3b, 0xd7, 0xfa, 0x9d, 0x2e, 0x20, 0x0d, 0x6b, 0x53, 0x75, 0x87, 0xe8, 0x3e, 0x43, 0x7f, 0xfd, 0xd4, 0x40, 0x6c, 0xa5, 0x1b, 0x5d, 0x7f, 0x05, 0x00, 0x00, 0xff, 0xff, 0x7e, 0x71, 0x4d, 0x40, 0x60, 0x01, 0x00, 0x00, }
ちょっとボリュームがありますね...。ざっと見た感じ、.protoファイルで定義したPersonやAddressBookなどの構造体が作成され、それぞれにゲッター/セッターが生成されているのがわかります。
Goから生成されたperson.pb.goを使ってみる
では最後に、この生成されたperson.pb.goのPerson構造体を使用するmain.goを作成します。protobufsSampleフォルダ直下にmain.goを作成し、以下のようなコードを記述します。
package main import ( "fmt" pb "github.com/daihase/protobufsSample/protofiles" "github.com/golang/protobuf/proto" ) func main() { p := &pb.Person{ Id: 1234, Name: "Roger F", Email: "rf@example.com", Phones: []*pb.Person_PhoneNumber{ {Number: "555-4321", Type: pb.Person_HOME}, }, } p1 := &pb.Person{} body, _ := proto.Marshal(p) _ = proto.Unmarshal(body, p1) fmt.Println("Original struct loaded from proto file:", p, "\n") fmt.Println("Marshaled proto data: ", body, "\n") fmt.Println("Unmarshaled struct: ", p1) }
protofilesパッケージからProtocol Bufferを読み込むためにインポートしていますが、ここは自分の環境にあわせたパスになるのでご注意ください。
コードを見ていきますと、まずperson.pb.goで定義されているPerson構造体、こちらを使って新たに構造体を作成し変数pへ代入しています。次にprotofileのPerson構造体を初期化し、p1に代入しています。最初に生死した変数pをproto.Marshal関数を使ってシリアライズ化しbodyへ代入、最後にそのbodyを使ってp1にJSON形式のものをパースするといった流れです。
Printlnでそれぞれの値を出力するようになってますので、早速以下のコマンドを叩いて確認してみましょう。
$ go run main.go
すると以下のような結果がターミナル上に出力されるかと思います。
Original struct loaded from proto file: name:"Roger F" id:1234 email:"rf@example.com" phones:<number:"555-4321" type:HOME > Marshaled proto data: [10 7 82 111 103 101 114 32 70 16 210 9 26 14 114 102 64 101 120 97 109 112 108 101 46 99 111 109 34 12 10 8 53 53 53 45 52 51 50 49 16 1] Unmarshaled struct: name:"Roger F" id:1234 email:"rf@example.com" phones:<number:"555-4321" type:HOME >
ちゃんとそれぞれの値が意図したように出力されていますね。
このサンプルだけだとJSONの代わりにProtocol Bufferを使う利点がいまいち見えてこないかもしれません。実際は今回サンプルに用いたmainとprotobufs、この2つのファイルは少ないオーバーヘッドで通信が行われています。
バイナリのサイズはテキストよりも小さいので、プロトコルマーシャリングされたデータのサイズはJSONよりも小さくなるのです。これが大きいサービスとかになってくると結構差も出てくるかと思います。また今回紹介したProtocol Buffersは何かと人気?なgRPCでもデータ層に用いられ、サーバー/クライアント間のAPI通信において非常に効力を発揮しています。
インターフェース定義言語を用いて.protoファイルとして定義し、サーバー/クライアントに必要なコードの雛形を生成、しかもGoだけでなく複数の言語に対応、これだけでも非常に便利でワクワクする技術と思いませんか!?
今度はそのgRPCを使い、よりProtocol Buffersの有用性を紹介出来たらと思います。
ちなみに今回作ったサンプルはGitHubにもあげてありますのでよかったらどうぞ〜
それでは良い開発ライフを〜