从零到一:使用 Go、Gin 和 GORM 构建一个 RESTful Todo API

前言

大家好!今天,我将带领大家从零开始,一步步构建一个功能完备的待办事项(Todo)后端服务。这个项目虽然简单,但涵盖了现代 Web 后端开发的核心技术和最佳实践。我们将使用 Go 语言作为开发语言,搭配高性能的 Gin Web 框架和强大的 GORM ORM 库,数据库则选用轻量级的 SQLite。

通过这个项目,你将学到:

  • 如何使用 Gin 搭建 RESTful API 服务。

  • 如何使用 GORM 定义数据模型并操作数据库。

  • 如何实现完整的 CRUD(创建、读取、更新、删除)业务逻辑。

  • 如何组织一个清晰、可维护的 Go 项目结构。

  • 如何处理跨域请求和 JSON 数据。

无论你是 Go 语言的新手,还是希望巩固 Web 开发技能的开发者,这篇文章都将为你提供一个绝佳的实战机会。

🛠 技术栈概览

在开始编码之前,让我们先了解一下项目所使用的核心技术:

  • 语言: Go 1.24.4

  • Web 框架: Gin (v1.11.0) - 一个用 Go 语言编写的高性能 HTTP Web 框架,以其极快的速度和简洁的 API 而闻名。

  • ORM: GORM (v1.31.0) - Go 语言领域功能最强大的 ORM 库之一,提供了对数据库的抽象,让我们可以用 Go 结构体来操作数据表。

  • 数据库: SQLite (v1.6.0) - 一个轻量级的、无服务器的、自包含的嵌入式 SQL 数据库引擎。

  • 跨域处理: gin-contrib/cors - Gin 官方提供的 CORS 中间件,可以轻松解决前端跨域请求问题。

📁 项目结构设计

一个良好的项目结构是项目成功的关键。我们采用了一个清晰、模块化的目录结构,将不同功能的代码分离开来,便于维护和扩展。

backend/
├── main.go              # 主程序入口, 路由配置和处理器定义
├── database/
│   ├── db.go           # 数据库初始化和连接管理
│   └── todo.go         # Todo 数据模型和数据库 CRUD 操作
├── go.mod              # Go 模块依赖管理
├── go.sum              # Go 模块依赖校验和
└── todo.db             # SQLite 数据库文件 (运行时自动生成)
  • main.go: 项目的启动文件,负责初始化数据库、设置 Gin 引擎、注册路由和启动 HTTP 服务。

  • database/ 目录:专门用于处理所有与数据库相关的逻辑。

    • db.go: 封装了数据库的连接、初始化以及 GORM 的配置。

    • todo.go: 定义了 Todo 结构体(即数据模型),并提供了所有针对 todos 表的增删改查函数。

👨‍💻 编码实战

接下来,让我们深入代码,看看每个部分是如何实现的。

第一步:定义数据模型和数据库操作 (database/todo.go)

我们首先定义 Todo 模型。这个模型对应数据库中的 todos 表。GORM 的 gorm.Model 会自动为我们添加 ID, CreatedAt, UpdatedAt, DeletedAt 四个字段,非常方便。

// 包 database 负责处理所有与数据库相关的操作
package database

import "gorm.io/gorm"

// Todo 定义了待办事项的数据模型
// gorm.Model 包含了 ID, CreatedAt, UpdatedAt, DeletedAt 四个字段
type Todo struct {
	gorm.Model
	Title     string `gorm:"not null" json:"title"`     // 待办事项的标题,不能为空
	Completed bool   `gorm:"not null;default:false" json:"completed"` // 完成状态,不能为空,默认为 false
}

// TableName 指定了 Todo 模型在数据库中对应的表名
// 返回 "todos"
func (todo Todo) TableName() string {
	return "todos"
}

// CreateTodo 用于向数据库中创建一个新的待-办事项
// 输入参数: todo (*Todo) - 指向要创建的 Todo 对象的指针
// 返回值: error - 如果创建过程中发生错误,则返回错误信息
func CreateTodo(todo *Todo) error {
	// 使用 GORM 的 Create 方法将 todo 对象插入数据库
	err := DB.Create(todo).Error
	if err != nil {
		// 如果发生错误,返回错误
		return err
	}
	// 如果没有错误,返回 nil
	return nil
}

// GetTodo 用于根据 ID 从数据库中获取单个待办事项
// 输入参数: id (int) - 要获取的 Todo 的 ID
// 返回值: (Todo, error) - 返回找到的 Todo 对象和可能的错误信息
func GetTodo(id int) (Todo, error) {
	// 声明一个 Todo 类型的变量用于存储查询结果
	var todo Todo
	// 使用 GORM 的 Where 方法构建查询条件,并用 First 方法获取第一条匹配的记录
	err := DB.Where("id=?", id).First(&todo).Error
	if err != nil {
		// 如果查询出错(例如,记录不存在),返回一个空的 Todo 对象和错误信息
		return Todo{}, err
	}
	// 如果找到记录,返回该记录和 nil
	return todo, nil
}

