プロトコル

gRPC Goの実装|protoc-gen-go-grpcの生成手順とNewClient移行を解説

gRPC Goの実装|protoc-gen-go-grpcの生成手順とNewClient移行を解説

GoでgRPCを書き始めるとき、最初に迷うのは生成ツールの入れ方です。プラグインは2つに分かれており、バージョンも別に動きます。そしてクライアント接続の入口であるgrpc.Dialは、すでに非推奨の扱いです。この記事では2026年9月時点の一次情報をもとに、protoからの生成、bufでの運用、grpc.NewClientへの移行、インターセプタでのログと認証までをGoの実装順に並べます。プロトコルの仕組みそのものはgRPCの仕組みとRESTとの違いに譲り、ここではGo側の工程だけを扱います。

まとめ|Goで先に決める3点と落ちやすい1点

着手前に決めるのは3つです。生成コードをリポジトリへコミットするか毎回生成するか、生成の呼び出しをprotoc直打ちにするかbufへ寄せるか、そしてプラグインのバージョンを何で固定するか。この3つは後から変えるとCIの手順とディレクトリ構成まで巻き戻るため、1人目が書き始める前に決めておきます。

落ちやすいのは接続側です。grpc.Dialとgrpc.DialContextはgrpc-goのソース上で「Deprecated: use NewClient instead」と明記されており、1.x系の間はサポートされるものの、新規コードで使う理由はありません。ところが単純に置き換えると、接続待ちのオプションが黙って無視され、名前解決の既定も変わります。ここが移行時の最大のつまずきです。

結論を先に置きます。2026年9月時点で本番に入れるなら、grpc-go 1.83系+protoc-gen-go-grpc 1.6系+buf 1.72系の組み合わせが素直です。生成はbufへ寄せ、プラグインはGo 1.24以降のtoolディレクティブでモジュールに固定します。

生成プラグインの構成|protoc-gen-goとgo-grpcを別に入れる理由

GoのgRPC実装で使うプラグインは2つあります。メッセージ型を出すprotoc-gen-goと、サービスのインターフェースとクライアントスタブを出すprotoc-gen-go-grpcです。この2つは配布元も別で、バージョン番号もまったく別に進みます。片方だけ上げてもう片方を放置した結果、生成物の互換要求が合わずにビルドが落ちる、という事故がここから生まれます。

protocプラグインが2つに分かれた経緯と出力先オプションの対応

分離の理由は責務の違いです。メッセージのシリアライズはProtocol Buffers側の仕組みで、gRPCがなくても成立します。一方サービス定義からのスタブ生成はgRPCに固有の処理です。そのため前者はprotobuf-goリポジトリ、後者はgrpc-goリポジトリのサブディレクトリで管理されています。

2026年9月2日にモジュールプロキシで確認した版は、google.golang.org/protobufが1.36.12(2026年8月10日)、google.golang.org/grpcが1.83.2(同8月25日)、protoc-gen-go-grpcが1.6.2(同5月7日)でした。番号の桁がそろっていないのは異常ではなく、別管理の当然の結果です。両者の対応を表にすると次のようになります。

オプション 出力されるもの 提供元
–go_out メッセージ型とマーシャル protobuf-go
–go-grpc_out サービスIFとスタブ grpc-go
–go_opt 出力パスとgo_package protobuf-go
–go-grpc_opt 未実装型の埋め込み要求 grpc-go

Go 1.24のtoolディレクティブでプラグイン版を固定する手順

プラグインをgo installで入れると、版はマシンごとに散らばります。CIのイメージを作り直した日から生成物の差分が出るのは、この管理の甘さが原因です。Go 1.24で追加されたtoolディレクティブを使うと、プラグインの版をモジュール側に書き込めます。

go get -tool google.golang.org/protobuf/cmd/[email protected]
go get -tool google.golang.org/grpc/cmd/[email protected]
go tool protoc-gen-go-grpc --version
go get tool   // tool メタパターンで一括更新

