Golang RESTful API 设计原则

RESTful API 设计原则

一、知识点总结

1.1 REST 的本质

REST(Representational State Transfer,表述性状态转移)是由 Roy Fielding 在其 2000 年博士论文中提出的架构风格。它不是协议、不是标准,而是一组设计约束和原则

  1. 资源(Resource)为核心:一切抽象为资源,用 URL 标识
  2. 统一接口:使用标准的 HTTP 方法操作资源
  3. 无状态(Stateless):每个请求自包含,服务端不保存客户端状态
  4. 可缓存:响应应显式标注是否可缓存
  5. 分层系统:客户端不需要知道是否直连服务端还是通过代理/网关

理解 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=admin
  • GET /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))
}

相关推荐
breeze jiang1 小时前
ESLint flat config 配置实战:五大字段、规则严重级别与 --fix 能力边界详解
开发语言·前端·javascript
weixin_307779131 小时前
PHP 大文件上传:内存占用、超时与最佳实践
linux·服务器·开发语言·nginx·php
m0_527034331 小时前
异步任务审核系统设计:消息队列、超时重试与失败补偿
java·大数据·开发语言
一朵好运莲2 小时前
智能体使用 Chrome DevTools MCP 调试浏览器
开发语言·javascript·react.js
秋田君2 小时前
QT_JSON文件操作
开发语言·qt·json
老赵的博客2 小时前
可维护性 可扩展性 可复用性
开发语言·c++
sibylyue2 小时前
工作流表单和流程设计前端
开发语言·javascript·开源
一个游离的指针2 小时前
JS中的对象的相关概念
开发语言·javascript·原型模式
jaysee-sjc2 小时前
【JavaWeb】Tlias智能学习辅助系统|后端Web实战(登录认证)
java·开发语言·前端·学习·mybatis