GORM 入门(二):从 database/sql 平滑过渡到 GORM

GORM 入门(二):从 database/sql 平滑过渡到 GORM

一、写在前面

在上一篇博客里,我们用 database/sql 学会了 Exec、Query、QueryRow 三大基础方法。虽然能跑通,但你应该也感受到了:

  • 每次都要手动写 SQL
  • 每次都要手动 Scan 到变量或结构体
  • 表字段一改,SQL 和 Scan 全都要跟着改

当业务变复杂时,这些重复劳动会让人崩溃。

这时候,GORM 就登场了。它是 Go 里最流行的 ORM 框架,能让你用操作对象的方式操作数据库,而不是手写 SQL。

这一篇博客,我们就从前一篇的代码出发,一步步引入 GORM。


二、GORM 是什么?

一句话:

GORM 是一个把「结构体」和「数据库表」自动映射起来的 ORM 框架。

你定义好结构体:

go 复制代码
type User struct {
    ID    int
    Name  string
    Email string
}

GORM 就知道:

  • 结构体 User → 表 users
  • 字段 ID → 列 id
  • 字段 Name → 列 name
  • 字段 Email → 列 email

之后你想查所有用户,只需要:

go 复制代码
var users []User
db.Find(&users)

不需要写 SELECT * FROM users,不需要手动 Scan,GORM 全都帮你做了。


三、安装 GORM

GORM 拆成了两个包:

包 作用
gorm.io/gorm GORM 核心,和数据库无关
gorm.io/driver/mysql MySQL 驱动,把 GORM 接到具体数据库上

安装命令:

bash 复制代码
go get -u gorm.io/gorm
go get gorm.io/driver/mysql

安装完成后,你的 go.mod 里会多出类似这些依赖:

复制代码
github.com/jinzhu/inflection v1.0.0
github.com/jinzhu/now v1.1.5
golang.org/x/text v0.42.0
gorm.io/gorm v1.31.2
gorm.io/driver/mysql v1.6.0

小提示 :GORM 从 v2 开始,包路径统一是 gorm.io/gorm,不再是老版本的 github.com/jinzhu/gorm。网上很多老教程写的还是 v1 的路径,注意甄别。


四、你的第一段 GORM 代码

先上一份能跑的完整代码:

go 复制代码
package main

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	ID    int
	Name  string
	Email string
}

func main() {
	// DSN (Data Source Name) 连接字符串
	dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm_db_new?charset=utf8mb4&parseTime=True&loc=Local"

	// 连接数据库
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Println("连接数据库失败:", err)
		return
	}

	// 打印数据库连接实例
	fmt.Println(db)

	// 查询所有用户
	var userList []User
	db.Find(&userList)
	fmt.Println(userList)
}

跑起来后你会看到类似输出:

复制代码
&{0xc0001b0000 <nil> 0 0xc000180000 1}
[{1 张三 zhangsan@example.com} {2 李四 lisi@example.com}]

slices 里那一串 {1 张三 xxx} 就是查出来的用户。


五、逐行拆解这段代码

5.1 定义结构体

go 复制代码
type User struct {
	ID    int
	Name  string
	Email string
}

GORM 的默认约定(新手最容易踩坑的地方,务必记住):

结构体 对应的表
User users(复数)
UserInfo user_infos
Person people(不规则复数)
字段 对应的列
ID id
Name name
UserName user_name(驼峰转下划线)

所以这段结构体虽然一个 tag 都没写,GORM 也能正确映射到 users 表。

5.2 连接数据库

go 复制代码
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

和 database/sql 最大的区别:

  • database/sql 用 sql.Open("mysql", dsn)
  • GORM 用 gorm.Open(mysql.Open(dsn), &gorm.Config{})

拆开看:

  • mysql.Open(dsn) → 生成一个 MySQL 的 dialector(拨号器)
  • gorm.Open(dialector, config) → 用这个 dialector 建立 GORM 实例

返回值 *gorm.DB 不是数据库连接,而是 GORM 的操作入口。它是线程安全的,可以在多个 goroutine 里共享,内部会自动维护连接池。

5.3 查询

go 复制代码
var userList []User
db.Find(&userList)
  • db.Find(&userList) → 等价于 SELECT * FROM users
  • 传入的是切片指针 &userList,GORM 会把每一行填充成 User 追加到切片里

如果换成单个对象:

