4、HTTP 方法与语义:GET/POST/PUT/DELETE/PATCH 的正确使用场景
我在刚接触 API 设计时,总觉得方法选什么都行。反正能调通,谁在乎是 GET 还是 POST?后来在一个金融项目里,就因为 GET 请求不小心改了数据,导致结算对账出了大问题。
其实,每个 HTTP 方法都对应一个明确的语义。错用了,轻则让同事骂娘,重则挖坑给整个系统埋雷。今天我们就来把这几个方法理清楚。
4.1 方法即契约:为什么要严格遵循语义?
说白了,HTTP 方法就是客户端和服务器之间的一纸契约。
- 客户端 通过方法告诉服务器:「我要干什么」。
- 服务器 根据方法判断:「该怎么处理」。
- 中间件(网关、缓存、代理) 也会看方法来决定:「能不能缓存?」、「要不要重试?」。
你想想看,如果用了 POST 去查询数据,缓存层根本不认,每次都得穿透到后端。明明可以走 CDN 缓存的 GET,硬生生搞成了全链路调用。
我个人习惯把方法选择当作接口设计的第一步。方法选对了,后面的参数、返回码、错误处理都会自然对齐。
4.2 GET:查询资源,别干别的
GET 的语义非常单纯:获取资源。它应该是幂等的、安全的。
- 幂等:调用 1 次和 100 次,结果一样。
- 安全:只读,不改变服务端状态。这就是为什么搜索引擎爬虫只发 GET 请求的原因。
我在项目中遇到过有人把删除操作也写成 GET。接口是 GET /users/123/delete。你想想看,搜索引擎来爬一次,用户就被删了。这能不出事吗?
4.3 POST:创建资源,非幂等
POST 是 创建新资源 的标准方法。它没有副作用——不对,它有副作用。每次调用都会在服务器上产生一个新资源。
非幂等是什么意思?
提交两次同样的 POST,会创建两条数据。比如下单接口,用户点了两次「提交」,你以为会报错?实际上生成了两个订单。
我记得在做电商 API 时,后端必须在上层加幂等令牌。否则 POST 本身就是「多发多创建」的语义。这是设计,不是 bug。
4.4 PUT:完整替换资源
PUT 的语义是 完整替换。你给我什么,我就把目标资源变成什么。如果你漏了字段,那个字段就会被重置为空或默认值。
幂等性: 提交一次和十次,最终结果一致。
我在项目中遇到过 PUT 误用的坑:前端只传了修改的字段。比如 PUT /users/123 只传了 {name: "张三"},结果用户的手机号被清空了。因为 PUT 的语义是「整个替换」,不是「局部更新」。
// 正确的 PUT:客户端必须提供完整资源
PUT /users/123
{
"name": "张三",
"phone": "138xxxx",
"email": "zhangsan@example.com"
}
// 如果只想改名字,应该用 PATCH
PATCH /users/123
{
"name": "张三"
}
4.5 DELETE:删除资源,幂等操作
DELETE,字面意思就是删除。它也是幂等的。第一次删除返回 204 No Content,第二次再删同样的资源,应该返回 404 Not Found。
我曾经见过一个实现:首次 DELETE 返回 200,第二次调用返回 400,理由是「资源已删除,不要再删了」。嗯,这其实是理解的偏差。客户端重复发送 DELETE 请求本身就是常见场景(网络重试),服务器应该优雅处理重复请求,而不是报错。
4.6 PATCH:局部更新资源
PATCH 是对 PUT 的补充。它只传递需要修改的字段。
- 非幂等吗? 这要看具体的实现。举例:
PATCH /cart/items传一个{op: "add", itemId: 1},每次调用都会往购物车加一件商品。这就是非幂等的 PATCH。 - 设计建议: 大部分「更新用户资料」「修改配置」的场景,PATCH 都可以设计成幂等。
我记得有一次重构老项目,把所有的 PUT 全改成了 PATCH。因为前端只传修改字段,后端却用 PUT 全量替换,空值覆盖导致数据丢失。从那次之后,我对 PUT 和 PATCH 的区分特别敏感。
4.7 方法选择决策表
我把几个核心维度整理成了表格,方便你对照选择。
| 方法 | 语义 | 幂等 | 安全(只读) | 请求体 | 典型场景 |
|---|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 否 | 列表、详情、搜索 |
| POST | 创建资源 | 否 | 否 | 是 | 下单、注册、上传 |
| PUT | 整体替换 | 是 | 否 | 是 | 更新完整配置 |
| DELETE | 删除资源 | 是 | 否 | 可选 | 用户注销、文档删除 |
| PATCH | 局部更新 | 视实现而定 | 否 | 是 | 修改昵称、调整权限 |
4.8 实战避坑:我踩过的三个方法坑
- 用 POST 做了批量查询:因为查询条件太多,怕 URL 太长。后来发现客户端做了自动重试,同一批数据后台插入了多份「查询日志」,存储爆炸。正确的做法是用 GET 加分页参数,或者明确告诉客户端这是查询。
- PUT 更新时漏了版本号:在没有乐观锁的情况下,两个客户端并发 PUT 同一个资源,后覆盖前,数据丢失。从此我习惯在 PUT 或 PATCH 请求中带上
If-Unmodified-Since或ETag。 - DELETE 返回了 200 和 body:客户端认为成功,但空 body 导致解析异常。其实规范里写得很清楚:DELETE 成功应该返回 204 No Content。如果有附加信息,放响应头里。