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.go 用 var 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)。想看哪个评论区告诉我~


参考资料

相关推荐
泡海椒6 小时前
JQuick-Excel 字段映射实战:用 MAPPING 固化 Excel 表头与业务字段契约
xml·java·开发语言·excel
lcj25116 小时前
【C++】set和map——详细使用说明
开发语言·c++·笔记·面试
潼心1412o6 小时前
C++初阶(长期更新)第3讲:类和对象(中)
开发语言·c++
外收内放6 小时前
Python基础语法练习题(40-42)
开发语言·python
程序喵大人6 小时前
【C++入门】编译链接模型 - 02 预处理把头文件怎样塞进源文件
开发语言·c++·预处理·编译链接·头文件·源文件
茉莉玫瑰花茶6 小时前
GO [ 并发 · 调度器 ]
开发语言·后端·golang
zhangzeyuaaa6 小时前
深入理解 Ruby 可变对象与不可变对象的原理、坑点与最佳实践
开发语言·后端·ruby
恋喵大鲤鱼6 小时前
go list command
golang·list
新鲜势力呀7 小时前
PHP 实战:第三方接口经常超时怎么办?从API调用优化到服务稳定性完整方案
开发语言·php