2.

gRPCクイックスタート|.proto定義からサーバ・クライアント実装まで

編集

このチュートリアルでは、Go言語とgRPCを使い、「名前を送ると挨拶を返す」ごく小さなサービス(Greeter)をゼロから組み立てます。具体的には、.protoファイルでサービスを定義し、protocでGoコードを生成し、サーバとクライアントを実装して、実際に通信させるまでを、コマンドとコードを示しながら一歩ずつ解説します。gRPCの公式クイックスタートをなぞりつつ、各ステップで「何をしているのか」を補足する構成です。

この記事の要点
  • 学ぶこと:.protoによるサービス定義 → protocによるコード生成 → サーバ実装 → クライアント実装 → 実行、という gRPC 開発の一連の流れ。
  • 作るもの:HelloRequest(名前)を受け取り HelloReply(挨拶文)を返す SayHello RPC を持つ Greeter サービス。
  • 前提:Go(比較的新しいバージョン)、protoc(Protocol Buffers コンパイラ)、ターミナル/コマンドプロンプトの基本操作。Go や protoc のバージョンは更新が早いため、実際の最新要件は公式ドキュメントで確認してください。
  • 使うツールチェーンgoogle.golang.org/protobuf 系の protoc-gen-go と、google.golang.org/grpc 系の protoc-gen-go-grpc。古い github.com/golang/protobuf 系ではなく、こちらの新しい系統を用います。

 

全体の流れ

gRPC アプリケーションの開発は、おおまかに次の順序で進みます。最初に全体像をつかんでおくと、各ステップの位置づけが分かりやすくなります。

手順 やること
1. 準備 Go・protoc・コード生成プラグインをインストールし、PATH を通す
2. 定義 .proto ファイルにサービスとメッセージを記述する
3. 生成 protoc で .proto から Go のコード(型・クライアント・サーバ雛形)を生成する
4. サーバ実装 生成されたインターフェースを満たすように、サーバ側のロジックを書く
5. クライアント実装 生成されたクライアントを使ってサーバを呼び出すコードを書く
6. 実行 サーバを起動し、別のターミナルからクライアントを実行して通信を確認する

 

1. 準備:必要なツールのインストール

まず、開発に必要な3種類のツールをそろえます。

  • Go … 本体。比較的新しいバージョンを推奨します。
  • Protocol Buffers コンパイラ(protoc) … .proto ファイルからコードを生成するコンパイラ。
  • Go 用のコード生成プラグイン … protoc に Go コードを出力させるためのプラグイン2種。

protoc 本体は、OS のパッケージマネージャや配布アーカイブから導入します(導入方法は OS により異なるため、公式の案内を参照してください)。続いて、Go 用のプラグインを 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

ここで導入する2つのプラグインの役割は次のとおりです。

  • protoc-gen-go … メッセージ型(リクエスト/レスポンスの構造体)を生成する。
  • protoc-gen-go-grpc … サービスのクライアント・サーバ用インターフェースを生成する。

なお @latest は最新版を取得する指定です。再現性を重視する場合は、特定のバージョンタグ(例:@v1.34.0 のような形式)を指定する運用も考えられます。バージョンの対応関係は更新されるため、固定する場合は公式の案内で確認してください。

# protoc 本体が呼べることの確認(例)
$ protoc --version

protoc コマンド、および go install で入れたプラグイン(通常は $GOPATH/bin もしくは $HOME/go/bin に置かれます)を、ターミナル/コマンドプロンプトから実行できるよう PATH を通してください。PATH が通っていないと、後段のコード生成でプラグインが見つからずエラーになります(具体的な設定方法は OS ごとに異なるため省略します)。

 

2. 定義:.proto ファイルを書く

gRPC のサービスは Protocol Buffers(protobuf)で定義します。今回は、クライアントから HelloRequest を受け取り、サーバから HelloReply を返す SayHello という RPC メソッドを持つ Greeter サービスを定義します。

例として、次のような helloworld.proto を考えます。

syntax = "proto3";

option go_package = "example.com/helloworld/helloworld";

package helloworld;