// GetTodos 用于从数据库中获取所有的待办事项列表
// 返回值: ([]Todo, error) - 返回 Todo 对象切片和可能的错误信息
func GetTodos() ([]Todo, error) {
	// 声明一个 Todo 切片用于存储所有记录
	var todos []Todo
	// 使用 GORM 的 Find 方法查询所有记录并存入 todos 切片
	err := DB.Find(&todos).Error
	if err != nil {
		// 如果查询出错,返回 nil 和错误信息
		return nil, err
	}
	// 返回查询到的所有记录和 nil
	return todos, nil
}

// UpdateTodo 用于更新数据库中已存在的待办事项
// 输入参数: todo (*Todo) - 指向包含更新后数据的 Todo 对象的指针
// 输入参数: id (int) - 要更新的 Todo 的 ID (虽然在当前实现中未使用,但保留以符合 RESTful 风格)
// 返回值: error - 如果更新过程中发生错误,则返回错误信息
func UpdateTodo(todo *Todo, id int) error {
	// 使用 GORM 的 Save 方法更新记录。Save 会更新所有字段,即使它们是零值。
	err := DB.Save(todo).Error
	if err != nil {
		// 如果更新出错,返回错误
		return err
	}
	// 如果没有错误,返回 nil
	return nil
}

// DeleteTodo 用于根据 ID 从数据库中删除一个待办事项
// 输入参数: id (int) - 要删除的 Todo 的 ID
// 返回值: error - 如果删除过程中发生错误,则返回错误信息
func DeleteTodo(id int) error {
	// 声明一个 Todo 变量
	var todo Todo
	// 首先,根据 ID 查找记录是否存在
	err := DB.Where("id=?", id).First(&todo).Error
	if err != nil {
		// 如果记录不存在或查找出错,返回错误
		return err
	}
	// 如果记录存在,则使用 GORM 的 Delete 方法删除它
	// 注意:这里传入 &Todo{} 和 id,GORM 会根据主键删除
	err = DB.Delete(&Todo{}, id).Error
	if err != nil {
		// 如果删除出错,返回错误
		return err
	}
	// 如果没有错误,返回 nil
	return nil
}

第二步:初始化数据库连接 (database/db.go)

这个文件负责连接数据库,并使用 GORM 的 AutoMigrate 功能自动创建(或更新)todos 表的结构。

// 包 database 负责处理所有与数据库相关的操作
package database

import (
	// 导入日志包,用于记录严重错误
	"log"

	// 导入 GORM 的 SQLite 驱动
	"gorm.io/driver/sqlite"
	// 导入 GORM 核心库
	"gorm.io/gorm"
)

// DB 是一个全局的数据库连接池指针,供其他包使用
var DB *gorm.DB

// Init 函数用于初始化数据库连接
func Init() {
	// 使用 gorm.Open 连接到 SQLite 数据库。 "todo.db" 是数据库文件名。
	db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{})
	// 如果连接过程中出现错误
	if err != nil {
		// 使用 log.Fatalf 打印错误信息并终止程序
		log.Fatalf("数据库连接失败: %v", err)
	}
	// 将成功创建的数据库连接赋值给全局变量 DB
	DB = db
	// 使用 AutoMigrate 自动迁移数据库结构
	// 它会根据 Todo 结构体的定义创建或更新 "todos" 表
	if err := DB.AutoMigrate(&Todo{}); err != nil {
		// 如果迁移过程中出现错误,打印错误信息并终止程序
		log.Fatalf("数据库迁移失败: %v", err)
	}
}

第三步:主程序入口与路由 (main.go)

这是我们服务的核心。它初始化数据库,创建 Gin 路由引擎,配置 CORS 中间件,并将每个 API 端点与相应的处理函数(Handler)绑定起来。

// 主包,程序的入口点
package main

import (
	// 导入自定义的 database 包,用于数据库操作
	"backend/database"
	// 导入 strconv 包,用于字符串和基本数据类型之间的转换
	"strconv"

	// 导入 Gin 的 CORS 中间件,用于处理跨域请求
	"github.com/gin-contrib/cors"
	// 导入 Gin Web 框架
	"github.com/gin-gonic/gin"
)

