概要
この記事では、Go 言語における gRPC 開発の初歩的な流れをまとめます。Protocol Buffers (以下 Protobuf) で定義した .proto
ファイルから自動生成されるコードや、Go Modules におけるモジュールパスの考え方など、PHP や Composer など他言語のエコシステムに慣れた方には少し驚きがあるかもしれません。ここでは、そのポイントをわかりやすく解説します。
前提
Protobuf:
データを効率よくバイナリシリアライズ/デシリアライズする仕組み。言語を超えたデータ交換が容易になる。
gRPC:
Protobuf を使った RPC (リモート呼び出し) フレームワーク。Protobuf がデフォルトだが、実は gRPC では他の形式も拡張次第で扱えなくはない。
使いどころ:
高速・軽量なデータ交換が求められる場面全般(ファイル保存、メッセージキュー、他の通信フレームワークなど)で、必ずしも gRPC がセットではない。Protobuf 単独での利用例も多々ある。
1. Go Modules の基礎知識
Go Modules とは?
Go は依存関係管理のために Go Modules を使います。プロジェクトルートで以下のコマンドを実行し、モジュールを初期化します。
go mod init github.com/user_id/project_name
-
go.mod
ファイル が生成され、これが依存関係を管理する上でのエントリーポイントとなります。 -
github.com/user_id/project_name
の部分はモジュールパスで、一般的に リポジトリの URL(または一意な識別子)を指定します。- 一意性を確保するため、URL 形式にするのが慣例です。
- PHP/Composer では「vendor/package」形式を使うのが一般的で、ここが Go の文化と大きく異なります。
2. Protocol Buffers (.proto) ファイルの役割
.proto
ファイルとは?
.proto
ファイルは Protobuf で定義したメッセージやサービス(gRPC を使用する場合)を記述するスキーマファイルです。たとえば、以下のように定義します。
syntax = "proto3";
package hello;
// Go 用のパッケージ指定 (インポートパス; パッケージ名)
option go_package = "github.com/user_id/project_name/proto/hello;hello";
// Greeter サービスの定義
service Greeter {
// リクエストとレスポンスの型を指定
rpc SayHello (HelloRequest) returns (HelloResponse);
}
// リクエストメッセージ
message HelloRequest {
string name = 1;
}
// レスポンスメッセージ
message HelloResponse {
string message = 1;
}
-
package hello;
Protobuf 上の名前空間を示すものです。 -
option go_package = ...
Go 用にコード生成する際の インポートパス と パッケージ名 を指定します。
例:option go_package = "github.com/user_id/project_name/proto/hello;hello";
-
github.com/user_id/project_name/proto/hello
はインポートパス -
hello
はパッケージ名
-
gRPC を利用しない単なる Protobuf シリアライズ(データを効率よくバイナリシリアライズ/デシリアライズする仕組みのこと)だけなら service
は不要ですが、gRPC で遠隔呼び出しするサービスを作るには service
を定義します。
3. protoc
を使った Go コード生成
Go で Protobuf と gRPC を使うには、以下のプラグインが必要です。
protoc-gen-go
protoc-gen-go-grpc
これらは go install
コマンドでインストール可能です。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
コード生成コマンドの例
.proto
ファイルから Go コードを生成するには、以下のようにコマンドを実行します。
protoc --go_out=. --go-grpc_out=. proto/hello.proto
-
--go_out=.
Protobuf のメッセージ定義を Go の構造体として出力 -
--go-grpc_out=.
gRPC サービスのインターフェースやクライアントスタブを Go で出力
このとき、.proto
ファイルに書いた option go_package
の設定に基づいて、ディレクトリ構造 が自動的に作成され、そこに hello.pb.go
や hello_grpc.pb.go
が生成されます。
例
hello.proto
内で指定したパッケージが
option go_package = "github.com/user_id/project_name/proto/hello;hello";
となっている場合、以下のように生成されます。
.
├── proto
│ └── hello.proto
└── github.com
└── user_id
└── project_name
└── proto
└── hello
├── hello.pb.go
└── hello_grpc.pb.go
「こんな階層ができるのはエキセントリックだ!」 と感じる方もいるかもしれませんが、これは Go がモジュールパスをそのままディレクトリ構造にマッピングする仕様 に起因しています。
※もし、.proto
ファイルのある場所に直接生成したい場合は、paths=source_relative
オプションを使います。
補足説明: go_package と .proto ファイルの配置に関する注意点
本記事で紹介している手順に従って進めると、hello.proto
内で設定した go_package
のパスと、実際の .proto
ファイルの配置場所が一致ません。これにより、生成されたコードが想定外のディレクトリに配置され、最終的に main.go
のインポートでエラーが発生します。
具体的な問題例
-
設定例
hello.proto
内の設定:option go_package = "github.com/yourusername/yourproject/proto/hello;hello";
-
配置の問題
指示どおりにhello.proto
をproto/
配下に置いてしまうと、生成されたコードが
yourproject/github.com/yourusername/yourproject/proto/hello/
のような余計なディレクトリ階層に出力される可能性があります。 -
結果としてのエラー
その結果、main.go
でimport pb "github.com/yourusername/yourproject/proto/hello"
と記述しても、実際の生成ファイルが存在する場所と一致せず、VSCode やコンパイラから「no required module provides package ...」というエラーが発生します。
解決方法
この問題を回避するためには、以下の2つの方法のいずれかを実施してください。
-
.proto ファイルの配置場所を調整する
- 具体的には、
hello.proto
をproto/hello/
フォルダに移動します。 - その上で、プロジェクトルート(例:
yourproject/
)から以下のように実行します。protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. proto/hello/hello.proto
- これにより、生成されたファイルが
yourproject/proto/hello/
に正しく配置され、インポートパスgithub.com/yourusername/yourproject/proto/hello
と一致します。
- 具体的には、
-
protoc コマンドのオプションを利用する
- 既存のディレクトリ構造を維持したい場合は、
paths=source_relative
オプションを用いて生成ファイルを.proto
ファイルと同じディレクトリに出力するように調整します。
- 既存のディレクトリ構造を維持したい場合は、
どちらの方法でも、モジュールルートとインポートパスが一致すれば、main.go
の import エラーは解消されます。プロジェクトの構成や運用に合わせて、適切な方法を選択してください。
詳しくは以下の記事を参考にしてください。
参考記事: Go + gRPC + Protocol Buffers 入門 〜 go_package の正しい設定とディレクトリ構成でハマらないために 〜
4. 自動生成されるコードの正体
.proto
から生成されるコードには、次のようなものが含まれています。
-
メッセージ定義
- Protobuf の
message
を Go のstruct
として表現 - バイナリ形式への変換メソッドなどが自動的に含まれる
- Protobuf の
-
gRPC サービスのインターフェースとクライアント
-
service
に対応する Go インターフェース(例:GreeterServer
) - クライアントスタブ(
GreeterClient
)およびサービスの登録関数(RegisterGreeterServer
)など
-
これによって、手書きで低レベルのシリアライズやネットワーク通信コードを書く必要がなく、生成されたインターフェースを実装したり、クライアントを呼び出したりする形で gRPC の機能が使えるようになります。
5. PHP や Composer との比較
-
PHP / Composer
-
composer.json
でvendor/package
のようにパッケージを指定 - インストールすると
vendor/
ディレクトリに依存ライブラリが配置される - リポジトリ URL を直接「ディレクトリ」として使うことは少ない
-
-
Go / Go Modules
- モジュールパスがそのままインポートパスとなる(
github.com/ユーザー名/リポジトリ名
など) - ディレクトリ構造とインポートパスが対応している
- Protocol Buffers のコード生成でも、このモジュールパスが利用されるため、やや複雑に見えるディレクトリ構造が作られる
- モジュールパスがそのままインポートパスとなる(
6. まとめ
-
Go Modules を使ってプロジェクトを初期化
go mod init github.com/user_id/project_name
-
.proto ファイルでメッセージとサービスを定義
-
option go_package
で Go のインポートパスとパッケージ名を指定
-
-
protoc
+ Go プラグインでコード生成protoc --go_out=. --go-grpc_out=. proto/*.proto
- ディレクトリ構造は
go_package
に基づく
-
生成されたコード(.pb.go, _grpc.pb.go)を使用
- サーバー側では、インターフェースを実装 &
Register〜Server
で登録 - クライアント側では、自動生成されたスタブを呼び出す
- サーバー側では、インターフェースを実装 &
Go の世界では「モジュールパス=インポートパス」が大前提となり、protoc
のコード生成もこの仕様に強く依存します。PHP エンジニアの方には少しエキセントリックに映るかもしれませんが、慣れてしまうとわかりやすい仕組みです。ぜひ試してみてください!
参考リンク
以上が、Go と Protocol Buffers による gRPC 開発の超入門的なまとめです。質問やフィードバックがあればお気軽にコメントください!