go get -toolはrequireに加えてtoolディレクティブをgo.modへ足し、go toolで呼び出せるようにします。toolメタパターンはモジュール内の全ツールを指すため、更新も1コマンドで済みます。チーム内の版ずれを消したいなら、この形へ寄せるのが手数が少ない選択です。

protoからの生成工程|go_package指定と出力パスの決め方

生成でつまずく箇所の大半は、出力先の指定という一点です。protoファイルのgo_packageと、コマンド側のpathsオプション、そして実際のディレクトリ構成の3つが噛み合っていないと、意図しない深い階層にファイルが出ます。protoの記法そのものはProtocol Buffersのproto定義の書き方にまとめてあるので、ここではGoの出力に関わる部分だけを扱います。

go_packageとM指定で生成先を決めるsource_relativeの効き方

最初の作業は、protoファイル側へのgo_packageの記述です。ここにはGoのインポートパスを入れます。書き忘れると生成が失敗するか、意図しないパッケージ名が付きます。

syntax = "proto3";
package greet.v1;
option go_package = "example.com/api/gen/greet/v1;greetv1";

service GreetService {
  rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
}

セミコロンの後ろはGoのパッケージ名で、省略するとパスの末尾要素から推測されます。v1のようなバージョン付きディレクトリを使う設計では、推測に任せるとパッケージ名がv1になってしまうため、明示しておくほうが読み手に優しくなります。

protoc -I proto \
  --go_out=gen --go_opt=paths=source_relative \
  --go-grpc_out=gen --go-grpc_opt=paths=source_relative \
  proto/greet/v1/greet.proto

paths=source_relativeを付けると、出力先はprotoの相対パスをそのまま引き継ぎます。付けない既定の挙動ではgo_packageのフルパスがディレクトリとして再現されるため、gen/example.com/api/gen/...のような二重構造になりがちです。protoを自分たちで所有している案件なら、source_relativeを既定にして構成を単純に保つのが扱いやすい方針です。第三者から受け取ったprotoでファイル単位に上書きしたいときだけ、M接頭辞の指定を使います。

生成物のUnimplemented埋め込みと互換維持の既定値の意味

生成されたサーバ側インターフェースには、UnimplementedGreetServiceServerという構造体が付いてきます。自作のサーバ構造体にこれを埋め込まないとインターフェースを満たせず、コンパイルが通りません。これは不便に見えて、実はprotoにRPCを追加したときにビルドが壊れないための仕組みです。

protoc-gen-go-grpc 1.6.2のソースでは、この挙動を決めるrequire_unimplemented_serversフラグの既定値がtrueになっています。falseへ落とせば旧来の書き方に戻りますが、そうするとprotoへのRPC追加が既存サーバのコンパイルエラーになります。マイクロサービス間でprotoを共有している構成なら、既定のtrueを維持して埋め込む形が安全です。

type server struct {
    greetv1.UnimplementedGreetServiceServer
}

func (s *server) SayHello(ctx context.Context, req *greetv1.SayHelloRequest) (*greetv1.SayHelloResponse, error) {
    return &greetv1.SayHelloResponse{Message: "hello " + req.GetName()}, nil
}

フィールドの読み出しは生成されたゲッターを通します。req.Nameではなくreq.GetName()を使うと、レシーバがnilでもゼロ値が返るため、境界での防御が1段減ります。

bufでの生成運用|設定ファイルへの集約とCIでの互換検査の置き方

protocの直打ちは、オプションが増えるほど誰も触れないシェルスクリプトへ育ちます。protoが数本のうちは問題になりませんが、ディレクトリが分かれ、プラグインが3つ4つに増えた段階で破綻します。bufはこの呼び出しを設定ファイルへ移し、あわせてlintと互換検査を持ち込むツールです。2026年9月時点の版は1.72.0(2026年7月17日)でした。

buf.gen.yamlのv2形式にプラグイン指定を移す書き換え手順

プラグイン指定はbuf.gen.yamlに書きます。現行のv2形式では、ローカルにあるプラグインをlocalキーで並べます。