// main 函数是程序的入口
func main() {
	// 调用 database 包的 Init 函数来初始化数据库连接和进行数据迁移
	database.Init()
	// 创建一个默认的 Gin 引擎
	router := gin.Default()
	// 使用 cors.Default() 中间件,允许所有来源的跨域请求
	router.Use(cors.Default())
	// 定义 API 路由
	// GET /todo/:id - 获取单个待办事项
	router.GET("/todo/:id", HandlerGetID())
	// GET /todo - 获取所有待办事项
	router.GET("/todo", HandlerGet())
	// POST /todo - 创建一个新的待办事项
	router.POST("/todo", HandlerCreate())
	// PUT /todo/:id - 更新一个已存在的待办事项
	router.PUT("/todo/:id", HandlerUpdate())
	// DELETE /todo/:id - 删除一个待办事项
	router.DELETE("/todo/:id", HandlerDelete())
	// 启动 HTTP 服务,监听在 8080 端口
	router.Run(":8080")
}

// HandlerGetID 返回一个处理获取单个 Todo 请求的 Gin HandlerFunc
// 作用: 根据 URL 中的 ID 获取特定的 Todo
// 输入: gin.Context - Gin 的上下文对象,包含了请求和响应的信息
// 输出: 无
func HandlerGetID() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从 URL 参数中获取 ID,并处理可能发生的错误
		id, err := getId(c)
		if err != nil {
			// 如果 ID 无效,直接返回
			return
		}
		// 调用数据库函数获取 Todo
		todo, err := database.GetTodo(id)
		if err != nil {
			// 如果数据库查询出错(例如,找不到记录),返回 404 Not Found
			c.JSON(404, gin.H{"error": "Todo 不存在"})
			return
		}
		// 成功找到,返回 200 OK 和 Todo 数据
		c.JSON(200, todo)
	}
}

// HandlerGet 返回一个处理获取所有 Todo 列表请求的 Gin HandlerFunc
// 作用: 获取所有的 Todo 列表
// 输入: gin.Context
// 输出: 无
func HandlerGet() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 调用数据库函数获取所有 Todos
		todos, err := database.GetTodos()
		if err != nil {
			// 如果获取失败,返回 500 Internal Server Error
			c.JSON(500, gin.H{"error": "获取 Todo 列表失败"})
			return
		}
		// 成功获取,返回 200 OK 和 Todos 列表
		c.JSON(200, todos)
	}
}

// HandlerCreate 返回一个处理创建新 Todo 请求的 Gin HandlerFunc
// 作用: 从请求体中解析 JSON 数据并创建一个新的 Todo
// 输入: gin.Context
// 输出: 无
func HandlerCreate() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 声明一个 database.Todo 变量用于存储绑定的数据
		var todo database.Todo
		// 将请求体中的 JSON 数据绑定到 todo 变量上
		if err := c.BindJSON(&todo); err != nil {
			// 如果数据绑定失败(如 JSON 格式错误),返回 400 Bad Request
			c.JSON(400, gin.H{"error": "无效的请求数据: " + err.Error()})
			return
		}
		// 简单的输入验证:标题不能为空
		if todo.Title == "" {
			c.JSON(400, gin.H{"error": "标题不能为空"})
			return
		}
		// 调用数据库函数创建 Todo
		if err := database.CreateTodo(&todo); err != nil {
			// 如果创建失败,返回 500 Internal Server Error
			c.JSON(500, gin.H{"error": "创建 Todo 失败"})
			return
		}
		// 成功创建,返回 200 OK 和新创建的 Todo 对象(此时已包含 ID 和时间戳)
		c.JSON(200, todo)
	}
}

// HandlerUpdate 返回一个处理更新 Todo 请求的 Gin HandlerFunc
// 作用: 根据 ID 更新一个已存在的 Todo
// 输入: gin.Context
// 输出: 无
func HandlerUpdate() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从 URL 参数中获取 ID
		id, err := getId(c)
		if err != nil {
			return
		}
		// 声明一个 database.Todo 变量
		var todo database.Todo
		// 绑定请求体中的 JSON 数据
		if err := c.BindJSON(&todo); err != nil {
			c.JSON(400, gin.H{"error": "无效的请求数据: " + err.Error()})
			return
		}
		// 将从 URL 获取的 int 类型的 id 转换为 uint 类型,并赋值给 todo 的 ID 字段
		// 这是因为 GORM 的模型 ID 是 uint 类型
		todo.ID = uint(id)
		// 调用数据库函数更新 Todo
		if err := database.UpdateTodo(&todo, id); err != nil {
			c.JSON(500, gin.H{"error": "更新 Todo 失败"})
			return
		}
		// 成功更新,返回 200 OK 和更新后的 Todo 对象
		c.JSON(200, todo)
	}
}

