Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Goでサブディレクトリを含むProtocol Buffers定義ファイルを扱う場合の上手な書き方

この記事はQiitaで公開されていました

問題

Goは、$GOPATH/src/ 以下にパッケージのソースコードを置くルール。自分で書いたコードも例外ではないので、以下の例では、自分で書くコードを $GOPATH/src/example.com/app に置いている。

$GOPATH/
└ ─ /src
    ├ ─ /example.com
    │   └ ─ /corp
    │       └ ─ /app
    │           └ ─ /rpc
    │               ├ ─ /user
    │               ├ ─ /blob
    │               └ ─ /log
    └ ─ /example.org
        └ ─ /app
            └ ─ /rpc
                └ ─ /main.go

Goでは、型や変数をpackagename.TypeNameのように書くので、UserMessageという型を、パッケージを分けてuser.Messageとする場合がある。だけども上の通り、別パッケージにするにはディレクトリを分ける必要があるので、Protocol Buffersコンパイラ(protoc)で生成するコードも例外なく、パッケージを分けるなら別の階層に置かなければならない。

このような、 Protocol Buffers(.proto)ファイルがサブディレクトリの.protoファイルを参照する 場合、出力先やオプションを適切に使わないと手間がかかったりビルドできなかったりする。

この記事で想定するProtocol Buffers定義の階層

$GOPATH/src/example.com/app/rpc 以下に .proto ファイルを全て置いた状態を想定する。

$GOPATH/src/example.com/
└ ─ /app
    └ ─ /rpc
        ├ ─ /user
        │   └ ─ /user.proto
        ├ ─ /blob
        │   └ ─ /blob.proto
        ├ ─ /log
        │   └ ─ /log.proto
        └ ─ /main.proto

最終的にどうすればいいか

protoファイルの書き方

参照されるだけの .proto ファイルは、option go_packageで正しいimport pathを設定しておく。以下は user.proto ファイルの例。

package user;
option go_package = "example.com/app/rpc/user";

参照する側の .proto ファイルは、上記に加えて、importの書き方をファイル階層に合わせる。

package rpc;
option go_package = "example.com/app/rpc";

import "user/user.proto";

protoc-gen-goのオプション

Goのコードを生成する場合は以下のコマンドで行う。

$ protoc -Irpc --go_out=plugins=grpc:$GOPATH/src rpc/main.proto
$ protoc -Irpc --go_out=plugins=grpc:$GOPATH/src rpc/user/user.proto
  • -Iオプションで.protoファイルのimportで参照するルートディレクトリを指定する
  • --go_out=オプションで出力先を変更する

Ruby等は、そのまま実行すれば良さそう。

$ plugin="protoc-gen-grpc=$(which grpc_tools_ruby_protoc_plugin)"
$ protoc -Irpc --ruby_out=. --grpc_out=. --plugin=$plugin rpc/main.proto
$ protoc -Irpc --ruby_out=. --grpc_out=. --plugin=$plugin rpc/user/user.proto

この書き方で使い回しができる .proto になる。

色々な失敗案

参考のため、試行錯誤した結果と使えない理由をまとめた。

デフォルトの動作

ディレクトリのファイルを参照する .proto ファイルは、特にオプションを入れない場合、生成された.pb.goファイルのimportパスが $GOPATH/src/ からになっていない。例えば main.proto ファイルで

syntax = "proto3";

package rpc;
import "user/user.proto";

と書くと、生成されたGoのコードは

package rpc

import "user"

のようなimportになってしまってパッケージを読み込めない。

go_packageオプション

この問題に対応するため、.proto ファイルのオプションでoption go_packageがあるけど、これを書いてしまうと、Goソースコードの生成先に、さらにパッケージ階層が作られてしまう。

参照される側:

syntax = "proto3";

package user;
option go_package = "example.com/app/rpc/user";

参照する側:

syntax = "proto3";

package rpc;
option go_package = "example.com/app/rpc";
import "user/user.proto";

生成コマンド:

$ protoc -Irpc --go_out=plugins=grpc:. rpc/main.proto
$ protoc -Irpc --go_out=plugins=grpc:. rpc/user/user.proto

結果:

$GOPATH/
└ ─ /src
    └ ─ /example.com
        └ ─ /app
            ├ ─ /example.com      <- 余計な階層が作られる
            │   └ ─ /app
            │       └ ─ /rpc
            │           ├ ─ /user
            │           │   └ ─ /user.pb.go
            │           └ ─ /main.pb.go
            └ ─ /rpc
                ├ ─ /user
                │   └ ─ /user.proto
                ├ ─ /blob
                │   └ ─ /blob.proto
                ├ ─ /log
                │   └ ─ /log.proto
                └ ─ /main.proto

protoc-gen-goのオプションを試す

protoc--go_out=オプション経由で、カンマ区切り文字列を指定すれば、protoc-gen-goへオプションを与えることができるがどれも微妙。

import_prefix

このオプションは、指定した文字列が標準パッケージ以外全ての先頭に付く。protocで生成したパッケージだけに付くなら問題ないけれど、github.com/golang/protobuf/protoの頭にもついてしまうのでよくない。

import_path

これは .proto ファイルにoption go_packageがない場合のみ、コマンドラインから与えることができるオプション。なので動作としてはoption go_packageと何も違いがない。

vendorに入れてみる

--go_out=で出力先ディレクトリが決められるので、option go_packageで階層が掘られてしまうならvendor以下に入れてしまおう案。

基本的にはうまく動いたけれど、depで管理していないパッケージがvendor以下に入ってしまうため、dep initdep ensureがエラーになってしまう。現状では無視することもできなさそうなので、使えない。