Go语言快速入门Go语言快速入门
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
  • 实战篇

    • 🚀 实战篇
    • 第1个项目 - 命令行文件管理器
    • 第2个项目 - RESTful API Todo服务
    • 第3个项目 - Web爬虫 新闻采集器
    • 第4个项目 - 实时聊天室 WebSocket
    • 第5个项目 - URL短链接服务
    • 第6个项目 - 完整博客系统

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

💪 练习题

  1. 添加 GET /todos?completed=true 接口,支持按完成状态筛选
  2. 实现分页功能:GET /todos?page=1&limit=10
  3. 添加参数验证,标题不能为空且长度限制在100个字符
  4. 实现批量删除功能: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文档 了解更多
最近更新: 2025/12/27 13:26
Contributors: 王长安
Prev
第1个项目 - 命令行文件管理器
Next
第3个项目 - Web爬虫 新闻采集器