// 挨拶サービスの定義
service Greeter {
  // 挨拶を送る
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 名前を含むリクエストメッセージ
message HelloRequest {
  string name = 1;
}

// 挨拶文を含むレスポンスメッセージ
message HelloReply {
  string message = 1;
}

各要素のポイントは以下のとおりです。

  • syntax = "proto3"; … protobuf の構文バージョン。現在は proto3 が一般的です。
  • option go_package … 生成される Go コードのインポートパス(パッケージの位置)を指定します。プロジェクトのモジュールパスに合わせます。
  • service / rpc … 呼び出せるメソッドを定義します。rpc メソッド名 (引数型) returns (戻り値型) という形です。
  • message … やり取りするデータ構造。各フィールドの末尾にある = 1フィールド番号で、エンコード時の識別子として使われます(値の代入ではありません)。一度割り当てた番号は変更しないのが原則です。

 

3. 生成:protoc で Go コードを作る

.proto を書いたら、protoc にプラグインを組み合わせて Go コードを生成します。.proto があるディレクトリで、次のようなコマンドを実行します(パスはプロジェクト構成に合わせて調整してください)。

$ protoc --go_out=. --go_opt=paths=source_relative \
     --go-grpc_out=. --go-grpc_opt=paths=source_relative \
     helloworld/helloworld.proto

このコマンドの各オプションの意味は次のとおりです。

オプション 役割
--go_out protoc-gen-go によるメッセージ型コードの出力先
--go-grpc_out protoc-gen-go-grpc によるサービス用コードの出力先
paths=source_relative .proto の位置を基準に、生成ファイルの出力場所を決める指定

成功すると、おおむね次の2ファイルが生成されます。

  • helloworld.pb.goHelloRequest / HelloReply などのメッセージ型。
  • helloworld_grpc.pb.goGreeterClient(クライアント)や GreeterServer(サーバ用インターフェース)など。

以降のサーバ・クライアント実装では、これらの生成済みコードを pb という別名でインポートして利用します。

 

4. サーバ実装

生成された GreeterServer インターフェースを満たすように、サーバ側のロジックを実装します。下記は greeter_server/main.go 相当の例です。まずは全体を眺めてから、要所を個別に解説します。

// コマンドライン引数を処理するための変数
var (
    port = flag.Int("port", 50051, "The server port")
)

// Greeter サービスを実装する構造体
type server struct {
    pb.UnimplementedGreeterServer
}

// SayHello メソッドの実装
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
    flag.Parse()

    // 指定ポートでリッスン
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // gRPC サーバーを作成
    s := grpc.NewServer()

    // Greeter サービスを登録
    pb.RegisterGreeterServer(s, &server{})

    log.Printf("server listening at %v", lis.Addr())

    // サーバー起動(リクエスト待機)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

このコードは gRPC を使って Greeter サービスのサーバを実装しており、コマンドライン引数からポート番号を受け取って起動します(引数を省略すればデフォルトの 50051 が使われます)。サーバはクライアントからの挨拶リクエストを受け付け、挨拶メッセージを応答として返します。要点を順に見ていきます。

(1) UnimplementedGreeterServer の埋め込み

server 構造体に pb.UnimplementedGreeterServer を埋め込んでいます。これは生成コードが提供する型で、将来サービスにメソッドが追加されても既存のサーバがコンパイルエラーにならないようにするための仕組み(前方互換の土台)です。

(2) SayHello メソッドの実装

SayHello は、生成された GreeterServer インターフェースが要求するメソッドです。受け取った HelloRequest の名前をログ出力し、"Hello " を前置した HelloReply を返します。RPC のビジネスロジックは、このように生成インターフェースのメソッドとして書きます。

(3) リッスンの設定

lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))

この行は、サーバが接続を待ち受けるネットワークアドレスを設定しています。動きを分解すると次のとおりです。

  1. fmt.Sprintf(":%d", *port) … ポート番号を ":50051" のような文字列に整形します。
  2. net.Listen("tcp", ...) … TCP を指定し、そのアドレスでクライアント接続を待ち受ける net.Listener を作成します。
  3. lis, err := ... … 生成された net.Listener とエラーを受け取ります。ポートが使用中などで失敗するとここで err に値が入ります。

(4) サーバの生成・登録・起動

grpc.NewServer() で gRPC サーバを作り、pb.RegisterGreeterServer で自作の server をそのサーバに紐付けます。最後に s.Serve(lis) でリクエストの受付を開始し、以降はクライアントからの呼び出しを待ち続けます。

 

5. クライアント実装

続いて、サーバを呼び出すクライアントを実装します。下記は greeter_client/main.go 相当の例です。

(1) コマンドライン引数の定義

var (
    addr = flag.String("addr", "localhost:50051", "the address to connect to")
    name = flag.String("name", defaultName, "Name to greet")
)

接続先アドレス addr と、挨拶する相手の名前 name をコマンドライン引数として定義しています。flag パッケージは Go 標準ライブラリで、コマンドライン引数を扱うためのものです。後述の flag.Parse() を呼ぶことで実際の引数値が反映されます。呼ばなければ、第2引数で指定したデフォルト値(ここでは localhost:50051defaultName)が使われます。

(2) サーバへの接続

conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
    log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

gRPC サーバへの接続(チャネル)を用意します。ここでは学習目的のため、TLS を使わない insecure.NewCredentials() を指定しています。実運用では、平文通信ではなく TLS などの安全な認証情報を用いるのが一般的です。

