RESTful API 设计:构建优雅的 Web 接口
在当今的软件开发中,API(应用程序接口)已经成为连接不同系统和服务的桥梁。RESTful API 因其简单性、灵活性和可扩展性,成为了最流行的 API 设计风格之一。
本文将深入探讨如何设计优秀的 RESTful API,让你的接口既符合规范,又易于使用。
什么是 REST?
REST(Representational State Transfer,表述性状态转移)是一种软件架构风格,由 Roy Fielding 在 2000 年的博士论文中首次提出。REST 的核心原则包括:
- 资源(Resource):一切皆资源,用 URI 标识
- 表述(Representation):资源可以有多种表述形式(JSON、XML 等)
- 状态转移(State Transfer):通过 HTTP 方法操作资源
- 无状态(Stateless):每个请求包含所有必要信息
- 统一接口(Uniform Interface):使用标准的 HTTP 方法
URL 设计原则
使用名词而非动词
❌ 错误示例:
GET /getUser?id=123
POST /createUser
DELETE /deleteUser/123
✅ 正确示例:
GET /users/123
POST /users
DELETE /users/123
URL 应该表示资源,操作由 HTTP 方法决定。
使用复数名词
❌ 错误:/user/123
✅ 正确:/users/123
保持一致性,即使某个资源只有一个实例。
使用层级结构表示关系
GET /users/123/orders # 获取用户 123 的所有订单
GET /users/123/orders/456 # 获取用户 123 的订单 456
GET /users/123/profile # 获取用户 123 的个人资料
但层级不宜过深,通常不超过 2 层。
使用小写字母和连字符
❌ 错误:/UserProfiles
❌ 错误:/user_profiles
✅ 正确:/user-profiles
版本号管理
方式 1:URL 版本(推荐)
GET /v1/users
GET /v2/users
方式 2:请求头版本
GET /users
Header: Accept: application/vnd.myapi.v2+json
HTTP 方法的正确使用
GET - 获取资源
// 获取资源列表
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
// 获取单个资源
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := extractID(r.URL.Path)
user, err := findUser(id)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// 路由注册
mux.HandleFunc("/users", getUsersHandler)
mux.HandleFunc("/users/", getUserHandler)
特点:
- 幂等:多次请求结果相同
- 安全:不修改服务器状态
- 可缓存
POST - 创建资源
func createUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 创建用户
id, err := createUser(&user)
if err != nil {
http.Error(w, "Failed to create user", http.StatusInternalServerError)
return
}
// 返回 201 Created 和资源位置
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Location", fmt.Sprintf("/users/%d", id))
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
特点:
- 非幂等:每次请求都会创建新资源
- 返回 201 Created 状态码
- 包含 Location 头指向新资源
PUT - 完整更新资源
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id := extractID(r.URL.Path)
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 完整更新
if err := replaceUser(id, &user); err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
}
特点:
- 幂等:多次请求结果相同
- 需要提供资源的完整数据
- 通常返回 204 No Content
PATCH - 部分更新资源
func patchUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id := extractID(r.URL.Path)
// 解析部分更新字段
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 部分更新
if err := updateUser(id, updates); err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
}
特点:
- 非幂等(理论上)
- 只需要提供要修改的字段
- 适合大量字段的资源
DELETE - 删除资源
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id := extractID(r.URL.Path)
if err := deleteUser(id); err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
}
特点:
- 幂等:删除已删除的资源不算错误
- 返回 204 No Content
状态码的正确使用
成功状态码
200 OK - 请求成功(GET、PUT、PATCH、DELETE)
201 Created - 资源创建成功(POST)
204 No Content - 请求成功但无返回内容(PUT、PATCH、DELETE)
客户端错误
400 Bad Request - 请求格式错误
401 Unauthorized - 未认证
403 Forbidden - 无权限
404 Not Found - 资源不存在
405 Method Not Allowed - 方法不允许
409 Conflict - 资源冲突
422 Unprocessable Entity - 请求格式正确但语义错误
服务器错误
500 Internal Server Error - 服务器内部错误
503 Service Unavailable - 服务不可用
查询参数设计
过滤
GET /users?status=active
GET /products?category=electronics&brand=apple
排序
GET /users?sort=created_at
GET /users?sort=-created_at # 降序
GET /users?sort=name,-created_at # 多字段排序
分页
方式 1:offset + limit
GET /users?offset=20&limit=10
方式 2:page + per_page
GET /users?page=2&per_page=10
方式 3:cursor(推荐用于大数据集)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=10
字段选择
GET /users?fields=id,name,email
只返回指定字段,减少数据传输。
响应格式设计
统一响应结构
{
"data": { ... },
"meta": {
"page": 1,
"per_page": 10,
"total": 100
},
"links": {
"self": "/users?page=1",
"next": "/users?page=2",
"prev": null
}
}
错误响应
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Must be greater than 0"
}
]
}
}
实战:完整的用户 API
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserStore struct {
mu sync.RWMutex
users map[int]*User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]*User),
nextID: 1,
}
}
var store = NewUserStore()
// Response 统一响应结构
type Response struct {
Data interface{} `json:"data,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
}
type Meta struct {
Total int `json:"total,omitempty"`
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{Data: data})
}
func respondError(w http.ResponseWriter, status int, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Error: &ErrorInfo{Code: code, Message: message},
})
}
// GET /users - 获取用户列表
func listUsers(w http.ResponseWriter, r *http.Request) {
store.mu.RLock()
defer store.mu.RUnlock()
users := make([]*User, 0, len(store.users))
for _, user := range store.users {
users = append(users, user)
}
respondJSON(w, http.StatusOK, users)
}
// GET /users/:id - 获取单个用户
func getUser(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
id, err := strconv.Atoi(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID")
return
}
store.mu.RLock()
defer store.mu.RUnlock()
user, exists := store.users[id]
if !exists {
respondError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
return
}
respondJSON(w, http.StatusOK, user)
}
// POST /users - 创建用户
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
respondError(w, http.StatusBadRequest, "INVALID_BODY", "Invalid request body")
return
}
// 验证
if user.Name == "" {
respondError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR", "Name is required")
return
}
store.mu.Lock()
user.ID = store.nextID
store.users[user.ID] = &user
store.nextID++
store.mu.Unlock()
w.Header().Set("Location", fmt.Sprintf("/users/%d", user.ID))
respondJSON(w, http.StatusCreated, user)
}
// PUT /users/:id - 更新用户
func updateUser(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
id, err := strconv.Atoi(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID")
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
respondError(w, http.StatusBadRequest, "INVALID_BODY", "Invalid request body")
return
}
store.mu.Lock()
defer store.mu.Unlock()
if _, exists := store.users[id]; !exists {
respondError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
return
}
user.ID = id
store.users[id] = &user
w.WriteHeader(http.StatusNoContent)
}
// DELETE /users/:id - 删除用户
func deleteUser(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
id, err := strconv.Atoi(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID")
return
}
store.mu.Lock()
defer store.mu.Unlock()
if _, exists := store.users[id]; !exists {
respondError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
return
}
delete(store.users, id)
w.WriteHeader(http.StatusNoContent)
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == "/users" {
switch r.Method {
case http.MethodGet:
listUsers(w, r)
case http.MethodPost:
createUser(w, r)
default:
respondError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
}
return
}
// /users/:id
switch r.Method {
case http.MethodGet:
getUser(w, r)
case http.MethodPut:
updateUser(w, r)
case http.MethodDelete:
deleteUser(w, r)
default:
respondError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/users/", usersHandler)
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", mux)
}
API 文档
使用 Swagger/OpenAPI 规范编写 API 文档:
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
summary: Get all users
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
'201':
description: User created
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
required:
- name
- email
总结
设计优秀的 RESTful API 需要遵循以下原则:
- 使用名词表示资源,动词由 HTTP 方法承担
- 正确使用 HTTP 方法:GET、POST、PUT、PATCH、DELETE
- 返回合适的状态码,让客户端了解请求结果
- 设计清晰的 URL 结构,使用层级表示关系
- 提供灵活的查询参数,支持过滤、排序、分页
- 统一的响应格式,包含数据、元数据和链接
- 完善的错误处理,提供有意义的错误信息
- 编写详细的文档,使用 OpenAPI 规范
常见问题解答(FAQ)
Q1: REST 和 RPC 怎么选?
- REST:适合面向资源的操作,浏览器友好,易于缓存
- RPC(如 gRPC):适合高性能内部通信,强类型接口
- 对外 API 首选 REST,内部服务间通信可选 gRPC
Q2: API 版本应该在 URL 还是 Header 里?
URL 版本(/v1/users)更直观、更易调试、对 CDN 更友好。Header 版本(Accept: application/vnd.myapi.v1+json)更 RESTful 但更难用。推荐 URL 版本。
Q3: 分页分页用 offset/limit 还是 cursor?
- offset/limit:实现简单,支持跳页,但大数据量时性能差
- cursor:性能好,适合实时数据,但不支持跳页
- 普通场景用 offset/limit,大数据量或实时数据用 cursor
Q4: PUT 和 PATCH 到底有什么区别?
- PUT:完整替换,客户端提供完整的资源表示
- PATCH:部分更新,客户端只提供要修改的字段
- 如果字段多且频繁部分更新,用 PATCH;否则用 PUT
Q5: 如何处理 API 的并发更新冲突?
使用乐观锁(ETag 或版本号):
// 响应头
w.Header().Set("ETag", fmt.Sprintf("%d", user.Version))
// 条件更新
if_match := r.Header.Get("If-Match")
if if_match != strconv.Itoa(user.Version) {
respondError(w, http.StatusPreconditionFailed, "VERSION_CONFLICT", "资源已被修改")
return
}
记住:好的 API 设计能让开发者事半功倍,而糟糕的设计会让使用者痛苦不堪。
实战扩展:API 限流与熔断
速率限制中间件
package main
import (
"net/http"
"sync"
"time"
)
// RateLimiter 简单令牌桶限流器
type RateLimiter struct {
mu sync.Mutex
tokens float64
lastTime time.Time
rate float64 // 每秒产生的令牌数
burst float64 // 桶容量
}
func NewRateLimiter(rate, burst int) *RateLimiter {
return &RateLimiter{
tokens: float64(burst),
lastTime: time.Now(),
rate: float64(rate),
burst: float64(burst),
}
}
func (rl *RateLimiter) Allow() bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
elapsed := now.Sub(rl.lastTime).Seconds()
rl.lastTime = now
rl.tokens = min(rl.burst, rl.tokens+elapsed*rl.rate)
if rl.tokens >= 1 {
rl.tokens--
return true
}
return false
}
func rateLimitMiddleware(rl *RateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !rl.Allow() {
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。