GoでgRPCサーバーを構築する — Protocol Buffersから実装まで

GoでgRPCサーバーを構築する — Protocol Buffersから実装まで | mohablog

REST APIの開発に慣れてくると、「もっと高速で型安全な通信ができないか」と考える場面が出てきます。マイクロサービス間の通信や、モバイルアプリとバックエンドの接続で、JSONベースのREST APIだとシリアライズのコストやスキーマの不整合が気になることがあります。

そんなときの選択肢がgRPCです。Googleが開発したRPCフレームワークで、Protocol Buffers(protobuf)による型安全なスキーマ定義とHTTP/2ベースの高速な通信が特徴です。この記事では、Go 1.22 + gRPC-Go 1.64の環境で、.protoファイルの定義からサーバー・クライアントの実装、ストリーミング通信までを順に解説します。

目次

gRPCとは — REST APIとの違いを理解する

gRPCの基本的な仕組み

gRPCはRemote Procedure Callの略で、ネットワーク越しに別のサーバーの関数を呼び出す仕組みです。呼び出し側(クライアント)は、あたかもローカルの関数を呼ぶかのようにリモートのサービスにアクセスできます。

通信の流れは以下のとおりです。

  • .protoファイルでサービスとメッセージの型を定義する
  • protocコンパイラでGoのコードを自動生成する
  • サーバーは生成されたインターフェースを実装する
  • クライアントは生成されたスタブを使ってサーバーを呼び出す

データのシリアライズにはProtocol Buffersというバイナリ形式を使うため、JSONと比べてペイロードサイズが小さく、パース速度も高速です。

RESTとgRPCの比較

REST APIとgRPCは目的が異なるため、一概にどちらが優れているとは言えません。ユースケースに応じて使い分けるのが正解です。

項目REST APIgRPC
プロトコルHTTP/1.1(HTTP/2も可)HTTP/2
データ形式JSON(テキスト)Protocol Buffers(バイナリ)
スキーマ定義OpenAPI(任意).protoファイル(必須)
型安全性低い(実行時エラー)高い(コンパイル時チェック)
ストリーミングWebSocketで別途実装標準でサポート
ブラウザ対応標準対応gRPC-Webが必要
デバッグの容易さcurlで簡単にテスト専用ツールが必要
向いている場面公開API、Webフロントエンドマイクロサービス間通信、モバイルアプリ

公開APIやブラウザからのアクセスが必要ならREST、マイクロサービス間の内部通信やパフォーマンスが求められる場面ではgRPCが適しています。

開発環境のセットアップ

必要なツールのインストール

gRPC開発に必要なツールは3つです。

# Protocol Buffersコンパイラのインストール(macOS)
brew install protobuf

# Go用のprotocプラグインをインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
$ protoc --version
libprotoc 27.3

$ which protoc-gen-go
/Users/username/go/bin/protoc-gen-go

$ which protoc-gen-go-grpc
/Users/username/go/bin/protoc-gen-go-grpc

protoc-gen-goはメッセージ型のGoコード生成、protoc-gen-go-grpcはgRPCサービスのGoコード生成を担当します。$GOPATH/bin(通常は~/go/bin)がPATHに入っていることを確認しておきましょう。

プロジェクト構成の作成

以下のディレクトリ構成で進めます。

grpc-demo/
+-- go.mod
+-- proto/
|   +-- user.proto
+-- pb/
|   +-- user.pb.go          (自動生成)
|   +-- user_grpc.pb.go     (自動生成)
+-- server/
|   +-- main.go
+-- client/
    +-- main.go
# プロジェクトの初期化
mkdir grpc-demo && cd grpc-demo
go mod init github.com/example/grpc-demo

# 依存パッケージの追加
go get google.golang.org/grpc@latest
go get google.golang.org/protobuf@latest

# ディレクトリの作成
mkdir -p proto pb server client
$ cat go.mod
module github.com/example/grpc-demo

go 1.22

require (
    google.golang.org/grpc v1.64.0
    google.golang.org/protobuf v1.34.1
)