なお、接続生成 API は gRPC のバージョンによって推奨される関数が変わってきています。比較的新しい版では grpc.NewClient が用いられます。古いコード例では grpc.Dial が使われていることがありますが、お使いのバージョンの公式ドキュメントに合わせて選んでください。

(3) クライアントの生成

c := pb.NewGreeterClient(conn)

生成コードが提供する NewGreeterClient に接続を渡し、Greeter サービスのクライアントを作ります。以降はこの c を通じて RPC を呼び出します。

(4) RPC の呼び出し

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
    log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())

指定した名前で SayHello を呼び出し、サーバからの応答をログ出力します。context.WithTimeout でタイムアウト付きのコンテキストを作っているため、一定時間(ここでは1秒)応答がなければ呼び出しが打ち切られます。gRPC ではこのように、呼び出しごとにコンテキストでタイムアウトやキャンセルを制御するのが基本です。

 

6. 実行:サーバとクライアントを動かす

実装ができたら動かしてみます。まず1つ目のターミナルでサーバを起動します。

$ go run greeter_server/main.go

続いて、別のターミナルをもう1つ開いてクライアントを実行します。

$ go run greeter_client/main.go
Greeting: Hello world

クライアント側に Greeting: Hello world と表示され、サーバ側のターミナルに Received: world と出力されれば成功です(defaultNameworld の場合の例)。サーバとクライアントが gRPC 経由で通信できたことになります。

 

応用:サービスにメソッドを追加してみる

gRPC 開発の流れに慣れるため、サービスへ新しい RPC を1つ追加してみましょう。「もう一度挨拶する」SayHelloAgain を加えます。.proto の service ブロックを次のように変更します。

// 挨拶サービスの定義
service Greeter {
  // 挨拶を送る
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // もう一度挨拶を送る
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

.proto を変更したら、必ずコードを再生成します。生成しないと、追加したメソッドに対応する型やインターフェースが反映されません。手順3と同じコマンドを再実行します。

$ protoc --go_out=. --go_opt=paths=source_relative \
     --go-grpc_out=. --go-grpc_opt=paths=source_relative \
     helloworld/helloworld.proto

再生成すると GreeterServer インターフェースに SayHelloAgain が加わります。サーバ側にその実装を追加します。

func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}

クライアント側でも、c.SayHello(...) と同様に c.SayHelloAgain(...) を呼び出すコードを足せば、新しい RPC を利用できます。このように「.proto を直す → 再生成する → 実装を足す」というサイクルが gRPC 開発の基本になります。

 

よくある落とし穴
  • protoc やプラグインが見つからないprotoc 本体や、go install で入れた protoc-gen-go / protoc-gen-go-grpc に PATH が通っていないと、コード生成が失敗します。プラグインは通常 $HOME/go/bin 等に置かれるので、そこを PATH に含めます。
  • go_package / import パスの不一致:.proto の option go_package や生成物のパスが、実際の Go モジュールのパスと食い違うと、生成コードをインポートできません。モジュール構成と一致させます。
  • 再生成のし忘れ:.proto を変更したのにコードを再生成しないと、新しいメソッドや型が反映されず、コンパイルエラーや「メソッド未実装」状態になります。.proto を直したら必ず再生成します。
  • ポート競合・接続先の食い違い:サーバの待ち受けポートとクライアントの接続先(既定では 50051)がずれていたり、ポートが他プロセスに使われていると接続できません。アドレスとポートを合わせ、競合がないか確認します。
  • バージョン差による API・コマンドの違い:gRPC や protobuf はバージョン更新が速く、接続用 API(grpc.NewClientgrpc.Dial など)や推奨手順が変わることがあります。エラー時はまず、お使いのバージョンに対応する公式ドキュメントを確認してください。

 

FAQ

Q1. protoc-gen-go と protoc-gen-go-grpc は何が違うのですか。両方必要ですか。

役割が異なります。protoc-gen-gomessage から生成されるデータ型(リクエスト/レスポンスの構造体)を担当し、protoc-gen-go-grpcservice から生成されるクライアント・サーバ用のインターフェースを担当します。gRPC サービスを Go で扱うには、通常この両方が必要です。

Q2. なぜ古い grpc.Dial ではなく grpc.NewClient を使う例になっているのですか。

比較的新しい gRPC for Go では、接続(チャネル)の生成に grpc.NewClient を用いる方針が示されているためです。インターネット上の古い記事では grpc.Dial を使った例も多く見られますが、利用しているバージョンによって推奨 API が異なります。実装時は、手元のライブラリのバージョンと、それに対応する公式ドキュメントの記述に合わせるのが確実です。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. Protocol Buffers
  2. 詳細説明付きクイックスタート
  3. Docker + Go言語 + gRPC で簡単なWebアプリケーションを作る その1

最近更新/作成されたページ