Gin + GORM

Gin + GORM 实战:把待办事项真正存进数据库

上一篇我们做了待办事项 API,但数据存在内存切片里,程序一重启就全没了。这一篇给它接上真正的数据库,用 Go 最流行的 ORM 框架 GORM,让数据"活"下来。

使用 SQLite(一个文件就是数据库),零配置、不用装数据库服务,Windows 也能直接跑通。

建议先看完系列前四篇(Go 项目结构、Go 常用标准库包、Gin 入门),衔接更顺。


0. 上一篇的问题

回顾一下上一篇的代码,数据是这样存的:

go 复制代码
var todos = []model.Todo{} // 内存切片

问题很明显:

  1. 程序一重启,数据全没了(内存一清空就消失)
  2. 数据多了以后,内存操作也管理不过来

解决办法就是数据库:数据写到磁盘上,程序重启后还在。而操作数据库,我们不用手写 SQL,用 GORM。


1. GORM 是什么?

GORM 是 Go 最流行的 ORM 框架(对象关系映射)。

一句话:ORM 是结构体和数据库表之间的"翻译官"。

复制代码
Go 结构体(Todo)  ←→  数据库表(todos)
你写:db.Create(&t)     实际执行:INSERT INTO todos ...
你写:db.First(&t, 1)   实际执行:SELECT * FROM todos WHERE id = 1

你只操作结构体,GORM 帮你翻译成 SQL 去数据库执行。这样代码里没有一堆字符串 SQL,类型安全,也更好读。

GORM 当前版本 v1.31(gorm.io/gorm),支持 MySQL、PostgreSQL、SQLite 等几乎所有主流数据库。本篇用 SQLite 入门,换 MySQL 只改一行连接代码。


2. 准备工作:安装 GORM 和 SQLite 驱动

在你上一篇的 mytodo 项目里执行:

bash 复制代码
go get gorm.io/gorm
go get github.com/glebarez/sqlite

3. 五分钟认识 GORM 核心五件套

先不看 Gin,单独跑一个最简单的 GORM 程序,把五个核心操作过一遍。

新建 main.go

go 复制代码
package main

import (
	"fmt"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

// User 结构体,对应数据库里的 users 表
type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string
	Age  int
}

func main() {
	// 1. 连接数据库(SQLite 就是一个文件,自动创建)
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		panic("连接数据库失败:" + err.Error())
	}

	// 2. 自动建表(没有 users 表就创建,有就跳过)
	db.AutoMigrate(&User{})

	// 3. 增:Create(注意要传地址,u.ID 会被自动填上)
	u := User{Name: "小明", Age: 20}
	db.Create(&u)
	fmt.Println("新记录的 ID:", u.ID)

	// 4. 查:First 按主键查一条
	var u1 User
	db.First(&u1, u.ID)
	fmt.Println("查到的:", u1.Name, u1.Age)

	// 5. 改:Save 保存修改
	u1.Age = 21
	db.Save(&u1)

	// 6. 删:Delete 按主键删除
	db.Delete(&User{}, u.ID)
	fmt.Println("搞定,去当前目录看看 test.db 文件吧")
}

运行 go run .,正常的话什么都不报错。此时项目目录下多了一个 test.db 文件------这就是你的数据库。

核心五件套速记:

操作 GORM 写法 对应 SQL
连接 gorm.Open(sqlite.Open("test.db"), ...) ---
建表 db.AutoMigrate(&User{}) CREATE TABLE
db.Create(&u) INSERT
db.First(&u, id) / db.Find(&list) SELECT
db.Save(&u) UPDATE
db.Delete(&User{}, id) DELETE

注意:增删改查全部要传地址&),原因和上一篇 ShouldBindJSON(&t) 一样------函数要修改你的变量,就得知道它在内存哪里。


4. 改造 model:给结构体加数据库标签

回到 Gin 项目,先升级数据模型。原来的 internal/model/todo.go

go 复制代码
package model

import "time"

// Todo 待办事项
type Todo struct {
	ID        uint      `json:"id" gorm:"primaryKey"`
	Title     string    `json:"title"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"created_at"`
}

和上一篇相比只多了两样东西:

  1. gorm:"primaryKey":告诉 GORM 这个字段是主键,会自动增长
  2. CreatedAt time.Time:GORM 的约定------叫这个名字的字段,插入时自动填当前时间,不用你管

5. 改造 handler:从"内存切片"换成"数据库操作"

原来的 internal/handler/todo.govar todos = []model.Todo{} 存数据,现在全部换成数据库操作,并且顺便补上"更新"和"删除"两个接口:

go 复制代码
package handler

import (
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"example.com/mytodo/internal/model"
)

// DB 全局数据库连接(演示用;正式项目一般封装成单例)
var DB *gorm.DB

// ListTodos GET /api/todos 待办列表
func ListTodos(c *gin.Context) {
	var todos []model.Todo
	DB.Find(&todos) // 查全部
	c.JSON(http.StatusOK, gin.H{"data": todos})
}

// CreateTodo POST /api/todos 新增待办
func CreateTodo(c *gin.Context) {
	var t model.Todo
	if err := c.ShouldBindJSON(&t); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	DB.Create(&t) // 插入数据库,t.ID 和 t.CreatedAt 会被自动填上
	c.JSON(http.StatusOK, gin.H{"data": t})
}

