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:
- GORM 用结构体映射表 ,
User→users,UserName→user_name gorm.Open(mysql.Open(dsn), &gorm.Config{})是连接入口- DSN 一定要加
parseTime=True&loc=Local .Error是统一的错误出口,别忘了检查AutoMigrate自动建表,开发阶段超好用CreatedAt/UpdatedAt是 GORM 的魔法字段,自动维护时间
下一篇博客,我们会开始讲 GORM 的 CRUD :Create、First、Find、Update、Delete 的完整用法和常用选项。
祝编码愉快 🚀