概要
この記事では、Go で gRPC サーバーを実装する際の基本的な流れと、Protocol Buffers の .proto
ファイルから自動生成されるコードがどのように活用されるのかについて、入門者向けに丁寧に解説します。
特に、.proto
ファイルと自動生成されたファイル(hello.pb.go
および hello_grpc.pb.go
)のコードを比較しながら、実際のサーバー実装(SayHello
メソッド)の理解を深めていきます。
前提条件
- Go の基本文法や構造体・関数の定義が理解できること
- gRPC の基本概念(RPC、サービス、クライアント・サーバー間通信)の理解
- Protocol Buffers の概要と、.proto ファイルの役割がわかること
-
Go Modules によるプロジェクト管理の基本
→ Go で始める gRPC 入門 — Protocol Buffers,protoc
から自動生成されるコードのしくみ
1. hello.proto の役割と内容
1-1. hello.proto とは?
.proto
ファイルは、gRPC サービスとデータ構造(メッセージ)を定義するスキーマファイルです。
以下は、シンプルな hello.proto の例です。
syntax = "proto3";
package hello;
// Go 用のパッケージ指定(インポートパス; パッケージ名)
// ここでは、リモートのプロジェクト URL 形式に基づいています。
option go_package = "github.com/yourusername/yourproject/proto/hello;hello";
// Greeter サービスの定義
service Greeter {
// SayHello メソッド:クライアントから HelloRequest を受け取り、HelloResponse を返す
rpc SayHello (HelloRequest) returns (HelloResponse);
}
// クライアントが送るリクエストメッセージ
message HelloRequest {
string name = 1;
}
// サーバーが返すレスポンスメッセージ
message HelloResponse {
string message = 1;
}
ポイント
-
package hello;
Protobuf の内部名前空間として利用します。 -
option go_package:
生成される Go コードのインポートパスとパッケージ名を指定します。
例えば、github.com/yourusername/yourproject/proto/hello
というパスと、パッケージ名hello
となります。
2. 自動生成されるコードの概要
.proto
ファイルからコードを生成すると、主に以下の2種類のファイルが作成されます。
-
hello.pb.go:
メッセージ(HelloRequest、HelloResponse)に対応する Go の構造体や、シリアライズ/デシリアライズ処理が実装されています。 -
hello_grpc.pb.go:
gRPC サービスのインターフェースが定義されます。
ここには、GreeterServer
インターフェースと、その補助としてのUnimplementedGreeterServer
が生成されます。
2-1. 生成されたコードの一部例
※ 以下は簡略化した抜粋例です。
hello_grpc.pb.go(抜粋)
// GreeterServer は、Greeter サービスのサーバー側インターフェースです。
type GreeterServer interface {
// SayHello は、HelloRequest を受け取り、HelloResponse を返すメソッド。
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
}
// UnimplementedGreeterServer は、GreeterServer の未実装メソッドを含む構造体です。
// これを埋め込むことで、将来的なメソッド追加に伴うコンパイルエラーを防ぎます。
type UnimplementedGreeterServer struct{}
func (UnimplementedGreeterServer) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
hello.pb.go(抜粋)
// HelloRequest は、クライアントからのリクエストメッセージです。
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
// HelloResponse は、サーバーからのレスポンスメッセージです。
type HelloResponse struct {
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
これらの自動生成コードは、hello.proto の定義に沿って生成され、サーバー側の実装で利用されます。
3. gRPC サーバーの実装 (server/main.go)
次に、生成されたコードを利用して、実際にサーバー側の実装を行います。
3-1. server/main.go の全体コード
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "github.com/yourusername/yourproject/proto/hello"
)
// server 構造体は Greeter サービスの実装を行う型です。
// UnimplementedGreeterServer を埋め込むことで、未実装のメソッドに対するデフォルト実装を利用します。
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello は、Greeter サービスで定義された RPC メソッドです。
// ctx: リクエストのコンテキスト(タイムアウトやキャンセル情報を含む)
// req: クライアントから送られてくる HelloRequest
// 戻り値: HelloResponse とエラー(エラーがなければ nil)
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
// req.GetName() で、リクエストの name フィールドの値を取得します。
log.Printf("Received request for name: %s", req.GetName())
// ここでは、HelloResponse の message フィールドに "Hello " とリクエストの name を連結して返しています。
return &pb.HelloResponse{Message: "Hello " + req.GetName()}, nil
}
func main() {
// 1. TCP ポート 50051 でリスナーを作成
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 2. gRPC サーバーのインスタンスを作成
s := grpc.NewServer()
// 3. 生成された Greeter サービスの実装(server 型)を登録
pb.RegisterGreeterServer(s, &server{})
// 4. Reflection API を有効化(grpcurl など外部ツールでサービス情報を取得するために便利)
reflection.Register(s)
// 5. リスナー上でサーバーを起動し、接続を待機
log.Printf("Server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
3-2. 各部分の解説
-
リスナーの作成:
lis, err := net.Listen("tcp", ":50051")
- TCP のポート 50051 を開き、サーバーが接続を待ち受ける準備をします。
-
gRPC サーバーの生成:
s := grpc.NewServer()
- 新しい gRPC サーバーを作成します。
-
サービスの登録:
pb.RegisterGreeterServer(s, &server{})
- 自動生成されたコードにある
RegisterGreeterServer
を呼び出し、自分が実装したserver
型(UnimplementedGreeterServer
を埋め込んでいる)を登録します。
- 自動生成されたコードにある
-
Reflection の有効化:
reflection.Register(s)
- 外部ツール(grpcurl など)がサーバーのサービス情報を取得できるように、Reflection API を有効化します。
-
サーバーの起動:
s.Serve(lis)
- リスナーを渡してサーバーを起動します。これにより、クライアントからの接続を待ち受けます。
-
SayHello の実装:
- リクエストの
name
フィールドをreq.GetName()
で取得し、ログに出力。 - 取得した値を用いて、
HelloResponse
のmessage
フィールドに"Hello " + req.GetName()
を設定して返します。
- リクエストの
4. サーバー実装のまとめ
-
hello.proto の定義:
- gRPC サービス(Greeter)と、メッセージ型(HelloRequest, HelloResponse)を定義します。
-
option go_package
により、生成されるコードのインポートパスとパッケージ名を指定しています。
-
自動生成コードの役割:
-
hello_grpc.pb.go
には、GreeterServer インターフェースとUnimplementedGreeterServer
が定義されています。 - サーバー側では、このインターフェースに沿って必要なメソッド(ここでは SayHello)を実装します。
-
-
サーバー実装:
- TCP リスナーでポート 50051 を開き、gRPC サーバーを作成してサービスを登録します。
- Reflection API を有効化して、外部ツールでのデバッグやテストを容易にしています。
-
SayHello メソッド:
- クライアントからのリクエストで送られる名前を取得し、それを元に "Hello [Name]" のレスポンスを返す実装です。
5. まとめと今後のステップ
この一連の流れで、hello.proto の定義に基づいて自動生成されたコードを活用し、gRPC サーバーが正しく実装される仕組みが構築されました。
PHP など他の言語と比べると、Go ではクラスの代わりに構造体とレシーバーを使ってメソッドを定義するため、初めは少し戸惑うかもしれませんが、今回の例でその基本的な考え方が理解できたと思います。
次のステップとしては、クライアント側の実装や、実際にサーバーと通信するテストを行って、システム全体の動作を確認してみましょう。
参考リンク
以上、gRPC サーバー実装と hello.proto の自動生成コードを比較しながら解説しました。
この記事が、私と同じようなGo入門者の方や他言語からの移行を検討している方の理解の助けになれば幸いです。
質問やフィードバックがあれば、ぜひコメントしてください!