Protocol Buffersでサービスを定義する

.protoファイルの書き方

gRPCの開発は.protoファイルの作成から始まります。ここではユーザー管理サービスを例に定義してみます。

// proto/user.proto
syntax = "proto3";

package user;

option go_package = "github.com/example/grpc-demo/pb";

// ユーザー情報を表すメッセージ
message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  string role = 4;
}

// ユーザー取得リクエスト
message GetUserRequest {
  int64 id = 1;
}

// ユーザー取得レスポンス
message GetUserResponse {
  User user = 1;
}

// ユーザー作成リクエスト
message CreateUserRequest {
  string name = 1;
  string email = 2;
  string role = 3;
}

// ユーザー作成レスポンス
message CreateUserResponse {
  User user = 1;
}

// ユーザー一覧リクエスト
message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

// ユーザー一覧レスポンス
message ListUsersResponse {
  repeated User users = 1;
  string next_page_token = 2;
}

// ユーザー管理サービスの定義
service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
protoファイルの構成要素:
- syntax: Protocol Buffersのバージョン(proto3を使用)
- package: 名前空間の定義
- option go_package: 生成されるGoコードのパッケージパス
- message: データ構造の定義(Goのstructに相当)
- service: RPCメソッドの定義(Goのinterfaceに相当)

各フィールドに付与する番号(= 1= 2など)はシリアライズ時のフィールド識別子です。一度定義したら変更しないのがルールで、フィールドを削除する場合も番号は再利用しません。これにより後方互換性が保たれます。

Goコードの自動生成

protocコマンドで.protoファイルからGoのコードを生成します。

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/user.proto
生成されるファイル:
  pb/user.pb.go       → メッセージ型(User, GetUserRequest等)の定義
  pb/user_grpc.pb.go  → gRPCサービスのインターフェースとクライアントスタブ

ただ、このprotocコマンドのオプションは長くて覚えにくいので、Makefileにまとめておくのがおすすめです。

