golang-grpc
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo gRPC (Production)
Go gRPC(生产环境)
Overview
概述
gRPC provides strongly-typed RPC APIs backed by Protocol Buffers, with first-class streaming support and excellent performance for service-to-service communication. This skill focuses on production defaults: versioned protos, deadlines, error codes, interceptors, health checks, TLS, and testability.
gRPC 基于 Protocol Buffers 提供强类型的 RPC API,具备一流的流式处理支持,在服务间通信中性能优异。本内容聚焦生产环境的默认实践:带版本的 Protos、截止时间、错误码、拦截器、健康检查、TLS 以及可测试性。
Quick Start
快速开始
1) Define a versioned protobuf API
1) 定义带版本的 Protobuf API
✅ Correct: versioned package
proto
// 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;
}❌ Wrong: unversioned package (hard to evolve)
proto
package users;✅ 正确示例:带版本的包
proto
// 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;
}❌ 错误示例:无版本的包(难以演进)
proto
package users;2) Generate Go code
2) 生成 Go 代码
Install generators:
bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestGenerate:
bash
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安装生成器:
bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest生成代码:
bash
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.proto3) Implement server with deadlines and status codes
3) 实现带截止时间和状态码的服务端
✅ Correct: validate + map errors to gRPC codes
go
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
}❌ Wrong: return raw errors (clients lose code semantics)
go
return nil, errors.New("user not found")✅ 正确示例:参数校验 + 错误映射为 gRPC 码
go
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
}❌ 错误示例:返回原始错误(客户端无法获取错误码语义)
go
return nil, errors.New("user not found")Core Concepts
核心概念
Deadlines and cancellation
截止时间与取消
Make every call bounded; enforce server-side timeouts for expensive handlers.
✅ Correct: require deadline
go
if _, ok := ctx.Deadline(); !ok {
return nil, status.Error(codes.InvalidArgument, "deadline required")
}为每个调用设置时间限制;对耗时的处理器强制设置服务端超时。
✅ 正确示例:要求设置截止时间
go
if _, ok := ctx.Deadline(); !ok {
return nil, status.Error(codes.InvalidArgument, "deadline required")
}Metadata
元数据
Use metadata for auth/session correlation, not for primary request data.
✅ Correct: read auth token from metadata
go
md, _ := metadata.FromIncomingContext(ctx)
auth := ""
if vals := md.Get("authorization"); len(vals) > 0 {
auth = vals[0]
}元数据用于认证/会话关联,而非承载主要请求数据。
✅ 正确示例:从元数据中读取认证令牌
go
md, _ := metadata.FromIncomingContext(ctx)
auth := ""
if vals := md.Get("authorization"); len(vals) > 0 {
auth = vals[0]
}Interceptors (Middleware)
拦截器(中间件)
Use interceptors for cross-cutting concerns: auth, logging, metrics, tracing, request IDs.
✅ Correct: unary interceptor with request ID
go
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
}
}使用拦截器处理横切关注点:认证、日志、指标、链路追踪、请求ID。
✅ 正确示例:带请求ID的一元拦截器
go
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
}
}Streaming patterns
流式处理模式
Server streaming (paginate or stream results)
服务端流式处理(分页或流式返回结果)
✅ Correct: stop on ctx.Done()
go
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
}✅ 正确示例:在 ctx.Done() 时停止
go
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
}Unary vs streaming decision
一元调用 vs 流式调用的决策
- Use unary for single request/response and simple retries.
- Use server streaming for large result sets or continuous updates.
- Use client streaming for bulk uploads with one final response.
- Use bidirectional streaming for interactive protocols.
- 对于单次请求/响应和简单重试场景,使用一元调用。
- 对于大型结果集或持续更新场景,使用服务端流式处理。
- 对于批量上传并返回单个最终响应的场景,使用客户端流式处理。
- 对于交互式协议,使用双向流式处理。
Production Hardening
生产环境加固
Health checks and reflection
健康检查与反射
Add health service; enable reflection only in non-production environments.
✅ Correct: health + conditional reflection
go
hs := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, hs)
if env != "production" {
reflection.Register(s)
}添加健康服务;仅在非生产环境启用反射。
✅ 正确示例:健康服务 + 条件反射
go
hs := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, hs)
if env != "production" {
reflection.Register(s)
}Graceful shutdown
优雅关闭
Prefer with a deadline.
GracefulStop✅ Correct: graceful stop
go
stopped := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(stopped)
}()
select {
case <-stopped:
case <-time.After(10 * time.Second):
grpcServer.Stop()
}优先使用带截止时间的 。
GracefulStop✅ 正确示例:优雅停止
go
stopped := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(stopped)
}()
select {
case <-stopped:
case <-time.After(10 * time.Second):
grpcServer.Stop()
}TLS
TLS
Use TLS (or mTLS) in production; avoid insecure credentials outside local dev.
✅ Correct: server TLS
go
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil { return err }
grpcServer := grpc.NewServer(grpc.Creds(creds))生产环境使用 TLS(或 mTLS);本地开发外避免使用不安全凭证。
✅ 正确示例:服务端 TLS
go
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil { return err }
grpcServer := grpc.NewServer(grpc.Creds(creds))Testing (bufconn)
测试(使用 bufconn)
Test gRPC handlers without opening real sockets using .
bufconn✅ Correct: in-memory gRPC test server
go
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使用 在不开启真实套接字的情况下测试 gRPC 处理器。
bufconn✅ 正确示例:内存中的 gRPC 测试服务端
go
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
_ = errAnti-Patterns
反模式
-
Ignore deadlines: unbounded handlers cause tail latency and resource exhaustion.
-
Return string errors: map domain errors towith
codes.*orstatus.Error.status.Errorf -
Stream without backpressure: stop onand handle
ctx.Done()errors.Send -
Expose reflection in production: treat reflection as a discovery surface.
-
忽略截止时间:无限制的处理器会导致尾部延迟和资源耗尽。
-
返回字符串错误:使用或
status.Error将领域错误映射为status.Errorf。codes.* -
流式处理无背压:在时停止,并处理
ctx.Done()错误。Send -
生产环境暴露反射:反射属于发现层面的功能,需谨慎使用。
Troubleshooting
故障排查
Symptom: clients see UNKNOWN
errors
UNKNOWN症状:客户端收到 UNKNOWN
错误
UNKNOWNActions:
- Return instead of raw errors.
status.Error(codes.X, "...") - Wrap domain errors into typed errors, then map to gRPC codes.
解决措施:
- 使用 替代返回原始错误。
status.Error(codes.X, "...") - 将领域错误包装为类型化错误,再映射为 gRPC 码。
Symptom: slow/hanging requests
症状:请求缓慢/挂起
Actions:
- Require deadlines and propagate to downstream calls.
ctx - Add server-side timeouts and bounded concurrency in repositories.
解决措施:
- 要求设置截止时间,并将 传递给下游调用。
ctx - 在服务端添加超时限制,并在仓储层设置并发上限。
Symptom: flaky streaming
症状:流式处理不稳定
Actions:
- Stop streaming on and handle
ctx.Done()errors.stream.Send - Avoid buffering entire result sets before sending.
解决措施:
- 在 时停止流式处理,并处理
ctx.Done()错误。stream.Send - 避免在发送前缓冲整个结果集。
Resources
参考资源
- gRPC Go: https://github.com/grpc/grpc-go
- Protobuf Go: https://pkg.go.dev/google.golang.org/protobuf
- gRPC error codes: https://grpc.io/docs/guides/error/
- gRPC Go:https://github.com/grpc/grpc-go
- Protobuf Go:https://pkg.go.dev/google.golang.org/protobuf
- gRPC 错误码:https://grpc.io/docs/guides/error/