RESTful API 设计原则
一、知识点总结
1.1 REST 的本质
REST(Representational State Transfer,表述性状态转移)是由 Roy Fielding 在其 2000 年博士论文中提出的架构风格。它不是协议、不是标准,而是一组设计约束和原则:
- 资源(Resource)为核心:一切抽象为资源,用 URL 标识
- 统一接口:使用标准的 HTTP 方法操作资源
- 无状态(Stateless):每个请求自包含,服务端不保存客户端状态
- 可缓存:响应应显式标注是否可缓存
- 分层系统:客户端不需要知道是否直连服务端还是通过代理/网关
理解 REST 的关键是:面向资源设计,而非面向动作设计 。不是 /getUser?id=1,而是 GET /users/1。
1.2 HTTP 方法语义
RESTful API 的核心是正确使用 HTTP 方法表达操作意图:
| 方法 | 语义 | 幂等性 | 安全性 | 典型用法 |
|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | 读取数据 |
| POST | 创建资源 | ❌ | ❌ | 新建实体 |
| PUT | 全量更新/替换 | ✅ | ❌ | 完整替换 |
| PATCH | 部分更新 | ❌ | ❌ | 修改部分字段 |
| DELETE | 删除资源 | ✅ | ❌ | 删除实体 |
- 幂等性:多次执行结果相同(如 DELETE 删一次和删多次结果一样)
- 安全性:不改变服务器状态(GET 不应产生副作用)
- POST vs PUT:POST 是"创建到集合中"(由服务端分配 ID),PUT 是"替换指定资源"(客户端指定完整资源路径)
1.3 URL 资源命名规范
好的资源命名让 API 自描述:
✅ 推荐做法:
- 名词复数:
/users、/orders、/products - 层级关系:
/users/123/orders(用户 123 的订单列表) - 小写+连字符:
/user-profiles(而非/userProfiles或/UserProfiles)
❌ 避免做法:
- 动词路径:
/getUsers、/createOrder - 单复数混用:
/user和/users语义不清 - 暴露实现细节:
/api/v1/getUserFromMySQL
1.4 HTTP 状态码规范
正确使用状态码让 API 更具可读性和可调试性:
| 类别 | 状态码 | 含义 |
|---|---|---|
| 成功 | 200 OK | 通用成功 |
| 成功 | 201 Created | 资源创建成功 |
| 成功 | 204 No Content | 成功但无返回体(如 DELETE) |
| 客户端错误 | 400 Bad Request | 请求参数错误 |
| 客户端错误 | 401 Unauthorized | 未认证 |
| 客户端错误 | 403 Forbidden | 无权限 |
| 客户端错误 | 404 Not Found | 资源不存在 |
| 客户端错误 | 409 Conflict | 资源冲突(如重复创建) |
| 客户端错误 | 422 Unprocessable Entity | 语义错误(如验证失败) |
| 服务端错误 | 500 Internal Server Error | 服务端内部错误 |
| 服务端错误 | 502 Bad Gateway | 网关错误 |
| 服务端错误 | 503 Service Unavailable | 服务不可用 |
注意:404 表示资源不存在,400 表示请求格式错误,401 表示未认证,403 表示已认证但无权限------这四个是日常最容易混淆的。
1.5 请求/响应体设计
请求体:
- 使用 JSON 格式(
Content-Type: application/json) - 字段命名使用
snake_case(Go 的encoding/json用标签控制) - 必填/选填通过文档约定,API 层面做校验
响应体结构建议:
json
{
"code": 0,
"message": "success",
"data": { ... }
}
或者遵循 HTTP 规范,用状态码表达错误,错误详情放响应体:
json
// 400 Bad Request
{
"error": "validation_failed",
"message": "email format is invalid",
"details": [
{"field": "email", "issue": "must be a valid email"}
]
}
1.6 API 版本控制策略
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL Path | /api/v1/users |
直观、易缓存 | URL 冗长 |
| Header | Accept: application/vnd.api.v1+json |
URL 干净 | 不够直观 |
| Query | /api/users?version=1 |
灵活 | 不符合 REST 理念 |
推荐:URL Path 版本化,简单直观,便于调试和缓存。
1.7 分页、过滤与排序
分页:
- 偏移分页:
?page=2&size=10 - 游标分页:
?after=xxx&limit=10(更适合大数据量)
过滤:
GET /users?status=active&role=adminGET /orders?created_after=2024-01-01
排序:
GET /users?sort=-created_at(-表示降序)GET /users?sort=+name,-age(多字段排序)
二、练习代码
示例 1:规范化的 RESTful 用户 API
go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
// ======== 数据模型 ========
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
type UpdateUserRequest struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Age int `json:"age,omitempty"`
Status string `json:"status,omitempty"`
}
// 统一响应格式
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// ======== 内存存储 ========
type UserStore struct {
mu sync.RWMutex
users map[int]*User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]*User),
nextID: 1,
}
}
func (s *UserStore) Create(req *CreateUserRequest) *User {
s.mu.Lock()
defer s.mu.Unlock()
user := &User{
ID: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
Status: "active",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
s.users[user.ID] = user
s.nextID++
return user
}
func (s *UserStore) Get(id int) (*User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
func (s *UserStore) List() []*User {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]*User, 0, len(s.users))
for _, u := range s.users {
list = append(list, u)
}
return list
}
func (s *UserStore) Update(id int, req *UpdateUserRequest) (*User, bool) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[id]
if !ok {
return nil, false
}
if req.Name != "" {
u.Name = req.Name
}
if req.Email != "" {
u.Email = req.Email
}
if req.Age > 0 {
u.Age = req.Age
}
if req.Status != "" {
u.Status = req.Status
}
u.UpdatedAt = time.Now()
return u, true
}
func (s *UserStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.users[id]
if ok {
delete(s.users, id)
}
return ok
}
// ======== HTTP Handlers ========
type UserHandler struct {
store *UserStore
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 路由分发:/api/v1/users 或 /api/v1/users/{id}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/users")
path = strings.Trim(path, "/")
if path == "" {
switch r.Method {
case http.MethodGet:
h.listUsers(w, r)
case http.MethodPost:
h.createUser(w, r)
default:
writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "method not allowed"})
}
return
}
// 解析 ID
id, err := strconv.Atoi(path)
if err != nil {
writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "invalid user id"})
return
}
switch r.Method {
case http.MethodGet:
h.getUser(w, r, id)
case http.MethodPut:
h.updateUser(w, r, id)
case http.MethodPatch:
h.patchUser(w, r, id)
case http.MethodDelete:
h.deleteUser(w, r, id)
default:
writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "method not allowed"})
}
}
func (h *UserHandler) listUsers(w http.ResponseWriter, r *http.Request) {
users := h.store.List()
writeJSON(w, http.StatusOK, Response{Code: 0, Message: "success", Data: users})
}
func (h *UserHandler) getUser(w http.ResponseWriter, r *http.Request, id int) {
user, ok := h.store.Get(id)
if !ok {
writeJSON(w, http.StatusNotFound, Response{Code: -1, Message: "user not found"})
return
}
writeJSON(w, http.StatusOK, Response{Code: 0, Message: "success", Data: user})
}
func (h *UserHandler) createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "invalid request body: " + err.Error()})
return
}
defer r.Body.Close()
if req.Name == "" || req.Email == "" {
writeJSON(w, http.StatusUnprocessableEntity, Response{Code: -1, Message: "name and email are required"})
return
}
user := h.store.Create(&req)
writeJSON(w, http.StatusCreated, Response{Code: 0, Message: "created", Data: user})
}
func (h *UserHandler) updateUser(w http.ResponseWriter, r *http.Request, id int) {
// PUT:全量替换
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: err.Error()})
return
}
defer r.Body.Close()
user, ok := h.store.Update(id, &req)
if !ok {
writeJSON(w, http.StatusNotFound, Response{Code: -1, Message: "user not found"})
return
}
writeJSON(w, http.StatusOK, Response{Code: 0, Message: "updated", Data: user})
}
func (h *UserHandler) patchUser(w http.ResponseWriter, r *http.Request, id int) {
// PATCH:部分更新
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: err.Error()})
return
}
defer r.Body.Close()
user, ok := h.store.Update(id, &req)
if !ok {
writeJSON(w, http.StatusNotFound, Response{Code: -1, Message: "user not found"})
return
}
writeJSON(w, http.StatusOK, Response{Code: 0, Message: "patched", Data: user})
}
func (h *UserHandler) deleteUser(w http.ResponseWriter, r *http.Request, id int) {
ok := h.store.Delete(id)
if !ok {
writeJSON(w, http.StatusNotFound, Response{Code: -1, Message: "user not found"})
return
}
writeJSON(w, http.StatusNoContent, Response{Code: 0, Message: "deleted"})
}
// writeJSON 统一写入 JSON 响应
func writeJSON(w http.ResponseWriter, status int, resp Response) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("encode json error: %v", err)
}
}
// ======== Main ========
func main() {
store := NewUserStore()
// 预置数据
store.Create(&CreateUserRequest{Name: "Alice", Email: "alice@example.com", Age: 25})
store.Create(&CreateUserRequest{Name: "Bob", Email: "bob@example.com", Age: 30})
mux := http.NewServeMux()
mux.Handle("/api/v1/users/", &UserHandler{store: store})
mux.Handle("/api/v1/users", &UserHandler{store: store})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "RESTful API Server")
fmt.Fprintln(w, " GET /api/v1/users - List users")
fmt.Fprintln(w, " POST /api/v1/users - Create user")
fmt.Fprintln(w, " GET /api/v1/users/{id} - Get user")
fmt.Fprintln(w, " PUT /api/v1/users/{id} - Full update")
fmt.Fprintln(w, " PATCH /api/v1/users/{id} - Partial update")
fmt.Fprintln(w, " DELETE /api/v1/users/{id} - Delete user")
})
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
示例 2:分页查询参数处理
go
package main
import (
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"strings"
)
// Pagination 分页参数
type Pagination struct {
Page int // 当前页码(从1开始)
Size int // 每页条数
Sort string // 排序字段
Order string // asc 或 desc
}
// ParsePagination 从 URL Query 中解析分页参数
func ParsePagination(values url.Values) Pagination {
p := Pagination{Page: 1, Size: 10, Order: "asc"}
if v := values.Get("page"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
p.Page = n
}
}
if v := values.Get("size"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 100 {
p.Size = n
}
}
if v := values.Get("sort"); v != "" {
p.Sort = v
}
if v := values.Get("order"); v != "" {
if v == "desc" || v == "asc" {
p.Order = v
}
}
return p
}
// Filter 过滤参数
type Filter struct {
Status string // active, inactive
MinAge int
MaxAge int
Query string // 模糊搜索
}
// ParseFilter 从 URL Query 中解析过滤参数
func ParseFilter(values url.Values) Filter {
f := Filter{}
f.Status = values.Get("status")
if v := values.Get("min_age"); v != "" {
f.MinAge, _ = strconv.Atoi(v)
}
if v := values.Get("max_age"); v != "" {
f.MaxAge, _ = strconv.Atoi(v)
}
f.Query = values.Get("q")
return f
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析分页和过滤参数
page := ParsePagination(r.URL.Query())
filter := ParseFilter(r.URL.Query())
// 模拟数据(实际应从数据库查询)
allUsers := []map[string]interface{}{
{"id": 1, "name": "Alice", "age": 25, "status": "active"},
{"id": 2, "name": "Bob", "age": 30, "status": "inactive"},
{"id": 3, "name": "Charlie", "age": 22, "status": "active"},
{"id": 4, "name": "Diana", "age": 28, "status": "active"},
{"id": 5, "name": "Eve", "age": 35, "status": "inactive"},
}
// 过滤
filtered := make([]map[string]interface{}, 0)
for _, u := range allUsers {
if filter.Status != "" && u["status"] != filter.Status {
continue
}
if filter.MinAge > 0 && u["age"].(int) < filter.MinAge {
continue
}
if filter.MaxAge > 0 && u["age"].(int) > filter.MaxAge {
continue
}
if filter.Query != "" && !strings.Contains(strings.ToLower(u["name"].(string)), strings.ToLower(filter.Query)) {
continue
}
filtered = append(filtered, u)
}
// 分页
total := len(filtered)
start := (page.Page - 1) * page.Size
end := start + page.Size
if start > total {
start = total
}
if end > total {
end = total
}
items := filtered[start:end]
fmt.Fprintf(w, "Pagination: page=%d, size=%d, sort=%s, order=%s\n",
page.Page, page.Size, page.Sort, page.Order)
fmt.Fprintf(w, "Filter: status=%s, min_age=%d, max_age=%d, q=%s\n",
filter.Status, filter.MinAge, filter.MaxAge, filter.Query)
fmt.Fprintf(w, "Total: %d, Returned: %d items\n", total, len(items))
for _, u := range items {
fmt.Fprintf(w, " %+v\n", u)
}
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Try these URLs:")
fmt.Fprintln(w, " /api/v1/users?page=1&size=2")
fmt.Fprintln(w, " /api/v1/users?status=active")
fmt.Fprintln(w, " /api/v1/users?min_age=25&max_age=30")
fmt.Fprintln(w, " /api/v1/users?q=al")
fmt.Fprintln(w, " /api/v1/users?sort=age&order=desc")
})
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}