第三章 API协议与选择:RESTful、GraphQL、gRPC 与版本控制

做企业级接入,选协议就像选交通工具。去机场坐地铁,搬家叫货车,临时买瓶水骑共享单车——没有的好坏,只有合不合适的场景。我见过太多团队一上来就拍脑袋定协议,结果后期接口改不动、客户端骂娘、服务端不敢动版本号。今天咱们就把这四种方案掰开揉碎了聊,顺便把版本控制这块硬骨头啃干净。

3.1 RESTful API — 老牌王者,但别滥用

REST 深入人心,资源导向、动词用 HTTP 方法、状态码清晰。但你想想看,真的每个接口都符合 REST 规范吗?我在项目中遇到过好几回:团队说用 REST,实际写出来的接口全是 POST /api/v1/doSomething,压根没遵循资源化设计。

核心原则

  • 资源 URL:/users、/orders/{id},不用动词
  • HTTP 方法:GET(查)、POST(增)、PUT(全量改)、PATCH(部分改)、DELETE(删)
  • 状态码:200 正常、201 创建、400 参数错、404 不存在、409 冲突、500 服务端炸

以我个人习惯,REST 最适用的是 CRUD 密集型场景——用户管理、订单查询、商品列表。但如果你要在一个请求里拿多个嵌套资源(比如用户 + 订单 + 商品详情),REST 就尴尬了:要么让客户端发多个请求,要么服务端搞个聚合接口,设计上开始拧巴。

我曾经踩过的坑: 用 REST 做实时推送接口。客户端轮询 /notifications 每 2 秒一次,数据库连接池直接被打满。后来换了 WebSocket + gRPC 双向流,问题才解决。REST 不适合高频实时场景,别硬套。

3.2 GraphQL — 灵活查询,谨防性能黑洞

GraphQL 解决的是“数据过取”和“数据不足”问题。客户端想要什么字段,自己定义查询结构,服务端只返回精确的数据。听起来很完美?嗯,这里要注意:客户端的一次查询,可能在服务端引发 N 次数据库查询。

我当年接一个电商后台的 GraphQL 网关,前端写了个查询:

query {
  orders(status: "shipped") {
    id
    user { name email }
    items { product { name price } quantity }
  }
}

看着简单吧?实际解析下来,order 先查一次,每个 order 的 user 再查一次,每个 order 的 items 又查一次,每个 item 的 product 再查一次。如果订单有 100 条,数据库查询次数就是 1 + 100 + 100 + 100 = 301 次!

避坑指南: 一定要用 DataLoader 做批量缓存 + 批量加载(Batching + Caching)。另外,对查询深度和复杂度设置阈值,防止恶意查询把服务端拖垮。我曾经给一个公开 API 的 GraphQL 端点加了最大深度 7 层、每个请求最多 100 个节点的限制,之后再没出现过 OOM。

GraphQL 适合:前端团队强势、多端(Web/iOS/Android)场景、数据模型频繁变更的产品。不适合:纯粹的内部 RPC、简单 CRUD(杀鸡用牛刀)。

3.3 gRPC — 高性能二进制协议,微服务标配

gRPC 基于 HTTP/2 + Protobuf,序列化快、支持双向流、强类型定义。说白了就是让服务之间通信像本地函数调用一样顺畅。我个人非常喜欢它的一点:自动生成桩代码,只要写好 .proto 文件,客户端和服务端代码一键生成,类型永远没错。

看一个简单的 .proto 定义:

syntax = "proto3";

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
}

