第2个项目 - RESTful API Todo服务
嘿,朋友们!我是长安。
今天我们要做的第二个实战项目是RESTful API Todo服务。这是一个Web后端API,实现了经典的Todo(待办事项)管理功能。
说实话,掌握RESTful API开发是后端工程师的必备技能。几乎所有的Web应用、移动App都需要后端API。我当年找工作的时候,面试官几乎都会问到RESTful API的设计。通过这个项目,你将学会如何用Go构建专业的HTTP服务!
🎯 项目目标
实现一个Todo服务API,支持以下功能:
| HTTP方法 | 路径 | 功能 | 示例 |
|---|---|---|---|
GET | /todos | 获取所有待办事项 | 返回JSON列表 |
GET | /todos/:id | 获取单个待办事项 | 返回单个Todo |
POST | /todos | 创建待办事项 | 提交JSON数据 |
PUT | /todos/:id | 更新待办事项 | 提交JSON数据 |
DELETE | /todos/:id | 删除待办事项 | 返回成功信息 |
📁 项目结构
rest-api-todo/
├── main.go # 程序入口
├── models/
│ └── todo.go # Todo数据模型
├── handlers/
│ └── todo.go # HTTP处理函数
├── storage/
│ └── memory.go # 内存存储(后续可改为数据库)
├── middleware/
│ └── logger.go # 日志中间件
└── go.mod
🚀 第一步:项目初始化
# 创建项目目录
mkdir rest-api-todo
cd rest-api-todo
# 初始化Go模块
go mod init github.com/yourusername/rest-api-todo
# 安装依赖(使用 Gin 框架)
go get -u github.com/gin-gonic/gin
# 创建目录结构
mkdir models handlers storage middleware
📦 第二步:定义数据模型
文件: models/todo.go
package models
import "time"
// Todo 待办事项结构体
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateTodoRequest 创建Todo的请求结构
type CreateTodoRequest struct {
Title string `json:"title" binding:"required,min=1,max=100"`
Description string `json:"description" binding:"max=500"`
}
// UpdateTodoRequest 更新Todo的请求结构
type UpdateTodoRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,max=100"`
Description *string `json:"description" binding:"omitempty,max=500"`
Completed *bool `json:"completed"`
}
💾 第三步:实现存储层
文件: storage/memory.go
package storage
import (
"errors"
"sync"
"time"
"github.com/yourusername/rest-api-todo/models"
)
var (
ErrNotFound = errors.New("todo not found")
)
// MemoryStorage 内存存储(线程安全)
type MemoryStorage struct {
todos map[int]*models.Todo
nextID int
mu sync.RWMutex
}
// NewMemoryStorage 创建新的内存存储
func NewMemoryStorage() *MemoryStorage {
return &MemoryStorage{
todos: make(map[int]*models.Todo),
nextID: 1,
}
}
// GetAll 获取所有待办事项
func (s *MemoryStorage) GetAll() []*models.Todo {
s.mu.RLock()
defer s.mu.RUnlock()
todos := make([]*models.Todo, 0, len(s.todos))
for _, todo := range s.todos {
todos = append(todos, todo)
}
return todos
}
// GetByID 根据ID获取待办事项
func (s *MemoryStorage) GetByID(id int) (*models.Todo, error) {
s.mu.RLock()
defer s.mu.RUnlock()
todo, exists := s.todos[id]
if !exists {
return nil, ErrNotFound
}
return todo, nil
}
// Create 创建新的待办事项
func (s *MemoryStorage) Create(title, description string) *models.Todo {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
todo := &models.Todo{
ID: s.nextID,
Title: title,
Description: description,
Completed: false,
CreatedAt: now,
UpdatedAt: now,
}
s.todos[s.nextID] = todo
s.nextID++
return todo
}
// Update 更新待办事项
func (s *MemoryStorage) Update(id int, title, description *string, completed *bool) (*models.Todo, error) {
s.mu.Lock()
defer s.mu.Unlock()
todo, exists := s.todos[id]
if !exists {
return nil, ErrNotFound
}
// 更新字段
if title != nil {
todo.Title = *title
}
if description != nil {
todo.Description = *description
}
if completed != nil {
todo.Completed = *completed
}
todo.UpdatedAt = time.Now()
return todo, nil
}
// Delete 删除待办事项
func (s *MemoryStorage) Delete(id int) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.todos[id]; !exists {
return ErrNotFound
}
delete(s.todos, id)
return nil
}
🎮 第四步:实现HTTP处理器
文件: handlers/todo.go
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yourusername/rest-api-todo/models"
"github.com/yourusername/rest-api-todo/storage"
)
// TodoHandler 处理Todo相关的HTTP请求
type TodoHandler struct {
storage *storage.MemoryStorage
}
// NewTodoHandler 创建新的TodoHandler
func NewTodoHandler(storage *storage.MemoryStorage) *TodoHandler {
return &TodoHandler{storage: storage}
}
// GetAll 获取所有待办事项
// GET /todos
func (h *TodoHandler) GetAll(c *gin.Context) {
todos := h.storage.GetAll()
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": todos,
"count": len(todos),
})
}
// GetByID 获取单个待办事项
// GET /todos/:id
func (h *TodoHandler) GetByID(c *gin.Context) {
// 解析ID参数
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "无效的ID",
})
return
}
// 查询数据
todo, err := h.storage.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "待办事项不存在",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": todo,
})
}
// Create 创建待办事项
// POST /todos
func (h *TodoHandler) Create(c *gin.Context) {
var req models.CreateTodoRequest
// 解析并验证请求body
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "请求参数错误: " + err.Error(),
})
return
}
// 创建Todo
todo := h.storage.Create(req.Title, req.Description)
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "创建成功",
"data": todo,
})
}
// Update 更新待办事项
// PUT /todos/:id
func (h *TodoHandler) Update(c *gin.Context) {
// 解析ID参数
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "无效的ID",
})
return
}
var req models.UpdateTodoRequest
// 解析并验证请求body
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "请求参数错误: " + err.Error(),
})
return
}
// 更新Todo
todo, err := h.storage.Update(id, req.Title, req.Description, req.Completed)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "待办事项不存在",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "更新成功",
"data": todo,
})
}
// Delete 删除待办事项
// DELETE /todos/:id
func (h *TodoHandler) Delete(c *gin.Context) {
// 解析ID参数
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "无效的ID",
})
return
}
// 删除Todo
err = h.storage.Delete(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "待办事项不存在",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "删除成功",
})
}
📝 第五步:实现日志中间件
文件: middleware/logger.go
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
// Logger 日志中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// 记录开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 计算耗时
duration := time.Since(startTime)
// 打印日志
log.Printf(
"[%s] %s %s %d %s",
c.Request.Method,
c.Request.RequestURI,
c.ClientIP(),
c.Writer.Status(),
duration,
)
}
}
🌐 第六步:实现主程序
文件: main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/yourusername/rest-api-todo/handlers"
"github.com/yourusername/rest-api-todo/middleware"
"github.com/yourusername/rest-api-todo/storage"
)
func main() {
// 创建存储
store := storage.NewMemoryStorage()
// 创建处理器
todoHandler := handlers.NewTodoHandler(store)
// 创建Gin引擎
router := gin.Default()
// 添加中间件
router.Use(middleware.Logger())
// 添加CORS中间件(允许跨域)
router.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
// 注册路由
api := router.Group("/api/v1")
{
todos := api.Group("/todos")
{
todos.GET("", todoHandler.GetAll)
todos.GET("/:id", todoHandler.GetByID)
todos.POST("", todoHandler.Create)
todos.PUT("/:id", todoHandler.Update)
todos.DELETE("/:id", todoHandler.Delete)
}
}
// 添加健康检查接口
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"message": "Todo API is running",
})
})
// 启动服务器
log.Println("🚀 服务器启动在 http://localhost:8080")
if err := router.Run(":8080"); err != nil {
log.Fatalf("启动服务器失败: %v", err)
}
}
🧪 第七步:测试API
1. 启动服务
# 运行程序
go run main.go
# 输出:
# 🚀 服务器启动在 http://localhost:8080
2. 测试API(使用curl)
# 1. 健康检查
curl http://localhost:8080/health
# 2. 创建Todo
curl -X POST http://localhost:8080/api/v1/todos \
-H "Content-Type: application/json" \
-d '{"title":"学习Go语言","description":"完成实战项目"}'
# 3. 获取所有Todo
curl http://localhost:8080/api/v1/todos
# 4. 获取单个Todo
curl http://localhost:8080/api/v1/todos/1
# 5. 更新Todo
curl -X PUT http://localhost:8080/api/v1/todos/1 \
-H "Content-Type: application/json" \
-d '{"completed":true}'
# 6. 删除Todo
curl -X DELETE http://localhost:8080/api/v1/todos/1
3. 使用Postman测试
推荐使用Postman或VSCode的REST Client插件进行测试。
创建文件: test.http
### 健康检查
GET http://localhost:8080/health
### 获取所有Todo
GET http://localhost:8080/api/v1/todos
### 创建Todo
POST http://localhost:8080/api/v1/todos
Content-Type: application/json
{
"title": "学习Go语言",
"description": "完成RESTful API项目"
}
### 获取单个Todo
GET http://localhost:8080/api/v1/todos/1
### 更新Todo
PUT http://localhost:8080/api/v1/todos/1
Content-Type: application/json
{
"title": "学习Go语言",
"completed": true
}
### 删除Todo
DELETE http://localhost:8080/api/v1/todos/1
📊 响应示例
创建成功
{
"success": true,
"message": "创建成功",
"data": {
"id": 1,
"title": "学习Go语言",
"description": "完成实战项目",
"completed": false,
"created_at": "2025-12-27T10:30:00Z",
"updated_at": "2025-12-27T10:30:00Z"
}
}
获取列表
{
"success": true,
"data": [
{
"id": 1,
"title": "学习Go语言",
"description": "完成实战项目",
"completed": false,
"created_at": "2025-12-27T10:30:00Z",
"updated_at": "2025-12-27T10:30:00Z"
}
],
"count": 1
}
💡 扩展思考
完成基础功能后,你可以尝试以下扩展:
1. 数据持久化
将内存存储改为数据库存储(MySQL/PostgreSQL):
# 安装GORM
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
2. 添加更多功能
- 用户认证(JWT Token)
- 分页和排序
- 搜索和筛选
- 标签管理
- 优先级设置
3. 改进API设计
- 统一的错误码
- 统一的响应格式
- API版本管理
- 请求限流
4. 添加单元测试
文件: handlers/todo_test.go
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/yourusername/rest-api-todo/storage"
)
func TestCreateTodo(t *testing.T) {
// 设置测试环境
gin.SetMode(gin.TestMode)
store := storage.NewMemoryStorage()
handler := NewTodoHandler(store)
router := gin.New()
router.POST("/todos", handler.Create)
// 准备测试数据
reqBody := map[string]string{
"title": "测试Todo",
"description": "这是一个测试",
}
jsonData, _ := json.Marshal(reqBody)
// 发送请求
req, _ := http.NewRequest("POST", "/todos", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// 验证结果
assert.Equal(t, 201, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.True(t, response["success"].(bool))
}
💪 练习题
- 添加
GET /todos?completed=true接口,支持按完成状态筛选 - 实现分页功能:
GET /todos?page=1&limit=10 - 添加参数验证,标题不能为空且长度限制在100个字符
- 实现批量删除功能:
DELETE /todos接受ID数组
💡 参考答案
// 筛选功能示例
func (h *TodoHandler) GetAll(c *gin.Context) {
completed := c.Query("completed")
todos := h.storage.GetAll()
// 如果指定了completed参数,进行筛选
if completed != "" {
isCompleted := completed == "true"
filtered := make([]*models.Todo, 0)
for _, todo := range todos {
if todo.Completed == isCompleted {
filtered = append(filtered, todo)
}
}
todos = filtered
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": todos,
"count": len(todos),
})
}
🎯 小结
恭喜你完成了第二个实战项目!通过这个项目,你学会了:
✅ 使用Gin框架构建HTTP服务
✅ RESTful API的设计原则
✅ JSON数据的序列化和反序列化
✅ HTTP请求的处理和响应
✅ 中间件的使用
✅ 线程安全的数据存储
下一个项目我们将实现Web爬虫,学习HTTP请求和HTML解析!
下一步: 第3个项目 - Web爬虫 新闻采集器 →
💬 遇到问题了吗?
- 确保端口8080没有被占用
- 检查JSON格式是否正确
- 查看服务器日志定位错误
- 访问 Gin文档 了解更多
