Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang RESTful API设计原则

Golang RESTful API设计原则用法及说明

作者:FfHUCisI

还在为RESTful API设计犯愁?本文系统总结REST架构核心原则,详解GET、POST、PUT、DELETE等HTTP方法语义与幂等性,教你规范资源命名和状态码使用,并对比API版本控制、分页过滤排序的多种实现方案,助你快速设计出清晰、可维护的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删除资源删除实体

1.3 URL 资源命名规范

好的资源命名让 API 自描述:

✅ 推荐做法:

❌ 避免做法:

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 请求/响应体设计

请求体:

响应体结构建议:

{
  "code": 0,
  "message": "success",
  "data": { ... }
}

或者遵循 HTTP 规范,用状态码表达错误,错误详情放响应体:

// 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 冗长
HeaderAccept: application/vnd.api.v1+jsonURL 干净不够直观
Query/api/users?version=1灵活不符合 REST 理念

推荐:URL Path 版本化,简单直观,便于调试和缓存。

1.7 分页、过滤与排序

分页:

过滤:

排序:

二、练习代码

示例 1:规范化的 RESTful 用户 API

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:分页查询参数处理

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))
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文