REST API开发
REST API开发
生活类比
想象你经营一家餐厅。顾客(客户端)来到前台,服务员(路由器)根据顾客的需求把他们引导到不同的窗口:
- 看菜单 → 前台查询窗口(GET)
- 点菜 → 下单窗口(POST)
- 改菜 → 修改订单窗口(PUT)
- 退菜 → 取消订单窗口(DELETE)
厨房(服务器)处理完请求后,把做好的菜(响应)通过服务员递给顾客。而保安(中间件)会在顾客进入餐厅前检查身份、记录来访时间,遇到突发情况还能紧急处理。
REST API 就是这样一套标准化的服务流程——客户端用统一的"动词"(HTTP方法)和"地址"(URL)与服务器沟通,服务器返回结构化的数据(JSON)。
项目需求
我们要开发一个待办事项(Todo)REST API,支持以下功能:
| 操作 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 获取所有待办 | GET |
/api/todos |
返回待办列表 |
| 获取单个待办 | GET |
/api/todos/{id} |
根据ID返回详情 |
| 创建待办 | POST |
/api/todos |
新增一条待办 |
| 更新待办 | PUT |
/api/todos/{id} |
修改指定待办 |
| 删除待办 | DELETE |
/api/todos/{id} |
删除指定待办 |
附加要求:
- 使用内存存储(map + 互斥锁)
- 实现中间件链:日志记录、身份认证、panic恢复
- 请求和响应均使用 JSON 格式
系统设计
客户端请求
│
▼
┌──────────────────────────────┐
│ 中间件链 │
│ Recovery → Auth → Logging │
│ ↓ ↓ ↓ │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ 路由器 │
│ GET /api/todos │
│ GET /api/todos/{id} │
│ POST /api/todos │
│ PUT /api/todos/{id} │
│ DELETE /api/todos/{id} │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ 内存存储层 │
│ map[string]Todo + sync.RWMutex │
└──────────────────────────────┘
完整代码
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
// Todo 待办事项结构体
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// TodoStore 内存存储,带读写锁保证并发安全
type TodoStore struct {
mu sync.RWMutex
todos map[string]Todo
}
// NewTodoStore 创建一个新的存储实例
func NewTodoStore() *TodoStore {
return &TodoStore{
todos: make(map[string]Todo),
}
}
// GetAll 获取所有待办事项
func (s *TodoStore) GetAll() []Todo {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]Todo, 0, len(s.todos))
for _, t := range s.todos {
list = append(list, t)
}
return list
}
// GetByID 根据ID获取单个待办事项
func (s *TodoStore) GetByID(id string) (Todo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
t, ok := s.todos[id]
return t, ok
}
// Create 创建新的待办事项
func (s *TodoStore) Create(title string) Todo {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().Format(time.RFC3339)
t := Todo{
ID: uuid.New().String(),
Title: title,
Completed: false,
CreatedAt: now,
UpdatedAt: now,
}
s.todos[t.ID] = t
return t
}
// Update 更新指定的待办事项
func (s *TodoStore) Update(id string, title string, completed bool) (Todo, bool) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.todos[id]
if !ok {
return Todo{}, false
}
t.Title = title
t.Completed = completed
t.UpdatedAt = time.Now().Format(time.RFC3339)
s.todos[id] = t
return t, true
}
// Delete 删除指定的待办事项
func (s *TodoStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.todos[id]; !ok {
return false
}
delete(s.todos, id)
return true
}
// --- 通用响应结构 ---
// APIResponse 统一的API响应格式
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// jsonResponse 发送JSON响应
func jsonResponse(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// --- 中间件 ---
// Middleware 中间件类型定义
type Middleware func(http.Handler) http.Handler
// RecoveryMiddleware panic恢复中间件
// 捕获处理函数中的panic,返回500错误而非让进程崩溃
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
jsonResponse(w, http.StatusInternalServerError, APIResponse{
Success: false,
Error: "服务器内部错误",
})
}
}()
next.ServeHTTP(w, r)
})
}
// LoggingMiddleware 请求日志中间件
// 记录每个请求的方法、路径和耗时
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("[LOG] %s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
// AuthMiddleware 简单的身份认证中间件
// 检查请求头中是否携带有效的 API Key
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
if apiKey != "secret-key-123" {
jsonResponse(w, http.StatusUnauthorized, APIResponse{
Success: false,
Error: "无效的API密钥",
})
return
}
next.ServeHTTP(w, r)
})
}
// Chain 将多个中间件串联成链
// 参数顺序即为执行顺序:Chain(A, B, C) → A(B(C(handler)))
func Chain(middlewares ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
next = middlewares[i](next)
}
return next
}
}
// --- 路由与处理器 ---
// Router 自定义路由器
type Router struct {
routes map[string]map[string]http.HandlerFunc // method -> path -> handler
middleware Middleware
}
// NewRouter 创建路由器
func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string]http.HandlerFunc),
}
}
// Use 注册全局中间件
func (rt *Router) Use(m Middleware) {
rt.middleware = m
}
// Handle 注册路由:方法 + 路径 + 处理函数
func (rt *Router) Handle(method, path string, handler http.HandlerFunc) {
if _, ok := rt.routes[method]; !ok {
rt.routes[method] = make(map[string]http.HandlerFunc)
}
rt.routes[method][path] = handler
}
// ServeHTTP 实现 http.Handler 接口,完成路由匹配
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
methodRoutes, ok := rt.routes[r.Method]
if !ok {
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{
Success: false,
Error: "不支持的请求方法",
})
return
}
// 精确匹配
if handler, ok := methodRoutes[r.URL.Path]; ok {
handler(w, r)
return
}
// 前缀匹配:尝试提取路径参数(如 /api/todos/{id})
for pattern, handler := range methodRoutes {
if strings.HasSuffix(pattern, "/{id}") {
prefix := strings.TrimSuffix(pattern, "/{id}")
if strings.HasPrefix(r.URL.Path, prefix+"/") {
// 将 {id} 放入请求头供处理器读取
id := strings.TrimPrefix(r.URL.Path, prefix+"/")
r.Header.Set("X-Resource-ID", id)
handler(w, r)
return
}
}
}
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "资源不存在",
})
}
// --- 处理器函数 ---
// handleGetTodos 获取所有待办事项
func handleGetTodos(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
todos := store.GetAll()
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todos,
})
}
}
// handleGetTodo 获取单个待办事项
func handleGetTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
todo, ok := store.GetByID(id)
if !ok {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "待办事项不存在",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todo,
})
}
}
// CreateTodoRequest 创建待办的请求体
type CreateTodoRequest struct {
Title string `json:"title"`
}
// handleCreateTodo 创建待办事项
func handleCreateTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req CreateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "无效的请求体",
})
return
}
if strings.TrimSpace(req.Title) == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "标题不能为空",
})
return
}
todo := store.Create(req.Title)
jsonResponse(w, http.StatusCreated, APIResponse{
Success: true,
Data: todo,
})
}
}
// UpdateTodoRequest 更新待办的请求体
type UpdateTodoRequest struct {
Title string `json:"title"`
Completed bool `json:"completed"`
}
// handleUpdateTodo 更新待办事项
func handleUpdateTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
var req UpdateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "无效的请求体",
})
return
}
if strings.TrimSpace(req.Title) == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "标题不能为空",
})
return
}
todo, ok := store.Update(id, req.Title, req.Completed)
if !ok {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "待办事项不存在",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todo,
})
}
}
// handleDeleteTodo 删除待办事项
func handleDeleteTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
if !store.Delete(id) {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "待办事项不存在",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: "删除成功",
})
}
}
func main() {
// 初始化存储
store := NewTodoStore()
// 创建路由器
router := NewRouter()
// 注册中间件链:Recovery → Auth → Logging
router.Use(Chain(RecoveryMiddleware, AuthMiddleware, LoggingMiddleware))
// 注册路由
router.Handle("GET", "/api/todos", handleGetTodos(store))
router.Handle("GET", "/api/todos/{id}", handleGetTodo(store))
router.Handle("POST", "/api/todos", handleCreateTodo(store))
router.Handle("PUT", "/api/todos/{id}", handleUpdateTodo(store))
router.Handle("DELETE", "/api/todos/{id}", handleDeleteTodo(store))
// 应用中间件并启动服务
addr := ":8080"
log.Printf("REST API 服务启动在 http://localhost%s", addr)
log.Fatal(http.ListenAndServe(addr, router.middleware(router)))
}
运行与测试
启动服务前,先初始化模块并安装依赖:
go mod init todo-api
go get github.com/google/uuid
go run main.go
使用 curl 测试各个接口:
# 创建待办事项
curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-H "X-API-Key: secret-key-123" \
-d '{"title": "学习Go语言"}'
# 获取所有待办
curl http://localhost:8080/api/todos \
-H "X-API-Key: secret-key-123"
# 获取单个待办(将 {id} 替换为实际返回的ID)
curl http://localhost:8080/api/todos/{id} \
-H "X-API-Key: secret-key-123"
# 更新待办
curl -X PUT http://localhost:8080/api/todos/{id} \
-H "Content-Type: application/json" \
-H "X-API-Key: secret-key-123" \
-d '{"title": "学习Go语言(已完成)", "completed": true}'
# 删除待办
curl -X DELETE http://localhost:8080/api/todos/{id} \
-H "X-API-Key: secret-key-123"
# 测试未认证请求(应返回401)
curl http://localhost:8080/api/todos
预期响应示例:
# 创建成功
{"success":true,"data":{"id":"a1b2c3d4-...","title":"学习Go语言","completed":false,"created_at":"2026-06-27T10:00:00+08:00","updated_at":"2026-06-27T10:00:00+08:00"}}
# 未认证
{"success":false,"error":"无效的API密钥"}
代码解析
1. 数据模型与存储层
Todo 结构体定义了待办事项的数据结构,使用 JSON 标签控制序列化字段名。TodoStore 用 sync.RWMutex 保护 map,读操作用读锁(RLock),写操作用写锁(Lock),保证并发安全。
2. 中间件模式
每个中间件都是 func(http.Handler) http.Handler 类型,通过装饰器模式包裹下一个处理器:
RecoveryMiddleware(
AuthMiddleware(
LoggingMiddleware(
最终处理器,
),
),
)
- RecoveryMiddleware:用
defer + recover捕获 panic - AuthMiddleware:检查请求头中的 API Key
- LoggingMiddleware:记录请求方法、路径、耗时
Chain 函数将多个中间件按参数顺序组合成一条链。
3. 自定义路由器
由于标准库的 http.ServeMux 在 Go 1.22 之前不支持路径参数,我们实现了一个简易路由器:
- 用嵌套
map[method][path]存储路由表 - 精确匹配优先,失败后尝试前缀匹配提取
{id} - 路径参数通过
X-Resource-ID请求头传递给处理器
4. 处理器闭包
handleGetTodos(store) 这类函数返回一个闭包,捕获了 store 引用。这样处理器可以访问共享的存储层,而无需全局变量。
5. 统一响应格式
所有接口返回相同的 JSON 结构 {success, data, error},客户端只需解析一次即可判断请求是否成功。
❓ 常见问题
Q1:为什么用 sync.RWMutex 而不是 sync.Mutex?
Mutex 在任意时刻只允许一个 goroutine 访问,而 RWMutex 允许多个读操作并发执行,只有写操作会独占锁。对于"读多写少"的场景(如 API 查询远多于修改),RWMutex 能显著提升并发性能。
Q2:生产环境应该用这个路由器吗?
不建议。这个路由器是教学用途的简化实现,不支持正则匹配、通配符、方法自动检测等高级功能。生产环境推荐使用成熟的路由库,如 chi、gorilla/mux 或 Go 1.22+ 内置的增强路由。
Q3:内存存储的数据在重启后会丢失,怎么解决?
本课程使用内存存储是为了聚焦 REST API 部分。在实际项目中,你应该将数据持久化到数据库(如 SQLite、PostgreSQL、MongoDB)。下一课我们将学习如何用 Go 操作数据库。
Q4:中间件的执行顺序有什么讲究?
有讲究。Recovery 放在最外层,确保能捕获所有内层的 panic;Auth 放在 Logging 之前,这样未认证的请求不会产生业务日志(但你也可以反过来,记录所有请求包括未认证的)。根据业务需求灵活调整即可。
📖 小节
本节课我们综合运用了前面学到的多个知识点,从零搭建了一个完整的 REST API:
- 用
net/http实现 HTTP 服务器和自定义路由器 - 用
encoding/json处理 JSON 序列化与反序列化 - 用
sync.RWMutex实现并发安全的内存存储 - 用中间件链模式实现横切关注点(日志、认证、恢复)
- 用闭包实现依赖注入,让处理器访问共享资源
- 遵循 RESTful 规范设计 API 路径和 HTTP 方法
这些模式是 Go Web 开发的基石,掌握它们后,你可以轻松扩展到数据库集成、JWT 认证、API 限流等更复杂的场景。
📝 作业
作业 1:添加分页支持
修改 GET /api/todos 接口,支持查询参数 ?page=1&size=10 实现分页返回。提示:用 r.URL.Query() 读取参数,对 GetAll 的结果做切片。
作业 2:添加 CORS 中间件
编写 CORSMiddleware,在响应头中添加:
Access-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, X-API-Key
处理 OPTIONS 预检请求,直接返回 204。将它加入中间件链。
作业 3:实现搜索功能
添加新路由 GET /api/todos/search?q=关键词,在内存存储中按标题模糊匹配(用 strings.Contains),返回匹配的待办列表。
下一课:数据库操作



