golang-grpc

golang-grpc

熱門

提供 Golang 微服務的 gRPC 使用指南、protobuf 組織方式及生產環境就緒模式。在實作、審查或除錯 gRPC 伺服器/用戶端、撰寫 proto 檔案、設定攔截器、以狀態碼處理 gRPC 錯誤、設定 TLS/mTLS、使用 bufconn 測試或處理串流 RPC 時使用。

2261星標
150分支
更新於 2026/6/6
SKILL.md
readonlyread-only
name
golang-grpc
description

提供 Golang 微服務的 gRPC 使用指南、protobuf 組織方式及生產環境就緒模式。在實作、審查或除錯 gRPC 伺服器/用戶端、撰寫 proto 檔案、設定攔截器、以狀態碼處理 gRPC 錯誤、設定 TLS/mTLS、使用 bufconn 測試或處理串流 RPC 時使用。

角色: 你是一位 Go 分散式系統工程師。你設計的 gRPC 服務必須正確且可操作——適當的狀態碼、截止時間、攔截器和優雅關機與正常路徑同等重要。

模式:

  • 建置模式 — 從頭實作新的 gRPC 伺服器或用戶端。
  • 審查模式 — 稽核現有 gRPC 程式碼的正確性、安全性及可操作性問題。

相依套件:

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

Go gRPC 最佳實務

將 gRPC 視為純傳輸層——與商業邏輯分離。官方 Go 實作為 google.golang.org/grpc

此技能並非詳盡無遺。請參閱函式庫文件與程式碼範例以取得更多資訊。Context7 可作為探索平台。如需 Go 套件文件、版本、符號及已知漏洞,→ 請參閱 samber/cc-skills-golang@golang-pkg-go-dev 技能。

快速參考

關注點 套件 / 工具
服務定義 protocbuf 搭配 .proto 檔案
程式碼生成 protoc-gen-goprotoc-gen-go-grpc
錯誤處理 google.golang.org/grpc/status 搭配 codes
豐富錯誤細節 google.golang.org/genproto/googleapis/rpc/errdetails
攔截器 grpc.ChainUnaryInterceptorgrpc.ChainStreamInterceptor
中介軟體生態系 github.com/grpc-ecosystem/go-grpc-middleware
測試 google.golang.org/grpc/test/bufconn
TLS / mTLS google.golang.org/grpc/credentials
健康檢查 google.golang.org/grpc/health

Proto 檔案組織

按領域組織,使用版本化目錄(proto/user/v1/)。一律使用 Request/Response 包裝訊息——裸型別如 string 無法在後續新增欄位。使用 buf generateprotoc 生成。

Proto 與程式碼生成參考

伺服器實作

  • 實作健康檢查服務(grpc_health_v1)—— Kubernetes 探針需要它來判斷就緒狀態
  • 使用攔截器處理橫切關注點(日誌、認證、復原)——保持商業邏輯乾淨
  • 使用 GracefulStop() 並搭配超時回退至 Stop()——在防止掛起的同時耗盡進行中的 RPC
  • 在生產環境中停用反射——它會暴露完整的 API 表面
srv := grpc.NewServer(
    grpc.ChainUnaryInterceptor(loggingInterceptor, recoveryInterceptor),
)
pb.RegisterUserServiceServer(srv, svc)
healthpb.RegisterHealthServer(srv, health.NewServer())

go srv.Serve(lis)

// 收到關機訊號時:
stopped := make(chan struct{})
go func() { srv.GracefulStop(); close(stopped) }()
select {
case <-stopped:
case <-time.After(15 * time.Second):
    srv.Stop()
}

攔截器模式

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

用戶端實作

  • 重複使用連線——gRPC 在單一 HTTP/2 連線上多工處理 RPC;每個請求建立新連線會浪費 TCP/TLS 握手
  • 為每個呼叫設定截止時間(context.WithTimeout)——沒有截止時間時,慢速上游會無限期掛起 goroutine
  • 使用 round_robin 搭配無頭 Kubernetes 服務,透過 dns:/// 方案
  • 透過 metadata.NewOutgoingContext 傳遞中繼資料(認證令牌、追蹤 ID)
conn, err := grpc.NewClient("dns:///user-service:50051",
    grpc.WithTransportCredentials(creds),
    grpc.WithDefaultServiceConfig(`{
        "loadBalancingPolicy": "round_robin",
        "methodConfig": [{
            "name": [{"service": ""}],
            "timeout": "5s",
            "retryPolicy": {
                "maxAttempts": 3,
                "initialBackoff": "0.1s",
                "maxBackoff": "1s",
                "backoffMultiplier": 2,
                "retryableStatusCodes": ["UNAVAILABLE"]
            }
        }]
    }`),
)
client := pb.NewUserServiceClient(conn)

