protobufとgrpcのGoコード生成先ディレクトリの指定を、protocコマンドのオプションで行う
gRPC 公式の Quick startを見ると、以下のコマンドが載っているのですが、ちょっと複雑でドキュメントを読んだだけではきちんと理解できませんでした。
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
そこで、手を動かしながらprotocコマンドで Go コードを生成してみた記録を本記事にまとめます。
簡単のため `--go_out` と `--go_opt` だけに注目
先程のコマンドを実行すると以下で示す 2 つのファイルが生成されます。
+ project_root_dir
+ helloworld
| helloworld.proto
| helloworld.pb.go // <- 生成されたGoコード
| helloworld_grpc.pb.go // <- 生成されたGoコード
protocコマンドの 4 つのオプション--go_out --go_opt --go-grpc_out --go-grpc_opt は、生成された Go コードがどのディレクトリに保存されるかを制御していて、まとめるとこうです。
_outオプション |
_opt オプション |
生成ファイル名 | |
|---|---|---|---|
| protobuf コード生成 | --go_out |
--go_opt |
hello.pb.go |
| grpc コード生成 | --go-grpc_out |
--go-grpc_opt |
hell_grpc.pb.go |
前半 2 つの「protobuf コード生成」に関わる--go_out と --go_optが理解できれば、後半 2 つも同時に理解できます。そこで、本記事ではこれより先は--go_out と --go_optだけに注目します。
protoc --go_out=. --go_opt=paths=source_relative \
helloworld/helloworld.proto
上記のように、注目すべきコマンドがスッキリしました!
0. 事前準備
まずは作業用ディレクトリを準備します。
mkdir protoc-go-experiments
cd protoc-go-experiments
つぎに helloworld ディレクトリを準備します。
mkdir helloworld
cd helloworld # protoc-go-experiments/helloworld
# ディレクトリ構造
+ protoc-go-experiments
+ helloworld # カレント・ディレクトリ
そしてhelloworld.protoファイルを作成しましょう。
# protoc-go-experiments/helloworld/helloworld.proto
cat << EOF > helloworld.proto
syntax = "proto3";
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
EOF
以下のようなディレクトリ構成で準備完了です。
+ protoc-go-experiments
+ helloworld # <- カレントディレクトリ
| helloworld.proto
1. .protoファイルと同じディレクトリからprotoc実行
それでは.protoファイルが配置されている、helloworldディレクトリ内からprotocコマンドを実行します。
# protoc-go-experiments/helloworld ディレクトリ内で以下を実行
protoc helloworld.proto # 引数で`.proto`ファイル名のみ指定
Missing output directives.
1.1 エラーを解決しながら動作を学ぶ
このエラーは --go_out オプションの指定で解決できます。
The argument to the
go_outflag is the directory where you want the compiler to write your Go output. - Protocol Buffers 公式: Go Generated Code
まずは「--go_out=.という形で、=の右側には.を置くものである」という単純化したルールを仮定します。
protoc --go_out=. helloworld.proto
すると、以下のエラーが表示されます。
protoc-gen-go: unable to determine Go import path for "helloworld.proto"
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.
上記のリンク先 https://developers.google.com/protocol-buffers/docs/reference/go-generated#package に従って、.protoファイル内でgo_packageを指定しましょう。
a "M" argument on the command line. はどうなる?
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
というエラーメッセージからわかるように、go_package の指定以外にも"M"フラグを使う方法があります。より具体的には--go_optにM${PROTO_FILE}=${GO_IMPORT_PATH}という形式でパッケージを指定する方法で、上記リンク先にも説明があります。
しかし、本記事ではgo_package を指定する方法のみを解説し、--go_optにM${PROTO_FILE}=${GO_IMPORT_PATH}という形式を利用する方法は、機会があれば別の記事で紹介したいと思います。「
Go パッケージ・パスの慣習に従うと、 Go パッケージは (github.com 前提で開発する場合) github.com/__github_username 以下に作成することになるので、go_package を次のように指定します。
syntax = "proto3";
// __github_username は自身のものに置き換えて下さい。
+ option go_package = "github.com/__github_username /protoc-go-experiments/helloworld";
// The greeting service definition.
service Greeter {
もう一度先程のコマンドを実行します。
protoc --go_out=. helloworld.proto
使いづらそうな深くネストしたディレクトリ構造になってしまいました。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
+ github.com # <- これ以下が生成されたGoコード
+ __github_username
+ protoc-go-experiments
+ helloworld
| helloworld.pb.go
いったんディレクトリごと生成結果を削除します。
rm -rf github.com
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
これで、次の実験への準備ができました。
1.2 --go_optにpaths=source_relativeを指定
ネストを避けるため、--go_optにpaths=source_relativeをつけます。
If the
paths=source_relativeflag is specified, the output file is placed in the same relative directory as the input file. - Protocol Buffers 公式: Go Generated Code
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld.proto
ディレクトリ構造がスッキリしました。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
| helloworld.pb.go # <- これが生成されたGoコード
それではここで生成した Go コードを一旦消去しましょう。
rm helloworld.pb.go
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
これで、次の実験への準備ができました。
1.3 --go_outの変更
これまでは以下の仮定のもと進めてきました。
まずは「
--go_out=.という形で、=の右側には.を置くものである」という単純化したルールを仮定します。
ここでは、その仮定を変えて、--go_outに違う値outdirを指定してみます。
protoc \
--go_out=outdir --go_opt=paths=source_relative \
helloworld.proto
outdir/: No such file or directory
outdirが存在しないことでエラーになってしまったので、outdirを作成します。
mkdir outdir
+ protoc-go-experiments
+ helloworld
| helloworld.proto
+ outdir
先程のコマンドをもう一度実行します。
protoc \
--go_out=outdir --go_opt=paths=source_relative \
helloworld.proto
+ protoc-go-experiments
+ helloworld
| helloworld.proto
+ outdir
| helloworld.pb.go # <- これが生成されたGoコードo
これで、--go_opt=paths=source_relativeと指定したときに、-go_outでどのように Go コード生成先ディレクトリを制御できるかわかりました。
それでは生成した Go コードを一旦消去しましょう。
rm -rf outdir
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
これで、次の実験への準備ができました。
1.4 ここまでのまとめ
- Go コード生成には
--go_outオプションを指定する。 -
--go_opt=paths=source_relativeをつけるとgithub.com/__github_username/...という深くネストされたディレクトリに保存されなくなる -
--go_opt=paths=source_relativeのもとで、--go_out=outidrのような指定で、Go コード生成先ディレクトリを制御できる
2. プロジェクトルートからprotocを実行
多くの grpc 利用プロジェクトでは、プロジェクトルート・ディレクトリからprotocを実行することと思います。本記事でもここからはプロジェクトルートに移動してprotocを試します。
cd ../
+ protoc-go-experiments # <- カレントディレクトリ
+ helloworld
| helloworld.proto
先ほどと同じコマンドを実行します。
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld.proto
helloworld.proto: No such file or directory
エラーを解決するため、引数helloworld.protoをhelloworld/helloworld.protoに変えます。
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/helloworld.proto
- のときと同じ生成結果を再現できました。
+ protoc-go-experiments
+ helloworld
| helloworld.pb.go # <- これが生成されたGoコード
| helloworld.proto
それでは生成した Go コードを一旦消去しましょう。
rm -rf helloworld/helloworld.pb.go
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
これで、次の実験への準備ができました。
3. 複数.protoファイルの利用
もう一つの.protoファイルを作成しましょう。
cat << EOF > helloworld/greeting.proto
syntax = "proto3";
option go_package = "github.com/richardimaoka/protoc-go-experiments/helloworld";
message Greeting { string greet_message = 1; }
EOF
+ protoc-go-experiments
+ helloworld
| greeting.proto
| helloworld.proto
複数.protoファイル利用時は、protocコマンドの引数でワイルドカードを使うと便利です。
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto
+ protoc-go-experiments
+ helloworld
| greeting.pb.go # <- これが生成されたGoコード
| greeting.proto
| helloworld.pb.go # <- これが生成されたGoコード
| helloworld.proto
生成した Go コードを一旦消去しましょう。
rm helloworld/greeting.pb.go
rm helloworld/helloworld.pb.go
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| greeting.proto
| helloworld.proto
これで、次の実験への準備ができました。
3.1 サブディレクトリ
次に、サブディレクトリに.protoファイルを配置します。
mkdir helloworld/subdir
cat << EOF > helloworld/subdir/subhello.proto
syntax = "proto3";
option go_package = "github.com/richardimaoka/protoc-go-experiments/helloworld/subdir";
// The greeting service definition.
service SubGreeter {
// Sends a greeting
rpc SayHello(SubHelloRequest) returns (SubHelloReply) {}
}
// The request message containing the user's name.
message SubHelloRequest { string name = 1; }
// The response message containing the greetings
message SubHelloReply { string message = 1; }
EOF
+ protoc-go-experiments
+ helloworld
| helloworld.proto
| greeting.proto
+ subdir
| subhello.proto
引数にhelloworldとhelloworld/subdirの両方を指定してコマンドを実行します。
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
+ protoc-go-experiments
+ helloworld
| helloworld.pb.go # <- これが生成されたGoコード
| helloworld.proto
| greeting.pb.go # <- これが生成されたGoコード
| greeting.proto
+ subdir
| subhello.pb.go # <- これが生成されたGoコード
| subhello.proto
生成した Go コードを一旦消去しましょう。
rm helloworld/greeting.pb.go
rm helloworld/helloworld.pb.go
rm helloworld/subdir/subhello.pb.go
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
| greeting.proto
+ subdir
| subhello.proto
これで、次の実験への準備ができました。
4. import を利用する
helloworld.protoから、別ファイルgreeting.protoで定義したmessage Greetingを利用しようとするとどうなるでしょう?
message HelloReply {
string message = 1;
+ Greeting greet = 2;
}
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
"Greeting" seems to be defined in "greeting.proto", which is not imported by "helloworld.proto".
To use it here, please add the necessary import.
4.1 importエラーの解決
エラーの解決には、エラーメッセージにあるように、helloworld.protoファイルの中で、greeting.protoファイルをimportします。
You can use definitions from other .proto files by importing them - Protocol Buffers 公式: Language Guide (proto3)
syntax = "proto3";
option go_package = "github.com/richardimaoka/protoc-go-experiments/helloworld";
+ import "greeting.proto";
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
しかし、まだエラー出ます。
greeting.proto: File not found.
helloworld/helloworld.proto: Import "greeting.proto" was not found or had errors.
helloworld/helloworld.proto:19:3: "Greeting" seems to be defined in "helloworld/greeting.proto",
which is not imported by "helloworld/helloworld.proto".
To use it here, please add the necessary import.
エラーメッセージにimported by "helloworld/helloworld.proto"とあるのでimportのパスを書き換えましょう。
syntax = "proto3";
option go_package = "github.com/richardimaoka/protoc-go-experiments/helloworld";
- import "greeting.proto";
+ import "helloworld/greeting.proto";
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
エラーが解決できました。
+ protoc-go-experiments
+ helloworld
| greeting.pb.go # <- これが生成されたGoコード
| greeting.proto
| helloworld.pb.go # <- これが生成されたGoコード
| helloworld.proto
+ subdir
| subhello.pb.go # <- これが生成されたGoコード
| subhello.proto
生成した Go コードを一旦消去しましょう。
rm helloworld/greeting.pb.go
rm helloworld/helloworld.pb.go
rm helloworld/subdir/subhello.pb.go
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
| greeting.proto
+ subdir
| subhello.proto
これで、次の実験への準備ができました。
4.2 --proto_path (省略形は-I)オプションの動作確認
先程のエラーは--proto_pathを指定によっても解決できます
IMPORT_PATHspecifies a directory in which to look for.protofiles when resolvingimportdirectives. - Protocol Bullfers 公式: Language Guide (proto3)
それではhelloworld.protoのimportパスを戻しましょう。
syntax = "proto3";
option go_package = "github.com/richardimaoka/protoc-go-experiments/helloworld";
- import "helloworld/greeting.proto";
+ import "greeting.proto";
--proto_path=helloworldを指定してコマンドを実行します。
protoc \
--proto_path=helloworld \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
pb.goファイルは、プロジェクトルートであるprotoc-go-experimentsに配置されてしまいました。先程までと配置が変わっています。
+ protoc-go-experiments
| greeting.pb.go # <- これが生成されたGoコード
| helloworld.pb.go # <- これが生成されたGoコード
+ subdir
| | subhello.pb.go # <- これが生成されたGoコード
|
+ helloworld # これ以下は.protoファイル
| helloworld.proto
| greeting.proto
+ subdir
| subhello.proto
ここまではhelloworldディレクトリの中に.protoファイルも.pb.goファイルも配置していたので、同じ配置を再現していきましょう。
生成した Go コードを一旦消去します。
rm greeting.pb.go
rm helloworld.pb.go
rm -rf subdir
+ protoc-go-experiments
+ helloworld
| greeting.proto
| helloworld.proto
+ subdir
| subhello.proto
--go_out=helloworldで Go コード生成先ディレクトリを変更します。
protoc \
--proto_path=helloworld \
--go_out=helloworld --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
+ protoc-go-experiments
+ helloworld
| greeting.pb.go # <- これが生成されたGoコード
| greeting.proto
| helloworld.pb.go # <- これが生成されたGoコード
| helloworld.proto
+ subdir
| subhello.pb.go # <- これが生成されたGoコード
| subhello.proto
たしかにこれまでの Go コード生成先と同じ状態を再現できましたが、この--proto_pathと--go_outの指定方法はベストプラクティスに沿っているのでしょうか?次からはこの点について見ていきます。
生成した Go コードを一旦消去しましょう。
rm helloworld/greeting.pb.go
rm helloworld/helloworld.pb.go
rm helloworld/subdir/subhello.pb.go
.protoファイルのみが残ります。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
| greeting.proto
+ subdir
| subhello.proto
これで、次の実験への準備ができました。
5. --proto_path --go_out --go_opt の指針
Protocol Buffers 公式ドキュメントには、.protoファイルの置き場所や、それに伴う--proto_path --go_out --go_opt 指定の指針が書いてあります。
まずは.protoファイルの置き場所です。
Prefer not to put
.protofiles in the same directory as other language sources. Consider creating a subpackageprotofor.protofiles, under the root package for your project. - Protocol Buffers 公式: Language Guide (proto3)
本記事ではprotoディレクトリの代わりに、helloworldディレクトリ以下に.protoファイルを配置しました。
+ protoc-go-experiments
+ helloworld
| helloworld.proto
| greeting.proto
+ subdir
| subhello.proto
ある程度以上の規模の開発プロジェクトであれば、ある.protoファイルから、別の.protoファイルをimportすることになるでしょう。
importには--proto_pathの指定が重要になり、仮に--proto_pathを指定しなかったとしても暗黙的に指定されていることになります。
If no flag was given, it looks in the directory in which the compiler was invoked. - Protocol Buffers 公式: Language Guide (proto3)
つまり、このコマンドをプロジェクトルートから実行すると、
protoc \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
このコマンドと同等です。
protoc \
--proto_path=. \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
先程の 4. では--proto_path=.と--proto_path=helloworld両方の指定を試しましたが、公式ドキュメントの指針はこちらです。
In general you should set the
--proto_pathflag to the root of your project and use fully qualified names for all imports. - Protocol Buffers 公式: Language Guide (proto3)
つまり、.protoファイル内のimportはこのように fully qualified なパスで指定し、
syntax = "proto3";
option go_package = "github.com/richardimaoka/protoc-go-experiments/helloworld";
- import "greeting.proto";
+ import "helloworld/greeting.proto";
protocは通常プロジェクトルートから実行するので、--proto_pathもプロジェクトルートを指定します。
protoc \
--proto_path=. \
--go_out=. --go_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
+ protoc-go-experiments
+ helloworld
| greeting.pb.go # <- これが生成されたGoコード
| greeting.proto
| helloworld.pb.go # <- これが生成されたGoコード
| helloworld.proto
+ subdir
| subhello.pb.go # <- これが生成されたGoコード
| subhello.proto
--go_optについては、本記事ではpaths=source_relativeのみ解説したので、他の値は機会があれば別記事で紹介します。
以上が公式ドキュメントに沿った--proto_path --go_out --go_opt の指定方法でした。
6. --go-grpc_outと--go-grpc_optの指定
冒頭に述べたように--go_outと--go_optと同様に--go-grpc_outと--go-grpc_optの指定すればよいだけです。
protoc \
--proto_path=. \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/*.proto helloworld/subdir/*.proto
これで、grpc を利用した開発プロジェクトでのprotocのオプション指定方法がわかりました。
Discussion