gRPC vs REST:Go 服务间通信怎么选 + grpc-gateway 兼容方案

起因

10 个 Go 微服务之间互通:

  • REST + JSON:易调试 / 浏览器友好 / 工具多
  • gRPC:类型严格 / 性能好 / 双向 streaming

之前都用 REST。后来内部 service 间改 gRPC,对外仍 REST,
靠 grpc-gateway 一份 proto 自动生成两套。

gRPC 优势(service 间)

1. 强类型

// user.proto
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}

message User {
  int64 id = 1;
  string email = 2;
  string name = 3;
  int64 created_at = 4;
}

message GetUserRequest {
  int64 id = 1;
}

protoc 生成 Go / Python / TypeScript / Java client + server stub。
改字段类型 → 客户端编译报错
vs REST 改 JSON schema → 客户端运行时炸。

2. 性能

  • HTTP/2 + protobuf binary:3-5x 比 JSON 快 + 小
  • 长连接复用:避免每请求 TCP / TLS 握手
  • 服务端 streaming / 双向 streaming 原生支持

我们一个高 QPS 服务从 REST + JSON 改 gRPC:

REST gRPC
P50 延迟 5ms 1.5ms
单连接 RPS 500 5000
CPU 占用 35% 12%

3-10x 提升。

3. 双向 streaming

service ChatService {
  rpc Chat(stream Message) returns (stream Message);
}

客户端 / 服务端都能持续发消息。WebSocket-like 但带类型。

适合:实时通知、聊天、log tail、bidirectional sync。

REST 优势(对外 API)

  • 浏览器直接调用(不需要特殊 client)
  • curl / Postman / 任意 HTTP tool 调试
  • HTTP cache friendly(GET / If-Modified-Since 等)
  • OpenAPI / Swagger 文档丰富
  • 简单 SDK 自动生成(openapi-generator 覆盖语言广)

对外暴露给"未知客户端" → 必须 REST 或 GraphQL。

两者结合:grpc-gateway

grpc-gateway 让一份 .proto 同时生成 gRPC server + REST proxy:

import "google/api/annotations.proto";

service UserService {
  rpc GetUser(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{id}"
    };
  }

  rpc CreateUser(CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/v1/users"
      body: "*"
    };
  }
}

protoc + grpc-gateway 插件生成:

  • UserServiceServer interface(你实现 gRPC server)
  • RegisterUserServiceHandlerServer(注册 REST → gRPC proxy)
// main.go
func main() {
    // gRPC server
    grpcServer := grpc.NewServer()
    userpb.RegisterUserServiceServer(grpcServer, &userServer{})

    // 启 gRPC :9090
    lis, _ := net.Listen("tcp", ":9090")
    go grpcServer.Serve(lis)

    // REST gateway :8080
    ctx := context.Background()
    mux := runtime.NewServeMux()
    err := userpb.RegisterUserServiceHandlerServer(ctx, mux, &userServer{})
    http.ListenAndServe(":8080", mux)
}

客户端两种方式调:

# REST
curl http://localhost:8080/v1/users/42

# gRPC
grpcurl -plaintext localhost:9090 user.UserService/GetUser -d '{"id": 42}'

服务端逻辑写一遍,两套 API 自动并存。
对外 REST,对内 gRPC

connect-rpc:现代替代

buf.build 出的 connect-go

  • 兼容 gRPC 协议
  • 同时支持 REST + JSON + gRPC-Web(无需 grpc-gateway)
  • 浏览器直接调
  • 比 grpc-gateway 简洁
type UserService struct{}
func (s *UserService) GetUser(ctx context.Context, req *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.User], error) {
    return connect.NewResponse(&userv1.User{Id: req.Msg.Id, Name: "..."}), nil
}

mux := http.NewServeMux()
mux.Handle(userv1connect.NewUserServiceHandler(&UserService{}))
http.ListenAndServe(":8080", mux)
curl -X POST http://localhost:8080/user.v1.UserService/GetUser \
  -H 'Content-Type: application/json' \
  -d '{"id": 42}'