go 复制代码
var user User
db.First(&user)              // 查第一条,相当于 LIMIT 1
db.First(&user, 1)           // 按主键查:WHERE id = 1
db.First(&user, "name = ?", "张三") // 条件查询

六、必须加上的两个改进

你那段代码虽然能跑,但有两个地方强烈建议改,这是初学者最常翻车的两个点。

6.1 DSN 一定要加 parseTime=True

原来的 DSN:

go 复制代码
dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm_db_new"

改进后:

go 复制代码
dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm_db_new?charset=utf8mb4&parseTime=True&loc=Local"

为什么?

我们的 users 表里有 created_at 和 updated_at 两个 datetime 字段。如果不加 parseTime=True,MySQL 驱动会把它们当成 []byte 返回,GORM 就没法把它们转成 time.Time ,将来你往结构体里加 CreatedAt time.Time 字段时会直接报错。

loc=Local 是为了让时间按本地时区解析,避免差 8 小时。

6.2 Find 一定要检查错误

原代码:

go 复制代码
db.Find(&userList)
fmt.Println(userList)

改进后:

go 复制代码
if err := db.Find(&userList).Error; err != nil {
	fmt.Println("查询失败:", err)
	return
}
fmt.Println(userList)

GORM 的错误处理风格是链式调用 + 末尾检查 .Error:

go 复制代码
db.Where("age > ?", 18).Order("id desc").Find(&users).Error

任何一步出错,都会保存在最终的 .Error 里。养成习惯:只要用到 GORM,返回的 *gorm.DB 最后都要检查一下 .Error。


七、database/sql vs GORM 对比

看完前面的内容,我们再用一张表把两者对照一下:

场景 database/sql GORM
连接 sql.Open("mysql", dsn) gorm.Open(mysql.Open(dsn), &gorm.Config{})
健康检查 db.Ping() 自动(首次使用才连)
建表 手写 CREATE TABLE db.AutoMigrate(&User{})
插入 db.Exec("INSERT ...", ...) db.Create(&user)
查询单条 db.QueryRow(...).Scan(...) db.First(&user, id)
查询多条 db.Query(...) + 手动 Scan db.Find(&users)
更新 db.Exec("UPDATE ...") db.Model(&user).Update("age", 20)
删除 db.Exec("DELETE ...") db.Delete(&user)
错误检查 每步 if err != nil 末尾 .Error

可以看到,GORM 把「SQL 拼接」和「结果扫描」这两件事全部隐掉了,代码量明显减少。


八、用 AutoMigrate 自动建表

GORM 最爽的功能之一:根据结构体自动建表。

