🐷

protobufとgrpcのGoコード生成先ディレクトリの指定を、protocコマンドのオプションで行う

2022/11/13に公開

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_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_optM${PROTO_FILE}=${GO_IMPORT_PATH}という形式でパッケージを指定する方法で、上記リンク先にも説明があります。

しかし、本記事ではgo_package を指定する方法のみを解説し、--go_optM${PROTO_FILE}=${GO_IMPORT_PATH}という形式を利用する方法は、機会があれば別の記事で紹介したいと思います。「

Go パッケージ・パスの慣習に従うと、 Go パッケージは (github.com 前提で開発する場合) github.com/__github_username 以下に作成することになるので、go_package を次のように指定します。

helloworld.proto を書き換えて下さい
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_optpaths=source_relativeを指定

ネストを避けるため、--go_optpaths=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

これで、次の実験への準備ができました。

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.protohelloworld/helloworld.protoに変えます。

コマンドを実行して下さい
protoc \
  --go_out=. --go_opt=paths=source_relative \
  helloworld/helloworld.proto
  1. のときと同じ生成結果を再現できました。
コマンド実行結果
+ 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

引数にhelloworldhelloworld/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を利用しようとするとどうなるでしょう?

helloworld/helloworld.proto
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)

helloworld/helloworld.proto
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のパスを書き換えましょう。

helloworld/helloworld.proto
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_PATH specifies a directory in which to look for .proto files when resolving import directives. - Protocol Bullfers 公式: Language Guide (proto3)

それではhelloworld.protoimportパスを戻しましょう。

helloworld/helloworld.proto
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 .proto files in the same directory as other language sources. Consider creating a subpackage proto 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 なパスで指定し、

helloworld/helloworld.proto
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のオプション指定方法がわかりました。

GitHubで編集を提案

Discussion