# {"id": 42, "name": "..."}

JSON over HTTP/1 / HTTP/2 / gRPC 同 endpoint 自动适配。
2024 后新项目推荐

gRPC client(Go)

import "google.golang.org/grpc"

conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()

client := userpb.NewUserServiceClient(conn)
resp, err := client.GetUser(ctx, &userpb.GetUserRequest{Id: 42})
fmt.Println(resp.Name)

调 client 跟调本地函数一样。

配置 keep-alive

conn, _ := grpc.Dial(addr,
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
        Time:                10 * time.Second,
        Timeout:             3 * time.Second,
        PermitWithoutStream: true,
    }),
)

NAT 后面长连接 ping 保活。

连接池

gRPC 单 connection 默认多路复用(HTTP/2 stream)→ 一般不需要 pool。
高 QPS 时 grpc.WithDefaultServiceConfig('{"loadBalancingPolicy":"round_robin"}')
+ DNS 解析多 IP 自动负载均衡。

错误处理

// 服务端
return nil, status.Errorf(codes.NotFound, "user %d not found", id)

// 客户端
resp, err := client.GetUser(...)
if err != nil {
    if status.Code(err) == codes.NotFound {
        // handle 404 equivalent
    }
}

gRPC 错误码标准化(NotFound / Unauthenticated / PermissionDenied / etc)。
比 REST 的"HTTP status + 自定义 body" 类型严格。

interceptor(middleware)

func loggingInterceptor(ctx context.Context, req interface{},
                         info *grpc.UnaryServerInfo,
                         handler grpc.UnaryHandler) (interface{}, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("%s took %v err=%v", info.FullMethod, time.Since(start), err)
    return resp, err
}

server := grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))

或者用现成 middleware 库:
- github.com/grpc-ecosystem/go-grpc-middleware/v2
- prometheus / opentelemetry / auth / recovery / rate-limit

实战 case:微服务架构

我们的架构:

[mobile app / web]    [外部 partner API]
       ↓                       ↓
       └─────── REST + JSON ───┘
                    ↓
            [API Gateway (Go)]
                    ↓
      ┌─────────────┼─────────────┐
      ↓             ↓             ↓
   [User svc]  [Order svc]   [Payment svc]
                    ↑             ↑
                    └─── gRPC ────┘
  • 对外 REST + OpenAPI 文档
  • 内部 service 间 gRPC(性能 + 类型)
  • gateway 翻译

proto 仓库化

Monorepo / 单独 repo 存所有 .proto:

proto-repo/
├── user/v1/user.proto
├── order/v1/order.proto
└── buf.yaml

buf 工具 lint + 兼容性检查:

buf lint
buf breaking --against '.git#branch=main'    # PR 不能破坏兼容
buf generate                                   # 生成 Go / TS / Python

防止"改 proto 删字段把客户端搞挂"。

何时不用 gRPC

  • 客户端是浏览器 + 没 backend proxy → 用 REST / GraphQL(gRPC-Web 也是
    选项但复杂)
  • 极简内部 tool → REST 更友好
  • 团队不愿学 protobuf → 强推有阻力

踩过的坑

  1. proto 字段 reserved:删字段要 reserved 5 占位防 future
    wire-incompatible。

  2. enum 默认 0 值:proto3 必须有 ENUM_UNSPECIFIED = 0;不写
    迁移问题大。

  3. timestamp / duration 用 google.protobuf.Timestamp / Duration
    不要用 int64 自己定义。Well-known types 跨语言兼容。

  4. streaming RPC 错误处理:mid-stream error 客户端要正确退出。
    ctx cancel + 关 stream。

  5. gRPC client 不复用 connection:每次 NewClient 新 connection
    → DoS PG / DB。client 在应用启动时建一次 + 全局共享。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。