3、HTTP方法与CRUD映射:GET(查询)、POST(创建)、PUT(全量更新)、PATCH(部分更新)、DELETE(删除)的语义与幂等性
RESTful 架构的核心是将业务操作抽象为对资源的统一操作,而 HTTP 协议提供的方法(Method)天然对应了数据库的 CRUD(Create、Read、Update、Delete)语义。理解每个方法的语义、副作用以及幂等性,是设计稳定、可预测 API 的基础。
3.1 HTTP 方法与 CRUD 的标准映射
下表展示了五种核心 HTTP 方法与 CRUD 操作的对应关系,以及它们在资源集合(Collection)和单个资源(Resource)上的典型 URL 模式。
| HTTP 方法 | CRUD 操作 | 集合 URL(如 /users) | 资源 URL(如 /users/{id}) | 请求体(Body) |
|---|---|---|---|---|
| GET | 查询(Read) | 列出所有资源 | 获取指定资源详情 | 无(或查询参数) |
| POST | 创建(Create) | 创建新资源 | 通常不使用(或作为子资源工厂) | 新资源的数据 |
| PUT | 全量更新(Update) | 批量替换(不常用) | 替换整个资源 | 完整的资源数据 |
| PATCH | 部分更新(Partial Update) | 通常不使用 | 更新资源的部分字段 | 需要更新的字段及值 |
| DELETE | 删除(Delete) | 清空集合(极少使用) | 删除指定资源 | 无(或可选确认信息) |
设计要点: 在 Go 的 Web 框架(如 Gin、Echo)中,路由注册应严格遵循上述映射。例如,
GET /users 不应执行创建操作,DELETE /users/{id} 不应返回资源列表。
3.2 幂等性:API 可靠性的基石
幂等性(Idempotency)是指:无论请求执行一次还是多次,对服务器资源产生的最终状态都是相同的。这是分布式系统中保证数据一致性的关键特性。
- GET:幂等。多次查询同一资源,返回结果应一致(不考虑并发修改)。
- PUT:幂等。多次执行相同的全量更新,资源最终状态与最后一次请求体一致。
- DELETE:幂等。第一次删除后资源已不存在,后续删除请求应返回相同状态(如 404 或 204)。
- POST:非幂等。多次创建会生成多个不同的资源(如不同的 ID)。
- PATCH:非幂等(默认)。如果 PATCH 操作是增量修改(如
{"count": "count + 1"}),多次执行会导致结果不同。
常见误区: 很多开发者误以为 PATCH 天然幂等。实际上,只有将 PATCH 设计为“设置字段为指定值”时才是幂等的;如果包含“相对操作”(如自增、追加),则必须通过版本号或条件请求来保证幂等性。
3.3 方法语义与状态码的配合
每个 HTTP 方法都有其推荐的响应状态码,这构成了 API 的“语义契约”。
| HTTP 方法 | 成功响应 | 常见错误响应 | 说明 |
|---|---|---|---|
| GET | 200 OK | 404 Not Found, 400 Bad Request | 返回资源表示(JSON/XML) |
| POST | 201 Created | 400 Bad Request, 409 Conflict | 响应头应包含 Location 指向新资源 |
| PUT | 200 OK 或 204 No Content | 400 Bad Request, 404 Not Found | 返回更新后的资源或空体 |
| PATCH | 200 OK 或 204 No Content | 400 Bad Request, 422 Unprocessable Entity | 返回更新后的资源或空体 |
| DELETE | 204 No Content | 404 Not Found | 通常不返回响应体 |
3.4 方法选择与幂等性决策流程
在实际 API 设计中,选择哪个方法不仅取决于操作类型,还取决于对幂等性的要求。下面的流程图可以帮助你快速决策。
3.5 Go 代码实现示例
以下代码展示了在 Gin 框架中如何根据方法语义实现对应的处理函数,并正确处理幂等性。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
var users = []User{{ID: 1, Name: "Alice", Age: 30}}
// GET /users/:id — 幂等查询
func getUser(c *gin.Context) {
id := c.Param("id")
for _, u := range users {
if fmt.Sprintf("%d", u.ID) == id {
c.JSON(http.StatusOK, u)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
}
// POST /users — 非幂等创建
func createUser(c *gin.Context) {
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newUser.ID = len(users) + 1
users = append(users, newUser)
// 返回 201 Created 和 Location 头
c.Header("Location", "/users/"+fmt.Sprintf("%d", newUser.ID))
c.JSON(http.StatusCreated, newUser)
}
// PUT /users/:id — 幂等全量更新
func updateUser(c *gin.Context) {
id := c.Param("id")
var updatedUser User
if err := c.ShouldBindJSON(&updatedUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for i, u := range users {
if fmt.Sprintf("%d", u.ID) == id {
updatedUser.ID = u.ID // 保持 ID 不变
users[i] = updatedUser
c.JSON(http.StatusOK, updatedUser)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
}
// DELETE /users/:id — 幂等删除
func deleteUser(c *gin.Context) {
id := c.Param("id")
for i, u := range users {
if fmt.Sprintf("%d", u.ID) == id {
users = append(users[:i], users[i+1:]...)
c.JSON(http.StatusNoContent, nil)
return
}
}
// 资源已不存在,仍返回 204(幂等)
c.JSON(http.StatusNoContent, nil)
}
func main() {
r := gin.Default()
r.GET("/users/:id", getUser)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.Run(":8080")
}
幂等性实现要点:
- DELETE 幂等: 即使资源不存在,也返回
204 No Content,而不是404,以保证客户端重试时不会收到错误。 - PUT 幂等: 必须要求客户端发送完整的资源表示,服务端用请求体完全替换现有资源。
- POST 非幂等: 每次调用都会创建新资源,服务端应生成唯一 ID 并返回
201 Created。
3.6 方法语义对比总结
为了帮助你在设计 API 时快速查阅,下表总结了各方法的核心特性。
| 特性 | GET | POST | PUT | PATCH | DELETE |
|---|---|---|---|---|---|
| CRUD | Read | Create | Update(全量) | Update(部分) | Delete |
| 幂等性 | 是 | 否 | 是 | 否(默认) | 是 |
| 请求体 | 无 | 有 | 有(完整) | 有(部分) | 无 |
| 成功状态码 | 200 | 201 | 200 / 204 | 200 / 204 | 204 |
| 缓存性 | 可缓存 | 不可缓存 | 不可缓存 | 不可缓存 | 不可缓存 |
| 安全性 | 安全(只读) | 不安全 | 不安全 | 不安全 | 不安全 |
安全性与幂等性的区别: 安全性指方法不会改变服务器状态(GET 是安全的);幂等性指多次执行结果一致(PUT 是幂等的但不安全)。设计 API 时,这两个概念常被混淆,务必区分。