4、状态码的合理使用

HTTP 状态码是服务器对客户端请求的响应标识,是 RESTful API 设计中沟通语义的关键桥梁。合理使用状态码能让调用方无需解析响应体即可判断请求结果,提升接口的可读性与可调试性。本章按 2xx、3xx、4xx、5xx 四个类别逐一拆解,并结合 Go Web 业务场景给出最佳实践。

4.1 2xx 成功:精确表达操作结果

2xx 系列表示请求已成功被服务器接收、理解并处理。不同操作应选用不同的成功码,避免一律返回 200 OK。

状态码 含义 典型场景 响应体
200 OK 请求成功,响应体包含请求的资源或结果 GET 查询、PUT 更新、部分 POST 操作 返回资源对象或列表
201 Created 资源创建成功 POST 新增资源 返回新资源对象,并携带 Location 头指向新资源 URI
204 No Content 请求成功,但无内容返回 DELETE 删除、部分 PUT/PATCH 静默更新 无响应体
实践建议:在 Go 的 net/http 或 Gin 框架中,显式设置状态码而非依赖默认值。例如:w.WriteHeader(http.StatusCreated) 或 c.JSON(http.StatusCreated, obj)。
// Go Gin 示例:创建用户返回 201
func CreateUser(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    // 业务逻辑:保存 user 到数据库
    c.JSON(http.StatusCreated, user)
}

// 删除用户返回 204
func DeleteUser(c *gin.Context) {
    id := c.Param("id")
    // 业务逻辑:删除 id 对应的用户
    c.Status(http.StatusNoContent)
}

4.2 3xx 重定向:谨慎使用,避免滥用

3xx 系列表示客户端需要采取进一步操作才能完成请求。在 RESTful API 中,重定向主要用于资源迁移、URI 规范化或临时转移,不建议在常规 CRUD 中频繁使用。

  • 301 Moved Permanently:资源已永久迁移到新 URI,客户端应更新缓存。
  • 302 Found:资源临时位于不同 URI,客户端应继续使用原 URI。
  • 307 Temporary Redirect:与 302 类似,但要求客户端保持请求方法不变。
  • 308 Permanent Redirect:与 301 类似,但要求客户端保持请求方法不变。
避坑提示:API 内部不应依赖重定向实现业务逻辑。浏览器或 HTTP 客户端默认会跟随重定向,可能导致 POST 请求被转为 GET,造成数据丢失或幂等问题。若必须重定向,优先使用 307/308 以保留请求方法。

4.3 4xx 客户端错误:明确错误原因

4xx 系列表示请求包含错误,服务器无法处理。这是 API 设计中最重要的反馈类别,必须给出清晰、具体的错误信息,帮助调用方快速定位问题。

状态码 含义 典型场景 响应体建议
400 Bad Request 请求语法错误或参数无效 JSON 格式错误、缺少必填字段、字段类型不匹配 返回具体字段错误列表
401 Unauthorized 未提供身份凭证或凭证无效 缺少 Token、Token 过期、签名错误 返回认证失败原因
403 Forbidden 已认证但无权限访问资源 普通用户尝试删除管理员资源 返回权限不足提示
404 Not Found 请求的资源不存在 查询不存在的用户 ID、访问已删除的订单 返回资源标识与不存在提示
409 Conflict 请求与当前资源状态冲突 创建重复用户名、版本号不匹配的更新 返回冲突详情与当前状态
错误响应体统一格式示例:
{
  "error": {
    "code": "USERNAME_EXISTS",
    "message": "用户名 'alice' 已被占用",
    "field": "username",
    "status": 409
  }
}

4.4 5xx 服务器错误:兜底但不掩盖

5xx 系列表示服务器在处理请求时发生内部错误。这类错误应尽量少出现,一旦出现必须记录详细日志,但返回给客户端的信息应保持克制,避免泄露敏感细节。

  • 500 Internal Server Error:通用服务器错误,无法归入其他 5xx 类别。
  • 502 Bad Gateway:上游服务返回无效响应。
  • 503 Service Unavailable:服务暂时过载或维护中,可配合 Retry-After 头使用。
  • 504 Gateway Timeout:上游服务超时未响应。
安全原则:生产环境禁止在 5xx 响应体中返回堆栈信息、数据库查询语句或框架版本号。统一返回 {"error": {"code": "INTERNAL_ERROR", "message": "服务器内部错误,请稍后重试"}},并在服务端日志中记录完整错误链路。

4.5 状态码选择决策流程

以下 SVG 流程图帮助开发者在收到请求后快速确定应返回的状态码:

HTTP 状态码选择决策流程 收到客户端请求 请求格式/参数正确? 否 400 Bad Request 是 认证通过? 否 401 Unauthorized 权限足够? 否 403 Forbidden 资源存在? 否 404 Not Found 存在冲突? 是 409 Conflict 判断操作类型 200 OK 201 Created 204 No Content 说明 GET → 200 POST → 201 DELETE → 204

4.6 状态码使用避坑清单

  • 不要用 200 表示一切:即使业务失败,也应返回 4xx 而非 200 + 错误码。例如用户余额不足应返回 400 或 409,而非 200 + {"code": "INSUFFICIENT_BALANCE"}。
  • 不要混淆 401 和 403:401 表示未认证或认证失败,客户端应重新提供凭证;403 表示已认证但无权限,客户端不应重试。
  • 不要返回 500 掩盖业务异常:数据库唯一约束冲突应返回 409,而非捕获异常后返回 500。
  • 不要忽略 204 的响应体:204 必须无响应体,即使返回空 JSON 对象 {} 也不符合规范。
  • 不要滥用 302:API 内部重定向优先使用 307/308 以保持请求方法不变。
Go 中间件统一状态码处理示例:
// 统一错误响应结构
type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Status  int    `json:"status"`
}

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        if len(c.Errors) > 0 {
            err := c.Errors.Last()
            c.JSON(http.StatusInternalServerError, APIError{
                Code:    "INTERNAL_ERROR",
                Message: "服务器内部错误",
                Status:  500,
            })
        }
    }
}