# Makefile
.PHONY: proto
proto:
	protoc --go_out=. --go_opt=paths=source_relative \
	       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
	       proto/*.proto

以後はmake protoだけで再生成できます。生成されたファイルは基本的にGitにコミットします。CIで毎回生成する方式もありますが、ローカル開発との差異を防ぐためにコミットしておく方が安全です。

gRPCサーバーを実装する

サービスインターフェースの実装

自動生成されたコードにはUserServiceServerというインターフェースが含まれています。これを実装するのがサーバー側の仕事です。

// server/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sync"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	pb "github.com/example/grpc-demo/pb"
)

// userServer はUserServiceServerインターフェースを実装する
type userServer struct {
	pb.UnimplementedUserServiceServer
	mu    sync.Mutex
	users map[int64]*pb.User
	nextID int64
}

func newUserServer() *userServer {
	return &userServer{
		users:  make(map[int64]*pb.User),
		nextID: 1,
	}
}

func (s *userServer) GetUser(
	ctx context.Context,
	req *pb.GetUserRequest,
) (*pb.GetUserResponse, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	user, ok := s.users[req.GetId()]
	if !ok {
		return nil, status.Errorf(
			codes.NotFound,
			"user with id %d not found", req.GetId(),
		)
	}
	return &pb.GetUserResponse{User: user}, nil
}

func (s *userServer) CreateUser(
	ctx context.Context,
	req *pb.CreateUserRequest,
) (*pb.CreateUserResponse, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	user := &pb.User{
		Id:    s.nextID,
		Name:  req.GetName(),
		Email: req.GetEmail(),
		Role:  req.GetRole(),
	}
	s.users[s.nextID] = user
	s.nextID++

	return &pb.CreateUserResponse{User: user}, nil
}

func (s *userServer) ListUsers(
	ctx context.Context,
	req *pb.ListUsersRequest,
) (*pb.ListUsersResponse, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	var users []*pb.User
	for _, u := range s.users {
		users = append(users, u)
	}
	return &pb.ListUsersResponse{Users: users}, nil
}
実装のポイント:
- UnimplementedUserServiceServer を埋め込むことで、
  未実装のメソッドがあってもコンパイルが通る
- エラーは google.golang.org/grpc/status パッケージで
  gRPCのステータスコードを返す
- sync.Mutex で並行アクセスに対応する

UnimplementedUserServiceServerの埋め込みは重要なポイントです。これにより、.protoに新しいメソッドを追加してコードを再生成しても、既存の実装が壊れません。未実装のメソッドが呼ばれた場合はcodes.Unimplementedが自動で返されます。

サーバーの起動

実装したサービスをgRPCサーバーに登録して起動します。

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterUserServiceServer(grpcServer, newUserServer())

	fmt.Println("gRPC server listening on :50051")
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
$ go run server/main.go
gRPC server listening on :50051

gRPCサーバーのデフォルトポートは50051が慣例的に使われます。本番環境ではこの値を環境変数から読み取るようにしておくのが望ましいです。

gRPCクライアントを実装する

クライアントの基本的な使い方

自動生成されたクライアントスタブを使って、サーバーのメソッドを呼び出します。

// client/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	pb "github.com/example/grpc-demo/pb"
)

func main() {
	// サーバーに接続
	conn, err := grpc.NewClient(
		"localhost:50051",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// ユーザーを作成
	createResp, err := client.CreateUser(ctx, &pb.CreateUserRequest{
		Name:  "Alice",
		Email: "alice@example.com",
		Role:  "engineer",
	})
	if err != nil {
		log.Fatalf("CreateUser failed: %v", err)
	}
	fmt.Printf("Created user: ID=%d, Name=%s\n",
		createResp.GetUser().GetId(),
		createResp.GetUser().GetName(),
	)

	// ユーザーを取得
	getResp, err := client.GetUser(ctx, &pb.GetUserRequest{
		Id: createResp.GetUser().GetId(),
	})
	if err != nil {
		log.Fatalf("GetUser failed: %v", err)
	}
	fmt.Printf("Got user: Name=%s, Email=%s, Role=%s\n",
		getResp.GetUser().GetName(),
		getResp.GetUser().GetEmail(),
		getResp.GetUser().GetRole(),
	)
}
$ go run client/main.go
Created user: ID=1, Name=Alice
Got user: Name=Alice, Email=alice@example.com, Role=engineer

クライアント側のコードはローカル関数の呼び出しとほぼ同じ見た目になっています。これがgRPCの大きなメリットで、ネットワーク通信を意識せずにサービス間連携のコードが書けます。

エラーハンドリングのパターン

gRPCのエラーにはステータスコードが含まれています。クライアント側ではstatus.FromError()でコードを取り出して分岐処理を行います。

import (
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 999})
if err != nil {
	st, ok := status.FromError(err)
	if ok {
		switch st.Code() {
		case codes.NotFound:
			fmt.Println("ユーザーが見つかりません")
		case codes.InvalidArgument:
			fmt.Printf("リクエストが不正です: %s\n", st.Message())
		case codes.Unavailable:
			fmt.Println("サーバーに接続できません")
		default:
			fmt.Printf("エラー: code=%s, msg=%s\n", st.Code(), st.Message())
		}
	}
	return
}
$ go run client/main.go
ユーザーが見つかりません

REST APIの場合、HTTPステータスコード(404、500など)の意味がアプリケーションによって異なることがありますが、gRPCのステータスコードは仕様で厳密に定義されているため、解釈のブレが起きにくいです。主要なコードを確認しておきましょう。

gRPCコードHTTP相当意味
codes.OK200成功
codes.NotFound404リソースが存在しない
codes.InvalidArgument400リクエストが不正
codes.Unauthenticated401認証が必要
codes.PermissionDenied403権限がない
codes.Internal500サーバー内部エラー
codes.Unavailable503サービス利用不可

ストリーミング通信を実装する

Server-Side Streamingの実装

gRPCのストリーミングはREST APIにはない強力な機能です。サーバーサイドストリーミングでは、1つのリクエストに対してサーバーが複数のレスポンスを順次送信できます。

まず.protoにストリーミングRPCを追加します。

// proto/user.proto に追加
service UserService {
  // ... 既存のメソッド ...

  // Server-Side Streaming: ユーザーを1件ずつ送信
  rpc StreamUsers(ListUsersRequest) returns (stream User);
}
stream キーワードの位置で通信パターンが変わる:
- rpc Method(Request) returns (Response)           → Unary(通常)
- rpc Method(Request) returns (stream Response)    → Server Streaming
- rpc Method(stream Request) returns (Response)    → Client Streaming
- rpc Method(stream Request) returns (stream Resp) → Bidirectional

サーバー側の実装はこうなります。

func (s *userServer) StreamUsers(
	req *pb.ListUsersRequest,
	stream pb.UserService_StreamUsersServer,
) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	for _, user := range s.users {
		if err := stream.Send(user); err != nil {
			return status.Errorf(codes.Internal,
				"failed to send user: %v", err)
		}
	}
	return nil
}

クライアント側ではループでレスポンスを受け取ります。

stream, err := client.StreamUsers(ctx, &pb.ListUsersRequest{})
if err != nil {
	log.Fatalf("StreamUsers failed: %v", err)
}

for {
	user, err := stream.Recv()
	if err == io.EOF {
		break
	}
	if err != nil {
		log.Fatalf("stream recv error: %v", err)
	}
	fmt.Printf("Received: ID=%d, Name=%s\n", user.GetId(), user.GetName())
}
$ go run client/main.go
Received: ID=1, Name=Alice
Received: ID=2, Name=Bob
Received: ID=3, Name=Charlie

Server-Side Streamingは、検索結果を順次返す場合や、大量のデータを分割して送信する場合に有効です。全データを一度にメモリに載せなくてもよいため、メモリ効率の面でもメリットがあります。

Bidirectional Streamingの概要

双方向ストリーミングでは、クライアントとサーバーが同時にデータを送受信できます。チャットアプリやリアルタイム監視など、双方向の通信が必要な場面で活躍します。

// 双方向ストリーミングの定義例
service ChatService {
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
// サーバー側の実装イメージ
func (s *chatServer) Chat(
	stream pb.ChatService_ChatServer,
) error {
	for {
		msg, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}

		// 受信したメッセージを加工して返送
		reply := &pb.ChatMessage{
			User:    "server",
			Content: fmt.Sprintf("Echo: %s", msg.GetContent()),
		}
		if err := stream.Send(reply); err != nil {
			return err
		}
	}
}

双方向ストリーミングはWebSocketの代替として使えますが、REST APIでWebSocketを使う場合と比べて、Protocol Buffersによる型安全性とコード自動生成の恩恵を受けられる点が大きな違いです。関連記事として「GoでWebSocketサーバーを実装する」のトピックもあわせて参考になります。

テストとデバッグのテクニック

grpcurlでサーバーをテストする

REST APIがcurlでテストできるように、gRPCサーバーはgrpcurlでテストできます。ただし、サーバー側でリフレクションを有効にしておく必要があります。

import "google.golang.org/grpc/reflection"

func main() {
	grpcServer := grpc.NewServer()
	pb.RegisterUserServiceServer(grpcServer, newUserServer())

	// リフレクションを有効化(開発環境のみ推奨)
	reflection.Register(grpcServer)

	// ... サーバー起動
}
# grpcurlのインストール
brew install grpcurl

# サービス一覧の確認
grpcurl -plaintext localhost:50051 list

# メソッド一覧の確認
grpcurl -plaintext localhost:50051 list user.UserService

# ユーザー作成
grpcurl -plaintext -d '{"name": "Alice", "email": "alice@example.com", "role": "engineer"}' \
  localhost:50051 user.UserService/CreateUser

# ユーザー取得
grpcurl -plaintext -d '{"id": 1}' \
  localhost:50051 user.UserService/GetUser
$ grpcurl -plaintext localhost:50051 list
grpc.reflection.v1.ServerReflection
user.UserService

$ grpcurl -plaintext -d '{"name": "Alice", "email": "alice@example.com", "role": "engineer"}' \
  localhost:50051 user.UserService/CreateUser
{
  "user": {
    "id": "1",
    "name": "Alice",
    "email": "alice@example.com",
    "role": "engineer"
  }
}

grpcurlがあれば、サーバーの動作確認をクライアントコードを書かずに行えるので、開発初期のデバッグがかなり楽になります。本番環境ではリフレクションを無効にしておくのがセキュリティ上のベストプラクティスです。

インターセプタでリクエストログを仕込む

gRPCのインターセプタは、HTTPミドルウェアに相当する仕組みです。リクエストのログ記録、認証チェック、メトリクス収集などに使います。

func loggingInterceptor(
	ctx context.Context,
	req interface{},
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (interface{}, error) {
	start := time.Now()

	// ハンドラの実行
	resp, err := handler(ctx, req)

	// ログ出力
	duration := time.Since(start)
	st, _ := status.FromError(err)
	log.Printf("method=%s duration=%s code=%s",
		info.FullMethod, duration, st.Code())

	return resp, err
}

// サーバー作成時にインターセプタを登録
grpcServer := grpc.NewServer(
	grpc.UnaryInterceptor(loggingInterceptor),
)
$ go run server/main.go
gRPC server listening on :50051
2026/04/12 10:30:15 method=/user.UserService/CreateUser duration=127.5us code=OK
2026/04/12 10:30:15 method=/user.UserService/GetUser duration=45.2us code=OK
2026/04/12 10:30:16 method=/user.UserService/GetUser duration=38.1us code=NotFound

複数のインターセプタを組み合わせたい場合は、grpc.ChainUnaryInterceptor()を使います。ストリーミングRPC用にはgrpc.StreamInterceptor()が別途用意されています。関連トピックとして「Goのミドルウェアパターン」を理解しておくと、インターセプタの設計がスムーズになります。

まとめ

GoでのgRPCサーバー構築について、Protocol Buffersの定義から実装までを振り返ります。

  • gRPCはHTTP/2 + Protocol Buffersによる高速で型安全なRPCフレームワーク。マイクロサービス間通信に最適
  • .protoファイルでサービスとメッセージを定義し、protocでGoコードを自動生成する
  • サーバーはUnimplementedXxxServerを埋め込み、必要なメソッドだけを実装する
  • エラーはgoogle.golang.org/grpc/statusパッケージでgRPCステータスコードを返す
  • ストリーミング通信(Server-Side、Client-Side、Bidirectional)が標準でサポートされている
  • grpcurlでRESTのcurlのようにテストでき、インターセプタでログや認証を横断的に処理できる

よくある質問(FAQ)

Q. gRPCとREST APIはどちらを選ぶべきですか?

ユースケースによって使い分けます。ブラウザから直接アクセスする公開APIにはREST APIが適しています。一方、マイクロサービス間の内部通信や、モバイルアプリとの通信でパフォーマンスが求められる場面ではgRPCが有利です。両者を併用するパターンも一般的で、外部向けにREST APIを公開し、内部通信にはgRPCを使うアーキテクチャは多くの現場で採用されています。

Q. .protoファイルを変更したら、既存のクライアントは動かなくなりますか?

Protocol Buffersは後方互換性を重視した設計になっています。フィールドの追加は互換性を壊しません。古いクライアントは新しいフィールドを無視し、新しいクライアントは古いサーバーからのレスポンスで新フィールドをゼロ値として扱います。ただし、フィールド番号の変更や削除済みフィールド番号の再利用は互換性を壊すため、絶対に避けてください。

Q. 本番環境でgRPCを運用する際に気を付けることは?

まずTLS暗号化を有効にしましょう。開発時はinsecure.NewCredentials()を使いますが、本番では証明書を設定する必要があります。次に、ヘルスチェックの実装です。gRPCには標準のヘルスチェックプロトコル(grpc.health.v1.Health)が定義されており、Kubernetes等のオーケストレータと連携できます。また、リフレクションは本番環境では無効にしておくのがセキュリティ上の推奨事項です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次