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 成功200OKGET、PUT、PATCH 成功返回
201CreatedPOST 创建资源成功
204No ContentDELETE 成功,无响应体
4xx 客户端错误400Bad Request请求体校验失败
401Unauthorized未提供或无效认证
403Forbidden无权限访问资源
404Not Found资源不存在
409Conflict资源冲突(如重复创建)
422Unprocessable Entity业务逻辑校验失败
5xx 服务端错误500Internal Server Error未捕获异常
503Service 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+jsonURL 干净调试不直观
查询参数/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))
    })
}
RESTful API 请求-响应生命周期 客户端 GET /v1/users 路由 + 版本 中间件 动词匹配 Handler 业务逻辑 查询/写入 数据存储 200 OK / 404 / 500 状态码契约: 2xx 表示成功;4xx 表示客户端需修正请求;5xx 表示服务端故障。客户端不应解析响应体判断成败。 三种版本控制策略对比 URL 路径 GET /v1/users GET /v2/users 优点:直观、缓存友好 缺点:URL 膨胀 推荐:外部公开 API 请求头 Accept: app/vnd.v1+json Accept: app/vnd.v2+json 优点:URL 干净 缺点:调试不直观 推荐:内部微服务 查询参数 GET /users?version=1 GET /users?version=2 优点:实现简单 缺点:缓存混乱 推荐:不推荐生产使用

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 隔离版本,避免版本间路由冲突。