提供 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 技能。
快速參考
| 關注點 | 套件 / 工具 |
|---|---|
| 服務定義 | protoc 或 buf 搭配 .proto 檔案 |
| 程式碼生成 | protoc-gen-go、protoc-gen-go-grpc |
| 錯誤處理 | google.golang.org/grpc/status 搭配 codes |
| 豐富錯誤細節 | google.golang.org/genproto/googleapis/rpc/errdetails |
| 攔截器 | grpc.ChainUnaryInterceptor、grpc.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 generate 或 protoc 生成。
伺服器實作
- 實作健康檢查服務(
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 測試






