3. RESTful API 设计原则
RESTful API 是微服务与前后端分离架构中最主流的接口契约风格。本章围绕资源命名、HTTP 动词、状态码与版本控制四个核心维度,给出可直接落地的设计规范与 Go 实现建议。
3.1 资源命名规范
资源是 REST 的核心抽象,命名必须遵循统一、可读、可预测的原则。
| 规范项 | 推荐做法 | 反例 |
|---|---|---|
| 名词复数 | /users、/orders | /getUser、/user |
| 小写字母 + 连字符 | /order-items | /orderItems、/order_items |
| 层级关系 | /users/{uid}/orders | /orders?user_id=123 |
| 避免动词 | /users/{uid}/activate(用 PATCH) | /activateUser |
| 查询参数过滤 | /orders?status=paid&page=1 | /orders/page/1/status/paid |
Go 实践建议:在路由注册时使用
chi 或 gin 的路径参数,保持 URL 模板与资源命名一致。例如 r.Get("/users/{uid}/orders", handler.ListUserOrders)。
3.2 HTTP 动词使用
每个动词对应一种标准操作语义,不可混用。
| 动词 | 操作 | 幂等 | 安全 | 典型场景 |
|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 列表、详情 |
| POST | 创建资源 | 否 | 否 | 新增订单 |
| PUT | 全量替换 | 是 | 否 | 更新用户全部字段 |
| PATCH | 部分更新 | 否 | 否 | 修改用户邮箱 |
| DELETE | 删除资源 | 是 | 否 | 删除评论 |
常见误区:不要用 GET 请求执行状态变更操作(如
GET /users/delete/1);不要用 POST 实现幂等查询(应改用 GET + 查询参数)。
// Go 路由示例 — 动词与路径分离
r.GET("/orders", handler.ListOrders)
r.POST("/orders", handler.CreateOrder)
r.GET("/orders/{oid}", handler.GetOrder)
r.PUT("/orders/{oid}", handler.ReplaceOrder)
r.PATCH("/orders/{oid}", handler.PatchOrder)
r.DELETE("/orders/{oid}", handler.DeleteOrder)
3.3 状态码选择
状态码是响应契约的第一行语义,客户端依赖它做分支处理。选择错误会导致调用方逻辑混乱。
| 范围 | 常用码 | 含义 | 使用条件 |
|---|---|---|---|
| 2xx 成功 | 200 | OK | GET、PUT、PATCH 成功返回 |
| 201 | Created | POST 创建资源成功 | |
| 204 | No Content | DELETE 成功,无响应体 | |
| 4xx 客户端错误 | 400 | Bad Request | 请求体校验失败 |
| 401 | Unauthorized | 未提供或无效认证 | |
| 403 | Forbidden | 无权限访问资源 | |
| 404 | Not Found | 资源不存在 | |
| 409 | Conflict | 资源冲突(如重复创建) | |
| 422 | Unprocessable Entity | 业务逻辑校验失败 | |
| 5xx 服务端错误 | 500 | Internal Server Error | 未捕获异常 |
| 503 | Service Unavailable | 熔断、限流、维护中 |
Go 错误映射示例:在 handler 层统一将业务错误码转为 HTTP 状态码,避免在业务逻辑中直接返回 500。
// 状态码映射函数
func httpStatusFromError(err error) int {
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrConflict):
return http.StatusConflict
case errors.Is(err, ErrValidation):
return http.StatusUnprocessableEntity
default:
return http.StatusInternalServerError
}
}
3.4 版本控制策略
API 版本控制是向后兼容的基石。主流策略有三种,各有适用场景。
| 策略 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径 | /v1/users、/v2/users | 直观、缓存友好 | URL 膨胀 |
| 请求头 | Accept: application/vnd.myapp.v1+json | URL 干净 | 调试不直观 |
| 查询参数 | /users?version=1 | 实现简单 | 缓存混乱、语义弱 |
推荐方案:对外部公开 API 使用 URL 路径版本(
/v1/);内部微服务间使用 gRPC 或自定义头版本。Go 中可通过中间件统一提取版本号并注入 context。
// 版本中间件示例
func VersionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
version := chi.URLParam(r, "version")
ctx := context.WithValue(r.Context(), "apiVersion", version)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
3.5 综合设计检查清单
在发布接口契约前,对照以下清单逐项确认。
- 资源命名:是否全部使用名词复数、小写连字符、层级路径?
- 动词语义:GET 是否只读?POST 是否非幂等?PUT 是否全量?PATCH 是否部分?
- 状态码:成功是否返回 200/201/204?客户端错误是否返回 4xx?服务端错误是否返回 5xx?
- 版本控制:是否明确版本策略?是否在路由层统一处理?是否在文档中标注废弃版本?
- 错误体:是否包含
code、message、details字段?
Go 项目落地:在
api/ 目录下按版本组织 handler 文件,例如 api/v1/user_handler.go、api/v2/user_handler.go。路由注册时通过 RouteGroup 隔离版本,避免版本间路由冲突。