mcpField
G
golang-grpc

golang-grpc

github.com/bobmatnyc開発78512 回インストール15 週間前に更新
リポジトリ

golang-grpc について

golang-grpcは、gRPCサービスを開発するGo開発者向けの本番運用スキルです。バージョン付きProtobuf API、ストリーミング、エラー処理、運用上の対策を扱います。

主な機能

  1. 01バージョン付きProtobuf APIとコード生成
  2. 02デッドラインとキャンセル処理
  3. 03インターセプターとステータスコードへのエラー変換
  4. 04ストリーミングRPCのパターン
  5. 05ヘルスチェック、TLS、グレースフルシャットダウン

活用シーン

  1. 01進化しやすいgRPC APIの設計
  2. 02RPC呼び出しのタイムアウトとエラー意味論の設定
  3. 03サーバーストリーミング結果の実装
  4. 04bufconnを使ったgRPCハンドラーのテスト

インストール

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

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill golang-grpc

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

同梱ファイルmetadata.json

詳細

Go gRPC(本番環境向け)

概要

gRPC は Protocol Buffers による型安全な RPC API を提供し、ストリーミングを標準でサポートしているほか、サービス間通信で優れたパフォーマンスを発揮します。このスキルでは、本番環境向けのデフォルト設定として、バージョン管理された proto、デッドライン、エラーコード、インターセプター、ヘルスチェック、TLS、テストのしやすさに焦点を当てます。

クイックスタート

1) バージョン管理された protobuf API を定義する

✅ 正しい例:バージョン管理されたパッケージ

// proto/users/v1/users.proto
syntax = "proto3";

package users.v1;
option go_package = "example.com/myapp/gen/users/v1;usersv1";

service UsersService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc ListUsers(ListUsersRequest) returns (stream User);
}

message GetUserRequest { string id = 1; }
message GetUserResponse { User user = 1; }
message ListUsersRequest { int32 page_size = 1; string page_token = 2; }

message User {
  string id = 1;
  string email = 2;
  string display_name = 3;
}

❌ 誤った例:バージョン管理されていないパッケージ(拡張しにくい)

package users;
2) Go コードを生成する

ジェネレーターをインストールします:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

生成します:

protoc -I proto \
  --go_out=./gen --go_opt=paths=source_relative \
  --go-grpc_out=./gen --go-grpc_opt=paths=source_relative \
  proto/users/v1/users.proto
3) デッドラインとステータスコードを使ってサーバーを実装する

✅ 正しい例:検証し、エラーを gRPC コードにマッピングする

package usersvc

import (
    "context"

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

    usersv1 "example.com/myapp/gen/users/v1"
)

type Service struct {
    usersv1.UnimplementedUsersServiceServer
    Repo Repo
}

type Repo interface {
    GetUser(ctx context.Context, id string) (User, error)
}

type User struct {
    ID, Email, DisplayName string
}

func (s *Service) GetUser(ctx context.Context, req *usersv1.GetUserRequest) (*usersv1.GetUserResponse, error) {
    if req.GetId() == "" {
        return nil, status.Error(codes.InvalidArgument, "id is required")
    }

    u, err := s.Repo.GetUser(ctx, req.GetId())
    if err != nil {
        if err == ErrNotFound {
            return nil, status.Error(codes.NotFound, "user not found")
        }
        return nil, status.Error(codes.Internal, "internal error")
    }

    return &usersv1.GetUserResponse{
        User: &usersv1.User{
            Id:          u.ID,
            Email:       u.Email,
            DisplayName: u.DisplayName,
        },
    }, nil
}

❌ 誤った例:生のエラーを返す(クライアントがコードの意味を扱えなくなります)

return nil, errors.New("user not found")

基本概念

デッドラインとキャンセル

すべての呼び出しに制限時間を設け、処理コストの高いハンドラーにはサーバー側のタイムアウトを適用します。

✅ 正しい例:デッドラインを必須にする

if _, ok := ctx.Deadline(); !ok {
    return nil, status.Error(codes.InvalidArgument, "deadline required")
}
メタデータ

メタデータは認証やセッションの関連付けに使用し、主要なリクエストデータには使用しないでください。

✅ 正しい例:メタデータから認証トークンを読み取る

md, _ := metadata.FromIncomingContext(ctx)
auth := ""
if vals := md.Get("authorization"); len(vals) > 0 {
    auth = vals[0]
}

インターセプター(ミドルウェア)

