Libra Studio Log

開発に関することやゲーム、ガジェットなどについてつらつらと書き記しています

GoでProtocol Buffersを使う

f:id:daihase:20190809022658p:plain

こんにちは、daihaseです。

今回はGoProtocol 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ファイルで定義したPersonAddressBookなどの構造体が作成され、それぞれにゲッター/セッターが生成されているのがわかります。

 

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にもあげてありますのでよかったらどうぞ〜

それでは良い開発ライフを〜