mcpField
G
go-grpc

go-grpc

github.com/eduardo-sl開発8391 回インストール2 週間前に更新
リポジトリ

go-grpc について

Go 開発者向けに、契約優先で gRPC サービスとクライアントを構築するためのスキルです。バージョン付き protobuf 契約、意味のあるステータスコード、期限の伝播、インターセプター、ストリーミング、インメモリテストを重視します。

主な機能

  1. 01バージョン付き protobuf 契約設計
  2. 02意味に沿った gRPC ステータスコード
  3. 03期限とコンテキストの伝播
  4. 04単項・ストリームインターセプター
  5. 05ストリーミングとクライアント設定
  6. 06bufconn テストの指針

活用シーン

  1. 01バージョン付き RPC インターフェースの設計
  2. 02ドメインエラーのステータスコードへの変換
  3. 03サービス呼び出し間の期限伝播
  4. 04ネットワークを使わない gRPC のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-grpc

インストール後、Claude Code で/go-grpcを入力して読み込まれたか確認します。

同梱ファイルreferences/clients.mdreferences/testing.md

詳細

Go gRPC サービス

gRPC のコントラクトファーストモデルの効果を得るには、コントラクトを API として扱う必要があります。バージョン付きパッケージ、意図的に設定したエラーコード、すべての箇所でのデッドライン、横断的な関心事に対応するインターセプターを用意します。

必要に応じて読み込む詳細なリファレンス資料:

  • references/clients.md — NewClient、サービス設定による再試行とロードバランシング、両側でのキープアライブ、ステータスコードの読み取り。
  • references/testing.md — メモリー内の bufconn サーバーとクライアント、ステータスコードのアサート。

以下の概要だけでは不十分な場合にのみ、リファレンスファイルを読んでください。

1. Proto の設計ルール

syntax = "proto3";

package payment.v1;                            // version IN the package
option go_package = "github.com/acme/payment-service/gen/payment/v1;paymentv1";

service PaymentService {
  rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse);
}

message CreatePaymentRequest {                 // one request/response pair
  string order_id = 1;                         // per RPC, always — even if
  int64 amount_cents = 2;                      // empty today
}

message CreatePaymentResponse {
  Payment payment = 1;
}
  • パッケージでバージョンを指定します(payment.v1)。破壊的変更の場合は payment.v2 を使います。
  • フィールドタグを再利用したり、番号を付け直したりしないでください。削除したタグは reserved 3, 7; で予約します。
  • RPC ごとに専用の Request/Response メッセージを用意します。後からフィールドを追加するのは問題ありませんが、共有メッセージを変更すると、それを使うすべての RPC が壊れます。
  • buf またはバージョンを固定した protoc を使って make generate で生成し、ツールチェーンの差異にビルドが依存しないよう、生成コードをコミットします。
  • CI では buf lint と buf breaking --against '.git#branch=main' を実行し、番号が付け直されたフィールドや削除された RPC がクライアントに届く前に検出します。フィールドのルール(長さ、範囲、形式)は protovalidate アノテーションで定義し、インターセプターで適用します。

2. エラー:文字列ではなくステータスコード

ドメインエラーを一か所でマッピングして、status.Error を返します。

func (s *Server) CreatePayment(ctx context.Context, req *pb.CreatePaymentRequest) (*pb.CreatePaymentResponse, error) {
    p, err := s.svc.Create(ctx, toDomain(req))
    if err != nil {
        return nil, toStatus(err)
    }
    return &pb.CreatePaymentResponse{Payment: fromDomain(p)}, nil
}

func toStatus(err error) error {
    switch {
    case errors.Is(err, domain.ErrNotFound):
        return status.Error(codes.NotFound, "payment not found")
    case errors.Is(err, domain.ErrDuplicate):
        return status.Error(codes.AlreadyExists, "payment already exists")
    case errors.Is(err, context.DeadlineExceeded):
        return status.Error(codes.DeadlineExceeded, "timed out")
    default:
        return status.Error(codes.Internal, "internal error") // no details leak
    }
}

重要なコードの意味:InvalidArgument(状態にかかわらず不正なリクエスト)、FailedPrecondition(不正な状態)、NotFound、AlreadyExists、Unauthenticated と PermissionDenied の使い分け、Unavailable(再試行可能)、Internal(バグ)。クライアントは status.FromError(err) でコードを読み取ります。メッセージを解析してはいけません。

3. デッドラインは必須

// Client — every call gets a deadline
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
resp, err := client.CreatePayment(ctx, req)

// Server — check before expensive work
if err := ctx.Err(); err != nil {
    return nil, status.FromContextError(err).Err()
}

サーバーはコンテキストを通じてクライアントのデッドラインを引き継ぎます。キャンセルがエンドツーエンドで伝播するよう、すべての下流呼び出し(DB、他の RPC)に ctx を渡してください。

4. 横断的な関心事にはインターセプターを使う

ハンドラーはビジネスロジックだけを扱い、リカバリー、認証、ログ記録、メトリクスはインターセプターに置きます。