錯誤處理

一律使用 status.Error 搭配特定代碼回傳 gRPC 錯誤——原始 error 會變成 codes.Unknown,無法提供客戶端可操作的資訊。客戶端根據代碼決定重試、快速失敗或降級。

代碼 使用時機
InvalidArgument 格式錯誤的輸入(缺少欄位、格式錯誤)
NotFound 實體不存在
AlreadyExists 建立失敗,實體已存在
PermissionDenied 呼叫者缺乏權限
Unauthenticated 缺少或無效的令牌
FailedPrecondition 系統未處於所需狀態
ResourceExhausted 速率限制或配額超額
Unavailable 暫時性問題,可安全重試
Internal 非預期的錯誤
DeadlineExceeded 逾時
// ✗ 不好——呼叫端收到 codes.Unknown,無法決定是否重試
return nil, fmt.Errorf("user not found")

// ✓ 好——特定代碼讓客戶端能適當應對
if errors.Is(err, ErrNotFound) {
    return nil, status.Errorf(codes.NotFound, "user %q not found", req.UserId)
}
return nil, status.Errorf(codes.Internal, "lookup failed: %v", err)

對於欄位層級的驗證錯誤,透過 status.WithDetails 附加 errdetails.BadRequest

串流

模式 使用案例
伺服器串流 伺服器發送序列(日誌追蹤、結果集)
客戶端串流 客戶端發送序列,伺服器回應一次(檔案上傳、批次)
雙向串流 雙方獨立發送(聊天、即時同步)

偏好串流而非大型單一訊息——避免每訊息大小限制並降低記憶體壓力。

func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    for _, u := range users {
        if err := stream.Send(u); err != nil {
            return err
        }
    }
    return nil
}

測試

使用 bufconn 進行記憶體內連線,可完整演練 gRPC 堆疊(序列化、攔截器、中繼資料)而無網路開銷。務必測試錯誤情境是否回傳預期的 gRPC 狀態碼。

測試模式與範例

安全性

  • 生產環境 MUST 啟用 TLS——憑證透過中繼資料傳輸
  • 服務間認證使用 mTLS 或委派給服務網格(Istio、Linkerd)
  • 使用者認證實作 credentials.PerRPCCredentials 並在認證攔截器中驗證令牌
  • 生產環境 SHOULD 停用反射以防止 API 探索

效能

設定 目的 典型值
keepalive.ServerParameters.Time 空閒連線的 Ping 間隔 30s
keepalive.ServerParameters.Timeout Ping 確認超時 10s
grpc.MaxRecvMsgSize 覆寫大型負載的 4 MB 預設值 16 MB
連線池 高負載串流的多個連線 4 個連線

大多數服務不需要連線池——在增加複雜度前先進行效能剖析。

常見錯誤

錯誤 修正
回傳原始 error 變成 codes.Unknown——客戶端無法決定是否重試。使用 status.Errorf 搭配特定代碼
客戶端呼叫未設定截止時間 慢速上游無限期掛起。一律使用 context.WithTimeout
每個請求建立新連線 浪費 TCP/TLS 握手。建立一次,重複使用——HTTP/2 多工處理 RPC
生產環境啟用反射 讓攻擊者能列舉所有方法。僅在開發/測試環境啟用
所有錯誤都使用 codes.Internal 錯誤代碼破壞客戶端重試邏輯。Unavailable 觸發重試;InvalidArgument 則否
使用裸型別作為 RPC 參數 無法為 string 新增欄位。包裝訊息允許向後相容的演進
缺少健康檢查服務 Kubernetes 無法判斷就緒狀態,部署期間會殺死 Pod
忽略 context 取消 呼叫端放棄後長時間操作仍繼續。檢查 ctx.Err()

交叉參考

  • → 請參閱 samber/cc-skills-golang@golang-context 技能以了解截止時間與取消模式
  • → 請參閱 samber/cc-skills-golang@golang-error-handling 技能以了解 gRPC 錯誤對應至 Go 錯誤
  • → 請參閱 samber/cc-skills-golang@golang-observability 技能以了解 gRPC 攔截器(日誌、追蹤、度量)
  • → 請參閱 samber/cc-skills-golang@golang-testing 技能以了解使用 bufconn 進行 gRPC 測試