version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-go-grpc
    out: gen
    opt: paths=source_relative

これで生成時に実行するコマンドはbuf generateの1行だけです。プラグインを追加するときも、YAMLへ数行足すだけで済みます。RESTを併設する構成なら、ここにgRPC-GatewayのREST生成プラグインを足す形になり、コマンドの複雑さがYAMLの行数へ置き換わります。

buf breakingでスキーマ互換を壊さないCIの組み立て方

protoの変更でクライアントが壊れる事故は、レビューだけでは止まりません。フィールド番号の付け替えや型の変更は、見た目の差分が小さいのに影響が大きい変更です。bufの互換検査をCIに置くと、これを機械で止められます。

buf lint
buf breaking --against ".git#branch=main"
buf generate
git diff --exit-code gen

4行目が効くのは、生成物をコミットする運用です。生成コードをリポジトリへ入れるかどうかは好みが分かれますが、Goの場合は入れておくとエディタの補完とレビューの読みやすさで得をします。入れる運用にしたなら、生成漏れをCIで検出する仕掛けまで含めて用意します。

クライアント接続|Dialの非推奨とNewClientへの移行判断

ここが本記事の核です。日本語で読める入門記事の多くはgrpc.Dialとgrpc.WithBlock()を組み合わせた形を載せていますが、grpc-go 1.83.2のclientconn.goではDialとDialContextの両方に「Deprecated: use NewClient instead. Will be supported throughout 1.x.」というコメントが付いています。1.x系の間は消えないため急ぐ必要はないものの、新規コードはNewClientで書くのが前提になりました。

NewClientで無視される接続待ちオプションと名前解決の差

単純な置き換えで済まない理由が2つあります。1つ目は無視されるオプションです。同じソースのコメントには、WithBlock・WithTimeout・WithReturnConnectionError・FailOnNonTempDialErrorが返すオプションはNewClientでは無視されると書かれています。つまり「起動時に接続できなければ失敗させる」という従来の書き方は、エラーも警告もなく無効化されます。

2つ目は名前解決の既定です。NewClientの既定スキームはdnsで、DialとDialContextは後方互換のためにpassthroughを使っていました。カスタムのダイアラへターゲット文字列がそのまま渡ることを前提にしたコードは、移行時に挙動が変わります。該当するのは独自のサービスディスカバリを挟んでいる構成で、素直にホスト名とポートを渡しているだけなら影響はありません。

conn, err := grpc.NewClient(
    "dns:///greet.internal:443",
    grpc.WithTransportCredentials(creds),
)
if err != nil {
    return err
}
defer conn.Close()
client := greetv1.NewGreetServiceClient(conn)

NewClientが返すのはアイドル状態のクライアントで、実際の接続は最初のRPCまで遅延する仕組みです。起動時に疎通を確かめたいなら、ヘルスチェック用のRPCを1本呼ぶか、conn.Connect()と状態監視を明示的に書きます。「接続できるまでブロックする」という発想自体を、リトライ前提の設計へ置き換えるほうが筋が通ります。

接続の使い回しとcontextでデッドラインを決める運用基準

次に落ちやすいのが接続の寿命です。ClientConnはRPCごとに作るものではなく、プロセスの寿命に合わせて1本持ち回します。内部で接続の再確立と負荷分散を扱うため、都度作ると接続確立のコストとファイルディスクリプタを無駄に消費します。

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
res, err := client.SayHello(ctx, &greetv1.SayHelloRequest{Name: "issoh"})

デッドラインはcontextで渡し、クライアント側から必ず設定します。gRPCはこの締め切りをサーバへ伝播させるため、上流が諦めた処理を下流が続ける事態を避けられる仕組みです。値の決め方は単純な勘ではなく、呼び出し先の実測p99に安全率を掛けて置きます。無設定のまま本番へ出すと、詰まった1本が接続を占有したまま残ります。

インターセプタ|ログと認証を差し込む位置と1本制約を回避する書き方