認証、ログ記録、メトリクス、トレース、リクエスト ID など、横断的な関心事にはインターセプターを使用します。

✅ 正しい例:リクエスト ID を使う単項インターセプター

func unaryRequestID() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        id := uuid.NewString()
        ctx = context.WithValue(ctx, requestIDKey{}, id)
        resp, err := handler(ctx, req)
        return resp, err
    }
}

ストリーミングのパターン

サーバーストリーミング(結果をページ分割またはストリーミングする)

✅ 正しい例:ctx.Done() で停止する

func (s *Service) ListUsers(req *usersv1.ListUsersRequest, stream usersv1.UsersService_ListUsersServer) error {
    users, err := s.Repo.ListUsers(stream.Context(), int(req.GetPageSize()))
    if err != nil {
        return status.Error(codes.Internal, "internal error")
    }

    for _, u := range users {
        select {
        case <-stream.Context().Done():
            return stream.Context().Err()
        default:
        }

        if err := stream.Send(&usersv1.User{
            Id:          u.ID,
            Email:       u.Email,
            DisplayName: u.DisplayName,
        }); err != nil {
            return err
        }
    }
    return nil
}
単項とストリーミングの使い分け
  • 単一のリクエスト/レスポンスと単純な再試行には、単項を使用します。
  • 大量の結果や継続的な更新には、サーバーストリーミングを使用します。
  • 一括アップロードして最後に 1 回だけレスポンスを返す場合は、クライアントストリーミングを使用します。
  • インタラクティブなプロトコルには、双方向ストリーミングを使用します。

本番環境向けの強化

ヘルスチェックとリフレクション

ヘルスサービスを追加し、リフレクションは本番環境以外でのみ有効にします。

✅ 正しい例:ヘルスチェックと条件付きリフレクション

hs := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, hs)

if env != "production" {
    reflection.Register(s)
}
グレースフルシャットダウン

デッドラインを設定した GracefulStop を優先してください。

✅ 正しい例:グレースフルストップ

stopped := make(chan struct{})
go func() {
    grpcServer.GracefulStop()
    close(stopped)
}()

select {
case <-stopped:
case <-time.After(10 * time.Second):
    grpcServer.Stop()
}
TLS

本番環境では TLS(または mTLS)を使用し、ローカル開発以外では安全でない認証情報を使用しないでください。

✅ 正しい例:サーバー TLS

creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil { return err }

grpcServer := grpc.NewServer(grpc.Creds(creds))

テスト(bufconn)

bufconn を使用すると、実際のソケットを開かずに gRPC ハンドラーをテストできます。

✅ 正しい例:メモリ内 gRPC テストサーバー

const bufSize = 1024 * 1024

lis := bufconn.Listen(bufSize)
srv := grpc.NewServer()
usersv1.RegisterUsersServiceServer(srv, &Service{Repo: repo})

go func() { _ = srv.Serve(lis) }()

ctx := context.Background()
conn, err := grpc.DialContext(
    ctx,
    "bufnet",
    grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil { t.Fatal(err) }
defer conn.Close()

client := usersv1.NewUsersServiceClient(conn)
resp, err := client.GetUser(ctx, &usersv1.GetUserRequest{Id: "1"})
_ = resp
_ = err

アンチパターン

  • デッドラインを無視する:制限のないハンドラーはテールレイテンシーを増大させ、リソースを枯渇させます。

  • 文字列のエラーを返す:ドメインエラーを status.Error または status.Errorf で codes.* にマッピングします。

  • バックプレッシャーなしでストリーミングする:ctx.Done() で停止し、Send のエラーを処理します。

  • 本番環境でリフレクションを公開する:リフレクションはサービスの発見に利用されるインターフェースとして扱います。

トラブルシューティング

症状:クライアントに UNKNOWN エラーが表示される

対処方法:

  • 生のエラーではなく、status.Error(codes.X, "...") を返します。
  • ドメインエラーを型付きエラーにラップしてから、gRPC コードにマッピングします。
症状:リクエストが遅い、または応答しない

対処方法:

  • デッドラインを必須にし、ctx を下流の呼び出しに伝播させます。
  • サーバー側のタイムアウトと、リポジトリーの同時実行数の上限を設定します。
症状:ストリーミングが不安定

対処方法:

  • ctx.Done() でストリーミングを停止し、stream.Send のエラーを処理します。
  • 送信前に結果セット全体をバッファーに格納しないようにします。

リソース

コメント