message GetUserRequest {
  int32 user_id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

这里 ListUsers 用了服务端流式返回,客户端可以一边接收一边处理,不用等全部数据。如果换成 REST 搞这个,就得用分页或者 WebSocket,麻烦不少。

gRPC 的适用场景

场景推荐度原因
微服务内部通信★★★★★低延迟、强类型、流式支持
移动端/前端★★☆☆☆浏览器原生不支持 HTTP/2 太完美,需要 gRPC-Web 代理
外部开放 API★★★☆☆需要额外提供 REST 转码,生态不如 REST 成熟
IoT / 低带宽★★★★☆Protobuf 比 JSON 小 3~10 倍

注意: gRPC 的负载均衡需要客户端感知服务发现(DNS + 后端名称解析),不能像 HTTP 那样直接挂 Nginx 轮询。我曾在 K8s 里用 gRPC 时没配 Headless Service 和 gRPC 专用负载均衡策略,结果连接全跑到一个 pod 上,其他 pod 空载。调了一下午才找到原因。

3.4 接口版本控制 — 逃不掉的难题

不管选哪种协议,版本控制都是刚需。老版本不能立刻下掉,新版本又要快速上线。常见的方案有三种,我把它们的优劣用表格列出来:

方案实现方式优点缺点适合场景
URL 路径版本/api/v1/users → /api/v2/users简单直观,调试直接看 URL容易产生大量冗余代码,路由膨胀团队小、版本迭代慢
请求头版本Accept: application/vnd.myapp.v1+jsonURL 整洁,一个端点可返回多版本调试隐藏,客户端容易忘传头API 网关统一处理
Header / 参数版本?version=1与 URL 路径类似,更灵活缓存容易混淆,语义不明确快速原型

我个人习惯用 URL 路径版本 + 配合 API 网关做路由分发。为什么?因为你看日志或者抓包时,一眼就知道哪个版本的接口出了问题。如果藏在请求头里,排查时得多一步。

我常用的版本策略:

  1. 主版本号(v1、v2)放在路径,次版本号(v1.1)不体现在 URL,通过请求头字段 X-API-Minor-Version 控制小变更
  2. 每个版本维护一个独立的 handler 包,方便后期独立下线
  3. 新接口功能必须新增版本号,不允许在旧版本里加参数导致非兼容变化
  4. 使用 semantic versioning 风格:API 版本 MAJOR.MINOR,破坏性变更就升 MAJOR
// 我的 Go 项目结构示例
apis/
├── v1/
│   ├── handler.go
│   ├── order.go
│   └── user.go
├── v2/
│   ├── handler.go
│   ├── order.go
│   └── user.go
└── router.go  // 根据版本号路由到对应 handler

3.5 协议选择决策框架

说到这,你可能会问:到底该用哪个?我画了一张决策流程图,帮你快速落定:

API协议选择决策流程 API协议选择决策流程 你的调用方是谁? 浏览器 / 移动端 内部微服务间 前端需求灵活? GraphQL REST 性能要求高? gRPC REST 不管选哪个,都要做版本控制 决策要点 • 浏览器/移动端:优先 REST / GraphQL,避免直接暴露 gRPC • 微服务间:优先 gRPC / REST,用 Protobuf 时注意负载均衡 • 版本控制:URL 路径版本 + 网关分离,破坏性变更升主版本 • 不要混合使用多种协议在同一端点上(增加维护复杂度)

你看这张图,核心问题就两个:谁在调用 + 性能要求有多高。如果调用方是浏览器,你又想给前端足够灵活的数据查询能力,那就选 GraphQL;如果只想简单暴露 CRUD,REST 足矣。内部服务之间,需要高性能 RPC 的用 gRPC,追求简单调试的用 REST。记住:不要为了炫技而选 gRPC,也不要因为怕麻烦死守 REST。

3.6 我的实战建议

  • 新项目起步: 如果不是非常明确的高性能 RPC 场景,先用 REST。混熟了再局部引入 gRPC 或 GraphQL。我见过一个团队上来就全上 gRPC,结果前端对接时发现浏览器不支持,又加了一层 gRPC-Web 代理,徒增复杂度。
  • 版本策略: 在 API 网关层统一做版本映射,不要在各个微服务里各搞一套。网关可以按版本号路由到不同上游,也能做灰度切换。
  • 文档自动化: 不管用哪个协议,都要求自动生成文档。REST 用 OpenAPI(Swagger),GraphQL 用 Introspection 自动生成 Playground,gRPC 用 protoc + grpcurl 生成手册。
  • 兼容性测试: 每次发布新版本,跑一遍老版本的集成测试。我曾经因为改了某个 gRPC 字段的编号(从 4 改成 5),导致旧客户端解析出错,全量回滚才救回来。Protobuf 的字段编号一旦发布绝不能改。

协议选择没有银弹。懂原理、看场景、留余地——这就是我做了多年 API 集成后最想说的。