// HandlerDelete 返回一个处理删除 Todo 请求的 Gin HandlerFunc
// 作用: 根据 ID 删除一个 Todo
// 输入: gin.Context
// 输出: 无
func HandlerDelete() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从 URL 参数中获取 ID
		id, err := getId(c)
		if err != nil {
			// 注意:这里为了与 README.md 中的 API 文档保持一致,当 ID 不存在时不直接返回 500,
			// 而是在 getId 函数中返回了 400。但如果 getId 内部逻辑变化,这里可能需要调整。
			// 实际上,如果 getId 返回错误,响应已经发送,这里可以简化。
			// 但为了清晰,我们假设可能存在 getId 内部未发送 JSON 响应的错误路径。
			c.JSON(500, gin.H{"error": "ID不存在"})
			return
		}
		// 调用数据库函数删除 Todo
		if err := database.DeleteTodo(id); err != nil {
			c.JSON(500, gin.H{"error": "删除 Todo 失败"})
			return
		}
		// 成功删除,返回 200 OK 和成功消息
		c.JSON(200, gin.H{"message": "删除成功!"})
	}
}

// getId 是一个辅助函数,用于从 Gin 上下文中提取并转换 ID
// 作用: 从 URL 路径参数中获取 "id",并将其从字符串转换为整数
// 输入: c (*gin.Context) - Gin 上下文
// 输出: (int, error) - 返回转换后的整数 ID 和可能发生的错误
func getId(c *gin.Context) (int, error) {
	// 从 URL 路径中获取名为 "id" 的参数值
	idStr := c.Param("id")
	// 使用 strconv.Atoi 将字符串 ID 转换为整数
	id, err := strconv.Atoi(idStr)
	if err != nil {
		// 如果转换失败(例如 "id" 不是数字),返回 400 Bad Request
		c.JSON(400, gin.H{"error": "无效的ID"})
		// 返回 0 和错误,通知调用者操作失败
		return 0, err
	}
	// 如果转换成功,返回整数 ID 和 nil
	return id, nil
}

第四步:依赖管理 (go.mod)

最后,go.mod 文件定义了我们的项目模块和所有依赖项及其版本。

module backend

go 1.24.4

require (
	github.com/gin-contrib/cors v1.7.6
	github.com/gin-gonic/gin v1.11.0
	gorm.io/driver/sqlite v1.6.0
	gorm.io/gorm v1.31.0
)

🚀 运行和测试

现在,代码已经全部完成,让我们来运行和测试它!

  1. 环境要求

    • 确保你已经安装了 Go (1.24.4 或更高版本)。
  2. 下载依赖

    在项目根目录(backend/)下打开终端,运行以下命令来下载 go.mod 中定义的所有依赖包:

    Bash

    go mod download
    
  3. 启动服务

    使用以下命令来编译并运行我们的服务:

    go run main.go
    

    如果一切顺利,你将在终端看到类似以下的输出,表示服务已在 http://localhost:8080 成功启动:

    [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
    
    [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
     - using code:   gin.SetMode(gin.ReleaseMode)
     - using env:    export GIN_MODE=release
    
    [GIN-debug] GET    /todo/:id                 --> main.HandlerGetID.func1 (3 handlers)
    [GIN-debug] GET    /todo                     --> main.HandlerGet.func1 (3 handlers)
    [GIN-debug] POST   /todo                     --> main.HandlerCreate.func1 (3 handlers)
    [GIN-debug] PUT    /todo/:id                 --> main.HandlerUpdate.func1 (3 handlers)
    [GIN-debug] DELETE /todo/:id                 --> main.HandlerDelete.func1 (3 handlers)
    [GIN-debug] Listening and serving HTTP on :8080
    
  4. 使用 cURL 测试 API

    你可以使用 cURL 或任何 API 测试工具(如 Postman)来验证接口是否正常工作。

    • 创建待办事项

      curl -X POST http://localhost:8080/todo \
        -H "Content-Type: application/json" \
        -d '{"title":"学习 Go 语言","completed":false}'
      
    • 获取所有待办事项

      curl http://localhost:8080/todo
      
    • 获取单个待办事项 (假设 ID 为 1)

      curl http://localhost:8080/todo/1
      

总结

恭喜你!你已经成功地使用 Go、Gin 和 GORM 构建了一个完整的 RESTful API 服务。我们从项目设计开始,一步步实现了数据模型、数据库操作和 API 接口,并最终成功运行和测试了我们的应用。

这个项目虽然简单,但它为你提供了一个坚实的起点,你可以基于它进行扩展,比如添加用户认证、输入验证、更复杂的业务逻辑等等。

希望这篇文章能帮助你更好地理解和使用 Go 语言进行后端开发。感谢阅读!

posted @ 2025-10-12 13:40  二见原莉莉子  阅读(11)  评论(0)    收藏  举报