ログ、認証、メトリクス、パニック回復。この4つは全RPCで同じ処理を通したい類であり、各ハンドラへ書くとすぐに破綻する処理です。gRPCではインターセプタとして横断的に差し込みます。ただしgrpc-goのAPIには、知らないと踏む制約があります。

サーバ側の指定は1本のみでChainを使うときの適用順序と注意

grpc-go 1.83.2のserver.goでは、UnaryInterceptorのコメントに「Only one unary interceptor can be installed」と書かれており、実装は2回目の設定でpanicします。ストリーム側のStreamInterceptorも同じです。複数並べたいときはChainUnaryInterceptorとChainStreamInterceptorを使います。

srv := grpc.NewServer(
    grpc.ChainUnaryInterceptor(recovery, logging, auth),
    grpc.ChainStreamInterceptor(recoveryStream, loggingStream),
)

適用順序は引数の並び順で、先頭が最も外側、末尾が実際のハンドラに最も近い位置に来ます。パニック回復を最外に置き、認証を内側へ寄せるのがよくある並びです。ログを最外に置くと回復前の異常を拾えない一方、最内に置くと認証で弾いた分が記録から漏れます。何を記録したいかで位置を決めます。

認証をインターセプタで検証する境界とステータスコードの返し方

認証情報はメタデータで運ばれます。インターセプタではmetadata.FromIncomingContextで取り出し、検証に失敗したらハンドラを呼ばずにエラーを返します。

func auth(ctx context.Context, req any, info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (any, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "no metadata")
    }
    if !verify(md.Get("authorization")) {
        return nil, status.Error(codes.Unauthenticated, "invalid token")
    }
    return handler(ctx, req)
}

ここで返すコードの選び方には基準があります。トークンが無い、または壊れている場合はUnauthenticated、本人は分かるが権限が足りない場合はPermissionDeniedです。この2つを混ぜると、クライアント側で再認証すべきか諦めるべきかの分岐が書けなくなります。16種のコードの意味とリトライ可否はgRPCのステータスコードの選び分けにまとめてあります。

境界の引き方として、インターセプタの担当はトークンの検証と呼び出し元の特定までです。「この利用者がこのレコードを触れるか」という判定はドメインの知識を要するため、ハンドラ側か、その下のサービス層へ置きます。ここを混ぜると、インターセプタが業務ロジックを抱えて肥大します。

採用の境界|grpc-goを選ぶ条件とConnectへ寄せる場面

gRPCをGoで書けること自体は、採用の理由になりません。ここでは条件を分けて言い切ります。

grpc-goを本番の標準にする3条件と社内前提をそろえる基準

次の3つがそろっているなら、grpc-goを標準にして構いません。第1に、通信の主な相手がブラウザではなくサーバやバッチであること。第2に、protoの所有者が社内に決まっており、変更のレビュー経路があること。第3に、ロードバランサやサービスメッシュがHTTP/2の通信を素通しできること。この3つ目は見落とされがちで、前段のプロキシがHTTP/2を終端してHTTP/1.1へ落とす構成では、そもそもgRPCが成立しません。

社内前提としてそろえるのは、Goの版とプラグインの版です。grpc-goはGoの最新2版をサポート対象としており、2026年9月時点の安定版は1.27.0です。ここから2版以上遅れた環境が残っていると、ライブラリの更新でビルドが通らなくなります。CIのGo版とローカルの版がずれていないかを、着手前に確かめておきます。

ConnectやRESTへ寄せる3場面と判断を戻すときの基準

見送る判断も条件で示します。1つ目は、ブラウザから直接叩く要求が主である場合。gRPCはブラウザから素で呼べないため、追加の層が必要になります。この場面ではConnectとgRPCの違いと使い方を先に読み、HTTP/1.1でも動くConnectを検討するほうが構成が短くなります。

