起因
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 插件生成:
UserServiceServerinterface(你实现 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 → 强推有阻力
踩过的坑
-
proto 字段 reserved:删字段要
reserved 5占位防 future
wire-incompatible。 -
enum 默认 0 值:proto3 必须有 ENUM_UNSPECIFIED = 0;不写
迁移问题大。 -
timestamp / duration 用 google.protobuf.Timestamp / Duration:
不要用 int64 自己定义。Well-known types 跨语言兼容。 -
streaming RPC 错误处理:mid-stream error 客户端要正确退出。
ctx cancel + 关 stream。 -
gRPC client 不复用 connection:每次 NewClient 新 connection
→ DoS PG / DB。client 在应用启动时建一次 + 全局共享。
登录后参与评论。