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
.proto
ファイルと同じディレクトリからprotoc
実行
1. それでは.proto
ファイルが配置されている、helloworld
ディレクトリ内からprotoc
コマンドを実行します。
# protoc-go-experiments/helloworld ディレクトリ内で以下を実行
protoc helloworld.proto # 引数で`.proto`ファイル名のみ指定
Missing output directives.
1.1 エラーを解決しながら動作を学ぶ
このエラーは --go_out
オプションの指定で解決できます。
The argument to the
go_out
flag 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
これで、次の実験への準備ができました。
--go_opt
にpaths=source_relative
を指定
1.2 ネストを避けるため、--go_opt
にpaths=source_relative
をつけます。
If the
paths=source_relative
flag 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
これで、次の実験への準備ができました。
--go_out
の変更
1.3 これまでは以下の仮定のもと進めてきました。
まずは「
--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 コード生成先ディレクトリを制御できる
protoc
を実行
2. プロジェクトルートから多くの 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
これで、次の実験への準備ができました。
.proto
ファイルの利用
3. 複数もう一つの.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.
import
エラーの解決
4.1 エラーの解決には、エラーメッセージにあるように、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
これで、次の実験への準備ができました。
--proto_path
(省略形は-I
)オプションの動作確認
4.2 先程のエラーは--proto_path
を指定によっても解決できます
IMPORT_PATH
specifies a directory in which to look for.proto
files when resolvingimport
directives. - 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
これで、次の実験への準備ができました。
--proto_path
--go_out
--go_opt
の指針
5. Protocol Buffers 公式ドキュメントには、.proto
ファイルの置き場所や、それに伴う--proto_path
--go_out
--go_opt
指定の指針が書いてあります。
まずは.proto
ファイルの置き場所です。
Prefer not to put
.proto
files in the same directory as other language sources. Consider creating a subpackageproto
for.proto
files, 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_path
flag 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
の指定方法でした。
--go-grpc_out
と--go-grpc_opt
の指定
6. 冒頭に述べたように--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