golang-grpc

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go 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@latest
Generate:
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.proto

3) 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
GracefulStop
with a deadline.
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
使用
bufconn
在不开启真实套接字的情况下测试 gRPC 处理器。
正确示例:内存中的 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
_ = err

Anti-Patterns

反模式

  • Ignore deadlines: unbounded handlers cause tail latency and resource exhaustion.
  • Return string errors: map domain errors to
    codes.*
    with
    status.Error
    or
    status.Errorf
    .
  • Stream without backpressure: stop on
    ctx.Done()
    and handle
    Send
    errors.
  • 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
错误

Actions:
  • Return
    status.Error(codes.X, "...")
    instead of raw errors.
  • 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
    ctx
    to downstream calls.
  • Add server-side timeouts and bounded concurrency in repositories.
解决措施:
  • 要求设置截止时间,并将
    ctx
    传递给下游调用。
  • 在服务端添加超时限制,并在仓储层设置并发上限。

Symptom: flaky streaming

症状:流式处理不稳定

Actions:
  • Stop streaming on
    ctx.Done()
    and handle
    stream.Send
    errors.
  • Avoid buffering entire result sets before sending.
解决措施:
  • ctx.Done()
    时停止流式处理,并处理
    stream.Send
    错误。
  • 避免在发送前缓冲整个结果集。

Resources

参考资源