go 复制代码
type User struct {
	ID        uint      `gorm:"primaryKey"`
	Name      string    `gorm:"size:50;not null;default:''"`
	Age       int       `gorm:"not null;default:0"`
	Email     string    `gorm:"size:100;not null;default:''"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

db.AutoMigrate(&User{})

执行后,GORM 会自动:

  • 检查 users 表是否存在,不存在就建
  • 存在则对比结构体,缺失的列自动补上(不会删列,安全)

AutoMigrate 非常适合开发阶段。但生产环境慎用 ,因为它的字段变更策略比较粗暴,推荐用专门的迁移工具(如 golang-migrate、goose)。

关于常用 tag

Tag 作用
gorm:"primaryKey" 指定主键
gorm:"size:50" varchar(50)
gorm:"not null" 非空约束
gorm:"default:0" 默认值
gorm:"uniqueIndex" 唯一索引
gorm:"column:user_name" 自定义列名
gorm:"-" 忽略该字段,不映射到数据库

九、再小进一步:加一个 Age 字段

你原来的结构体里少了 age,而数据库表里是有这个字段的。虽然 GORM 默认是查多少列填多少字段,少写字段不会报错,但为了数据完整,我们补上:

go 复制代码
type User struct {
	ID        uint      `gorm:"primaryKey"`
	Name      string    `gorm:"size:50"`
	Age       int
	Email     string    `gorm:"size:100"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

再次运行 db.Find(&userList),就能看到完整信息了。

提示 :CreatedAt 和 UpdatedAt 是 GORM 的魔法字段名 。只要有这两个字段,GORM 会在 Create / Update 时自动填充当前时间,完全不用你操心。


十、完整可运行版本

把前面的所有改进合并,最终版本如下:

go 复制代码
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	ID        uint      `gorm:"primaryKey"`
	Name      string    `gorm:"size:50;not null;default:''"`
	Age       int       `gorm:"not null;default:0"`
	Email     string    `gorm:"size:100;not null;default:''"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

func main() {
	// 1. DSN:注意 parseTime=True 和 loc=Local
	dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm_db_new?charset=utf8mb4&parseTime=True&loc=Local"

	// 2. 连接 GORM
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatalln("连接数据库失败:", err)
	}

	// 3. 拿到底层 *sql.DB,验证连接是否真的建立
	sqlDB, err := db.DB()
	if err != nil {
		log.Fatalln("获取底层连接失败:", err)
	}
	defer sqlDB.Close()

	if err = sqlDB.Ping(); err != nil {
		log.Fatalln("Ping 失败:", err)
	}
	fmt.Println("✅ 数据库连接成功")

	// 4. 自动建表 / 补列(开发阶段使用)
	if err = db.AutoMigrate(&User{}); err != nil {
		log.Fatalln("AutoMigrate 失败:", err)
	}

	// 5. 查询所有用户
	var userList []User
	if err = db.Find(&userList).Error; err != nil {
		log.Fatalln("查询失败:", err)
	}

	fmt.Println("查询到", len(userList), "条用户记录:")
	for _, u := range userList {
		fmt.Printf("id=%d name=%s age=%d email=%s\n", u.ID, u.Name, u.Age, u.Email)
	}
}

十一、常见坑总结

坑 现象 解决方案
DSN 缺 parseTime=True datetime 字段 Scan 报错 DSN 加上 parseTime=True&loc=Local
忽略 .Error 查询失败了也不知道 末尾统一 .Error 检查
结构体是单数 表名对不上 GORM 默认表名是结构体名的复数
传值而不是指针 Find(userList) 没有效果 必须传 &userList
直接用 db 打印 输出看不懂的一堆指针 用 .DB() 拿底层 *sql.DB 检查
用 v1 的老教程 包路径 github.com/jinzhu/gorm 报错 用 v2:gorm.io/gorm
生产环境乱用 AutoMigrate 表结构被莫名修改 用专业迁移工具

十二、总结

这一篇我们从 database/sql 的写法自然过渡到了 GORM:

  1. GORM 用结构体映射表 ,User → users,UserName → user_name
  2. gorm.Open(mysql.Open(dsn), &gorm.Config{}) 是连接入口
  3. DSN 一定要加 parseTime=True&loc=Local
  4. .Error 是统一的错误出口,别忘了检查
  5. AutoMigrate 自动建表,开发阶段超好用
  6. CreatedAt / UpdatedAt 是 GORM 的魔法字段,自动维护时间

下一篇博客,我们会开始讲 GORM 的 CRUD :Create、First、Find、Update、Delete 的完整用法和常用选项。

祝编码愉快 🚀

相关推荐
长春的KAKA6 小时前
GitLab 项目页面报 500 错误的完整修复记录:PostgreSQL 数据库损坏、统计表缺失及数据库迁移
数据库·postgresql·gitlab
这个DBA有点耶6 小时前
关系数据库管理系统选型指南:四步决策框架与主流产品技术路线对比
数据库·sql·程序人生·mysql·database·dba
jason.zeng@15022076 小时前
(七)「固化 Rest 接口 + Text-to-SQL 灵活查询」双模式 Agent 架构教程
数据库·python·sql·ai·架构·langchain·ai编程
我不会起名字3227 小时前
MVCC 快照读为什么读不到刚提交的数据:ReadView 的 4 条可见性规则
数据库·mysql·innodb·mvcc·事务隔离
2501_933670797 小时前
2027届数据类专业投用户增长岗:A/B实验、指标和SQL准备思路
数据库·sql
用户337922545687 小时前
Sirchmunk 深度解析(一):一个无需向量数据库的自进化搜索引擎架构设计
数据库
染指11107 小时前
139.Agent-多Agent框架-Skills渐进式加载文档
数据库·人工智能·langchain·agents
骑着蜗牛撵大象3277 小时前
SpringBoot+Vue3 企业智能体侧挂架构:独立服务、独立数据库与主线零侵入落地
数据库·spring boot·架构·vue·springboot·事件驱动·服务拆分
螺蛳粉 螺蛳粉8 小时前
第四篇:Keepalived + MySQL 主从高可用实战
数据库·mysql·adb·keepalived·高可用