404 Not Found

404 Not Found


nginx

REST API开发

REST API开发

生活类比

想象你经营一家餐厅。顾客(客户端)来到前台,服务员(路由器)根据顾客的需求把他们引导到不同的窗口:

厨房(服务器)处理完请求后,把做好的菜(响应)通过服务员递给顾客。而保安(中间件)会在顾客进入餐厅前检查身份、记录来访时间,遇到突发情况还能紧急处理。

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} 删除指定待办

附加要求:

系统设计

客户端请求
    │
    ▼
┌──────────────────────────────┐
│         中间件链              │
│  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 │
└──────────────────────────────┘

完整代码

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

运行与测试

启动服务前,先初始化模块并安装依赖:

BASH
go mod init todo-api
go get github.com/google/uuid
go run main.go

使用 curl 测试各个接口:

BASH
# 创建待办事项
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

预期响应示例:

TEXT
# 创建成功
{"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 标签控制序列化字段名。TodoStoresync.RWMutex 保护 map,读操作用读锁(RLock),写操作用写锁(Lock),保证并发安全。

2. 中间件模式

每个中间件都是 func(http.Handler) http.Handler 类型,通过装饰器模式包裹下一个处理器:

TEXT
RecoveryMiddleware(
  AuthMiddleware(
    LoggingMiddleware(
      最终处理器,
    ),
  ),
)

Chain 函数将多个中间件按参数顺序组合成一条链。

3. 自定义路由器

由于标准库的 http.ServeMux 在 Go 1.22 之前不支持路径参数,我们实现了一个简易路由器:

4. 处理器闭包

handleGetTodos(store) 这类函数返回一个闭包,捕获了 store 引用。这样处理器可以访问共享的存储层,而无需全局变量。

5. 统一响应格式

所有接口返回相同的 JSON 结构 {success, data, error},客户端只需解析一次即可判断请求是否成功。

❓ 常见问题

Q1:为什么用 sync.RWMutex 而不是 sync.Mutex

Mutex 在任意时刻只允许一个 goroutine 访问,而 RWMutex 允许多个读操作并发执行,只有写操作会独占锁。对于"读多写少"的场景(如 API 查询远多于修改),RWMutex 能显著提升并发性能。

Q2:生产环境应该用这个路由器吗?

不建议。这个路由器是教学用途的简化实现,不支持正则匹配、通配符、方法自动检测等高级功能。生产环境推荐使用成熟的路由库,如 chigorilla/mux 或 Go 1.22+ 内置的增强路由。

Q3:内存存储的数据在重启后会丢失,怎么解决?

本课程使用内存存储是为了聚焦 REST API 部分。在实际项目中,你应该将数据持久化到数据库(如 SQLite、PostgreSQL、MongoDB)。下一课我们将学习如何用 Go 操作数据库。

Q4:中间件的执行顺序有什么讲究?

有讲究。Recovery 放在最外层,确保能捕获所有内层的 panic;Auth 放在 Logging 之前,这样未认证的请求不会产生业务日志(但你也可以反过来,记录所有请求包括未认证的)。根据业务需求灵活调整即可。

📖 小节

本节课我们综合运用了前面学到的多个知识点,从零搭建了一个完整的 REST API:

这些模式是 Go Web 开发的基石,掌握它们后,你可以轻松扩展到数据库集成、JWT 认证、API 限流等更复杂的场景。

📝 作业

作业 1:添加分页支持

修改 GET /api/todos 接口,支持查询参数 ?page=1&size=10 实现分页返回。提示:用 r.URL.Query() 读取参数,对 GetAll 的结果做切片。

作业 2:添加 CORS 中间件

编写 CORSMiddleware,在响应头中添加:

处理 OPTIONS 预检请求,直接返回 204。将它加入中间件链。

作业 3:实现搜索功能

添加新路由 GET /api/todos/search?q=关键词,在内存存储中按标题模糊匹配(用 strings.Contains),返回匹配的待办列表。


下一课:数据库操作

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