// GetTodo GET /api/todos/:id 查单个待办
func GetTodo(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	var t model.Todo
	if err := DB.First(&t, id).Error; err != nil { // 按主键查
		c.JSON(http.StatusNotFound, gin.H{"error": "没有这条待办"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": t})
}

// UpdateTodo PUT /api/todos/:id 更新待办(比如标记完成)
func UpdateTodo(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	var t model.Todo
	if err := DB.First(&t, id).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "没有这条待办"})
		return
	}
	// 把请求体里的 JSON 合并进已查出的记录(没传的字段保持原值)
	if err := c.ShouldBindJSON(&t); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	DB.Save(&t) // 保存修改
	c.JSON(http.StatusOK, gin.H{"data": t})
}

// DeleteTodo DELETE /api/todos/:id 删除待办
func DeleteTodo(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	DB.Delete(&model.Todo{}, id) // 按主键删除
	c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

注意 DB.First(&t, id).Error 这个写法:GORM 支持链式调用,最后用 .Error 拿结果。如果没查到记录,Error 不为空,返回 404------和上一篇 if err != nil 的判断思路完全一样。


6. 改造 main.go:连接数据库 + 自动建表 + 新路由

入口文件 cmd/server/main.go 也要动:

go 复制代码
package main

import (
	"log"

	"github.com/gin-gonic/gin"
	"github.com/glebarez/sqlite"
	"gorm.io/gorm"

	"example.com/mytodo/internal/handler"
	"example.com/mytodo/internal/model"
)

func main() {
	// 1. 连接数据库(todo.db 是文件,程序重启数据不丢)
	db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{})
	if err != nil {
		log.Fatal("连接数据库失败:", err)
	}
	handler.DB = db // 交给 handler 层使用

	// 2. 自动建表(表不存在就创建,存在就跳过)
	db.AutoMigrate(&model.Todo{})

	// 3. 注册路由(新增了 PUT 和 DELETE)
	r := gin.Default()
	api := r.Group("/api")
	{
		api.GET("/todos", handler.ListTodos)
		api.POST("/todos", handler.CreateTodo)
		api.GET("/todos/:id", handler.GetTodo)
		api.PUT("/todos/:id", handler.UpdateTodo)
		api.DELETE("/todos/:id", handler.DeleteTodo)
	}

	log.Println("服务启动:http://localhost:8080")
	r.Run(":8080")
}

7. 运行并测试:关键验证"重启不丢"

启动服务:

bash 复制代码
go run ./cmd/server

另开一个终端,把 CRUD 全测一遍:

bash 复制代码
# 1. 新增两条
curl -X POST http://localhost:8080/api/todos \
  -H "Content-Type: application/json" -d '{"title":"买牛奶","done":false}'

curl -X POST http://localhost:8080/api/todos \
  -H "Content-Type: application/json" -d '{"title":"写 Gin 博客","done":false}'

# 2. 查列表(能看到刚才两条,且自动带了 id 和 created_at)
curl http://localhost:8080/api/todos

# 3. 把第 1 条标记为完成
curl -X PUT http://localhost:8080/api/todos/1 \
  -H "Content-Type: application/json" -d '{"done":true}'

# 4. 删除第 2 条
curl -X DELETE http://localhost:8080/api/todos/2

# 5. 再查一次列表确认
curl http://localhost:8080/api/todos

最后一步是重点 :按 Ctrl+C 停掉服务,再执行 go run ./cmd/server 重启,然后重新查列表------

bash 复制代码
curl http://localhost:8080/api/todos

你会发现数据还在 !因为数据已经写进了磁盘上的 todo.db 文件,这就是数据库的意义。

顺手看一眼项目目录,会多出 todo.db 文件,它就是整个数据库。备份它 = 备份所有数据。


8. 总结:一张表 + 三个坑

GORM 速查表:

我想做 GORM 写法
连接数据库 gorm.Open(sqlite.Open("todo.db"), &gorm.Config{})
自动建表 db.AutoMigrate(&Todo{})
新增 db.Create(&t)
查全部 db.Find(&todos)
按主键查一条 db.First(&t, id)
保存修改 db.Save(&t)
按主键删除 db.Delete(&Todo{}, id)

数据库接好之后,下一篇可以讲讲 GORM 的进阶:一对多关联(比如"用户拥有多条待办")、条件查询,或者给接口加上登录鉴权(JWT)。想看哪个评论区告诉我~


参考资料

相关推荐
零基础的修炼1 小时前
CUDA知识汇总
开发语言·c++·编辑器
妙码生花1 小时前
golang 应用服务端部署(使用 systemd 服务)
开发语言·人工智能·后端·golang·node.js·php·gin
重生之小比特1 小时前
【Java SE】运算符
java·开发语言
Patrick在香港1 小时前
Claude + Python 数据管道:一次字段改名,786 行历史数据被静默重写
开发语言·windows·python·claude·数据工程·数据校验·数据管道
CodeStats1 小时前
【Java数据类型】Java底层数据类型原理(下):时间API、JSON与String存储演进
java·开发语言·数据类型·string·byte
Brilliantwxx2 小时前
【STM32 】把 printf 搬到串口上 —— C 标准 IO 函数重定向与多文件工程搭建(实战串口电灯)
c语言·开发语言·stm32·单片机·嵌入式硬件·架构·ecmascript
流浪0012 小时前
大厂 C/C++ 2023–2025 面试答案题汇总
开发语言·c++
Yanjun2i2 小时前
Agent学习记录六:Tool 类 + Tool Registry
开发语言·python·学习
一条泥憨鱼2 小时前
【从0开始学习计算机网络】| TIME_WAIT 为什么是 2MSL,CLOSE_WAIT / TIME_WAIT 堆积怎么排查
服务器·开发语言·网络·计算机网络·网络安全