2つ目は、公開APIとして社外の不特定多数へ出す場合。protoの配布とコード生成を相手に要求する前提が置けないなら、RESTのままにするか、gRPC-Gatewayで両方を出す構成にします。3つ目は、チームにprotoを管理できる人が1人もいない場合。スキーマ駆動の利点は所有と運用に支えられており、放置されたprotoは型安全の看板を掛けた負債になります。

判断を戻す基準も決めておきます。サーバ間通信の本数が5本を超えたら、RESTのままでいるコストのほうが上がります。逆に、gRPCで組んだのにブラウザ向けの要求が半分を超えたなら、Connectへの寄せ替えを検討する時期です。どちらも「本数」という数えられる指標に落としておくと、感覚論での引き直しを避けられます。

受託開発でproto所有と生成のタイミングを決める契約時の取り決め

受託の現場では、技術選定よりも先に決めるべき取り決めがあります。protoの正本をどのリポジトリに置くか、生成物をコミットするか、そして納品後に誰がプラグインを更新するか。この3点を契約時に書いていないと、引き渡し後の最初のproto変更で手が止まります。一創ではAPIの設計と実装を請ける際、この所有と生成の取り決めを設計書に明記した上で着手しています。分担の切り方から相談したい場合はAPI開発・システム連携の支援をご覧ください。

よくある質問

grpc.Dialは今すぐ書き換える必要がありますか?

急ぐ必要はありません。grpc-go 1.83.2のソースには「Will be supported throughout 1.x」と書かれており、1.x系の間は動きます。ただし新規コードはNewClientで書き、既存コードは触るタイミングで直すのが妥当な進め方です。書き換える際はWithBlock系のオプションが無視される点と、既定の名前解決が変わる点を先に確認します。

protoc-gen-goとprotoc-gen-go-grpcの版はそろえるべきですか?

番号をそろえる意味はありません。別リポジトリで管理されており、2026年9月時点で前者は1.36.12、後者は1.6.2です。そろえるべきなのは番号ではなく、チーム内とCIでの版の一致です。Go 1.24以降のtoolディレクティブでgo.modに書き込んでおけば、この一致は機械で担保できます。

生成したGoコードはリポジトリにコミットすべきですか?

Goではコミットする運用が扱いやすいと考えています。エディタの補完が最初から効き、レビューで型の変更が見えるためです。ただし生成漏れが起きるので、CIでbuf generateを実行して差分が出ないことを検査する仕掛けを同時に入れます。生成物を除外する運用なら、開発者全員のプラグイン版が固定されている前提が必要です。

protocを直接使うのとbufを使うのはどちらがよいですか?

protoが数本で、プラグインも2つだけならprotocの直打ちで足ります。分岐点はプラグインが3つ以上になるか、protoのディレクトリが複数に分かれるかです。加えてbufには互換検査があり、フィールド番号の付け替えのような危険な変更をCIで止められます。この検査を使いたい時点で、乗り換える価値が出ます。

Goで書いたサーバをPythonやRustのクライアントから呼べますか?

呼べます。protoが同一であれば言語をまたいだ相互接続はgRPCの前提そのものです。確認すべきは3点で、同じprotoの版を参照しているか、TLSの設定が両側でそろっているか、圧縮の指定が食い違っていないかです。protoの正本を1か所に決め、各言語のリポジトリはそこから生成する形にしておくと、ずれの検出が早くなります。モバイルのクライアントを足す場合は、gRPC FlutterでのDart側の生成手順が同じprotoからの生成にあたります。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.15 テックブログ リゾートトラスト不正アクセス:会員2.6万件・カハラ横浜3.6万件の対象確認と窓口
  2. 2026.10.16 テックブログ バーガーキング不正アクセスで約1,118万件の情報漏洩|対象確認・案内メールの真偽と運営側のログ点検
  3. 2026.10.16 テックブログ ソースネクスト不正アクセスで最大2,252万件流出|ログインできない理由・パスワード再設定と会員APIの点検
  4. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更の手順
  5. 2026.10.16 テックブログ ワラウ不正アクセスで連携トークン流出|Yahoo! JAPAN ID再連携と対象確認

RELATED POSTS 関連記事

目次