srv := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        recoveryInterceptor,   // outermost: panic → codes.Internal
        loggingInterceptor,
        authInterceptor,
    ),
)

func loggingInterceptor(ctx context.Context, req any,
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    slog.InfoContext(ctx, "rpc",
        slog.String("method", info.FullMethod),
        slog.Duration("duration", time.Since(start)),
        slog.String("code", status.Code(err).String()),
    )
    return resp, err
}

順序が重要です。最初にリカバリー(最外層)、次にオブザーバビリティ、最後に認証を配置します。ストリーミング RPC には、対応する StreamInterceptor 版が必要です。

5. ストリーミング

  • 大きな結果セットにはサーバーストリーミングを使います。ループ内で stream.Send を呼び、ステータス付きで中断するには nil 以外のエラーを返します。
  • クライアントストリーミング/双方向ストリーミングは、プロトコル上本当に必要な場合にのみ使います。開いた各ストリームがゴルーチンとフロー制御の状態を保持するためです。
  • 常に ctx.Done() で終了します。
func (s *Server) WatchPayments(req *pb.WatchRequest, stream pb.PaymentService_WatchPaymentsServer) error {
    for {
        select {
        case <-stream.Context().Done():
            return status.FromContextError(stream.Context().Err()).Err()
        case ev := <-s.events:
            if err := stream.Send(toProto(ev)); err != nil {
                return err
            }
        }
    }
}

6. クライアント

  • 起動時にターゲットごとに一度 grpc.NewClient(非推奨の Dial に代わるもの)を呼び出し、*grpc.ClientConn を共有します。接続は多重化を行い、再接続します。
  • サービス設定で再試行、タイムアウト、ロードバランシングを設定します。冪等なメソッドに限り UNAVAILABLE を再試行し、round_robin を使います(デフォルトの pick_first はすべてのリクエストを 1 つのバックエンドに送ります)。
  • クライアントのキープアライブ ping は、サーバーの EnforcementPolicy(デフォルトの MinTime は 5 分)で許可されている必要があります。許可されていない場合、サーバーは too_many_pings で接続を閉じます。
  • status.Code(err) で分岐し、メッセージで分岐しないでください。

例:references/clients.md。

7. テスト

生成されたクライアントを使い、bufconn 経由でテストします。ネットワークは使わず、本番用インターセプターを備えた実際のサーバーをテストできます。WithContextDialer を指定して passthrough:///bufnet に接続します。NewClient のデフォルトの dns リゾルバーは名前解決を試みるためです。status.Code(err) でアサートします。例:references/testing.md。

8. 本番サーバーのセットアップ

lis, err := net.Listen("tcp", cfg.Addr)
if err != nil {
    return fmt.Errorf("listen: %w", err)
}

srv := grpc.NewServer(
    grpc.Creds(creds),                // TLS; mTLS for service-to-service
    grpc.MaxRecvMsgSize(4<<20),       // explicit; size it to the largest valid request
    grpc.ChainUnaryInterceptor(...),
)
pb.RegisterPaymentServiceServer(srv, server)

healthSrv := health.NewServer() // grpc.health.v1 — load balancers need it
healthpb.RegisterHealthServer(srv, healthSrv)
reflection.Register(srv)        // grpcurl/debugging; gate on non-prod if policy requires

go func() {
    <-ctx.Done()
    stopped := make(chan struct{})
    go func() { srv.GracefulStop(); close(stopped) }()
    select {
    case <-stopped:                 // in-flight RPCs finished
    case <-time.After(10 * time.Second):
        srv.Stop()                  // force after grace period
    }
}()

return srv.Serve(lis)

mTLS では、サーバーの tls.Config に ClientCAs と ClientAuth: tls.RequireAndVerifyClientCert を設定します。ハンドラーはメタデータを信用せず、peer.FromContext(ctx) から呼び出し元の検証済み ID を読み取ります。

検証チェックリスト

  1. Proto パッケージはバージョン管理(*.v1)されていること。タグを再利用せず、削除には reserved を使用すること
  2. RPC ごとに専用の Request/Response メッセージを用意すること
  3. 固定したツール(buf/protoc)で生成コードを作成してコミットし、CI で buf breaking を実行すること
  4. ハンドラーのすべてのエラーは、意味的に正しいコードを指定した status.Error であること
  5. codes.Internal の応答で内部エラーのテキストが漏えいしないこと
  6. すべてのクライアント呼び出しにデッドラインを設定し、すべてのレイヤーに ctx を伝播すること
  7. リカバリー、ロギング、認証をチェーン形式のインターセプター(単項+ストリーム)として実装すること
  8. ストリームでは stream.Context().Done() を選択すること
  9. ヘルスサービスを登録し、強制フォールバック付きのグレースフル停止を行うこと。TLS(内部では mTLS)と明示的な受信制限を設定すること
  10. 実行中のサーバーに対する grpcurl のスモークテスト(または生成クライアントのテスト)が成功すること
  11. ターゲットごとに共有 ClientConn を 1 つ使用し、grpc.NewClient で構築すること。再試行は UNAVAILABLE に限定すること

コメント