GORM 是 Go 语言里常用的 ORM 框架,可以把数据库表映射成 Go 结构体,让我们用面向对象的方式操作 MySQL、PostgreSQL、SQLite 等数据库。
什么是 GORM#
ORM 的全称是 Object Relational Mapping,也就是对象关系映射。
在 Go 项目里,如果不用 ORM,通常需要手写 SQL:
1
| rows, err := db.Query("select id, name, age from users where id = ?", id)
|
使用 GORM 后,可以用结构体和方法调用来操作数据库:
1
2
| var user User
db.First(&user, id)
|
GORM 会根据结构体、字段、方法调用生成对应 SQL。
GORM 常见特点:
- 支持 MySQL、PostgreSQL、SQLite、SQL Server 等数据库。
- 支持结构体和表的自动映射。
- 支持增删改查、事务、关联关系、预加载。
- 支持自动迁移表结构。
- 支持 Hook、软删除、日志、连接池等功能。
安装 GORM#
以 MySQL 为例:
1
2
| go get gorm.io/gorm
go get gorm.io/driver/mysql
|
如果使用 SQLite:
1
| go get gorm.io/driver/sqlite
|
如果使用 PostgreSQL:
1
| go get gorm.io/driver/postgres
|
连接数据库#
连接 MySQL#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:password@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
_ = db
}
|
DSN 说明:
root:password:数据库用户名和密码。127.0.0.1:3306:数据库地址和端口。test:数据库名。charset=utf8mb4:支持完整 UTF-8 字符。parseTime=True:把数据库时间转换成 Go 的 time.Time。loc=Local:使用本地时区。
连接 SQLite#
1
2
3
4
| db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic(err)
}
|
SQLite 适合本地测试、小工具、单机应用。
定义模型#
GORM 用结构体表示数据库表。
1
2
3
4
5
6
7
8
| type User struct {
ID uint
Name string
Email string
Age int
CreatedAt time.Time
UpdatedAt time.Time
}
|
默认规则:
- 结构体名
User 对应表名 users。 - 字段
Name 对应列名 name。 ID 默认是主键。CreatedAt 创建时自动填充。UpdatedAt 更新时自动填充。
如果想使用 GORM 内置基础模型,可以嵌入 gorm.Model:
1
2
3
4
5
6
| type User struct {
gorm.Model
Name string
Email string
Age int
}
|
gorm.Model 包含:
1
2
3
4
5
6
| type Model struct {
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
|
其中 DeletedAt 用于软删除。
自动迁移表结构#
GORM 可以根据结构体自动创建或更新表结构:
1
2
3
4
| err := db.AutoMigrate(&User{})
if err != nil {
panic(err)
}
|
AutoMigrate 会创建表、添加缺少的字段、创建索引和约束,但一般不会主动删除字段,避免误删数据。
开发环境可以使用 AutoMigrate,生产环境更推荐使用专门的迁移工具管理表结构。
使用 tag 控制字段#
GORM 支持通过结构体 tag 控制字段属性:
1
2
3
4
5
6
| type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:64;not null"`
Email string `gorm:"uniqueIndex;size:128"`
Age int `gorm:"default:18"`
}
|
常见 tag:
primaryKey:主键。column:name:指定列名。type:varchar(100):指定数据库类型。size:64:指定字段长度。not null:非空。default:18:默认值。uniqueIndex:唯一索引。index:普通索引。-:忽略该字段,不映射到数据库。
示例:
1
2
3
4
5
| type User struct {
ID uint
NickName string `gorm:"column:nick_name"`
Password string `gorm:"-"`
}
|
新增数据#
创建一条记录#
1
2
3
4
5
6
7
8
9
10
11
| user := User{
Name: "Tom",
Email: "tom@example.com",
Age: 18,
}
result := db.Create(&user)
fmt.Println(result.Error)
fmt.Println(result.RowsAffected)
fmt.Println(user.ID)
|
Create 成功后,GORM 会把自增主键回填到结构体中。
批量创建#
1
2
3
4
5
6
| users := []User{
{Name: "Tom", Email: "tom@example.com", Age: 18},
{Name: "Jack", Email: "jack@example.com", Age: 20},
}
db.Create(&users)
|
查询数据#
根据主键查询#
1
2
3
| var user User
db.First(&user, 1)
|
常见方法:
First:按主键升序查询第一条。Last:按主键降序查询第一条。Take:查询一条,不指定排序。
查询所有记录#
1
2
3
| var users []User
db.Find(&users)
|
条件查询#
1
2
3
| var user User
db.Where("name = ?", "Tom").First(&user)
|
多个条件:
1
| db.Where("name = ? and age > ?", "Tom", 18).Find(&users)
|
使用结构体条件:
1
| db.Where(&User{Name: "Tom"}).Find(&users)
|
使用 map 条件:
1
2
3
4
| db.Where(map[string]any{
"name": "Tom",
"age": 18,
}).Find(&users)
|
排序、分页#
1
2
3
4
5
| db.Where("age > ?", 18).
Order("created_at desc").
Limit(10).
Offset(20).
Find(&users)
|
这段代码表示查询年龄大于 18 的用户,按创建时间倒序,跳过前 20 条,取 10 条。
选择指定字段#
1
| db.Select("id", "name", "email").Find(&users)
|
查询数量#
1
2
3
| var count int64
db.Model(&User{}).Where("age > ?", 18).Count(&count)
|
更新数据#
更新单个字段#
1
| db.Model(&User{}).Where("id = ?", 1).Update("age", 20)
|
更新多个字段#
1
2
3
4
| db.Model(&User{}).Where("id = ?", 1).Updates(map[string]any{
"name": "Jack",
"age": 21,
})
|
也可以使用结构体:
1
2
3
4
| db.Model(&user).Updates(User{
Name: "Jack",
Age: 21,
})
|
注意,使用结构体更新时,GORM 默认不会更新零值字段,比如 0、false、空字符串。
1
2
3
| db.Model(&user).Updates(User{
Age: 0, // 默认不会更新
})
|
如果确实要更新零值,可以使用 map:
1
2
3
| db.Model(&user).Updates(map[string]any{
"age": 0,
})
|
或者使用 Select:
1
| db.Model(&user).Select("Age").Updates(User{Age: 0})
|
删除数据#
根据主键删除#
条件删除#
1
| db.Where("age < ?", 18).Delete(&User{})
|
如果模型里包含 gorm.DeletedAt,GORM 默认执行软删除,也就是只更新 deleted_at 字段,不会真正删除数据库记录。
1
2
3
4
| type User struct {
gorm.Model
Name string
}
|
软删除后,普通查询不会查到这条数据。
如果要查询被软删除的数据:
1
| db.Unscoped().Where("id = ?", 1).First(&user)
|
如果要物理删除:
1
| db.Unscoped().Delete(&user)
|
错误处理#
GORM 的大部分操作都会返回 *gorm.DB,错误信息在 Error 字段中。
1
2
3
4
| result := db.First(&user, 1)
if result.Error != nil {
panic(result.Error)
}
|
判断记录不存在:
1
2
3
4
| result := db.First(&user, 1)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
fmt.Println("user not found")
}
|
也可以查看影响行数:
1
2
3
4
| result := db.Model(&User{}).Where("id = ?", 1).Update("age", 20)
fmt.Println(result.RowsAffected)
fmt.Println(result.Error)
|
事务用于保证一组数据库操作要么全部成功,要么全部失败。
手动事务#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| tx := db.Begin()
if err := tx.Error; err != nil {
panic(err)
}
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return
}
if err := tx.Create(&order).Error; err != nil {
tx.Rollback()
return
}
tx.Commit()
|
Transaction 方法#
更推荐使用 Transaction,代码更简洁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
if err := tx.Create(&order).Error; err != nil {
return err
}
return nil
})
if err != nil {
fmt.Println("transaction failed:", err)
}
|
只要回调函数返回错误,GORM 就会自动回滚。返回 nil 则自动提交。
关联关系#
GORM 支持常见表关系:一对一、一对多、多对多。
一对多#
一个用户有多篇文章:
1
2
3
4
5
6
7
8
9
10
11
| type User struct {
ID uint
Name string
Posts []Post
}
type Post struct {
ID uint
Title string
UserID uint
}
|
创建:
1
2
3
4
5
6
7
8
9
| user := User{
Name: "Tom",
Posts: []Post{
{Title: "GORM 入门"},
{Title: "Go Web 开发"},
},
}
db.Create(&user)
|
预加载关联数据:
1
2
3
| var user User
db.Preload("Posts").First(&user, 1)
|
如果不使用 Preload,默认只查询 users 表,不会自动查 posts。
多对多#
用户和角色是多对多关系:
1
2
3
4
5
6
7
8
9
10
| type User struct {
ID uint
Name string
Roles []Role `gorm:"many2many:user_roles;"`
}
type Role struct {
ID uint
Name string
}
|
GORM 会通过中间表 user_roles 维护关联。
查询时预加载:
1
| db.Preload("Roles").First(&user, 1)
|
原生 SQL#
有些复杂查询直接写 SQL 更清晰,GORM 也支持原生 SQL。
Raw 查询#
1
2
3
| var users []User
db.Raw("select id, name, age from users where age > ?", 18).Scan(&users)
|
Exec 执行#
1
| db.Exec("update users set age = age + 1 where id = ?", 1)
|
ORM 不是必须替代所有 SQL。复杂报表、复杂 join、性能敏感 SQL,直接写原生 SQL 反而更清楚。
日志和调试#
调试时可以使用 Debug() 打印 SQL:
1
| db.Debug().Where("name = ?", "Tom").First(&user)
|
也可以在初始化时配置日志级别:
1
2
3
| db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
|
这样可以看到 GORM 实际执行的 SQL,排查条件、字段、关联查询问题会方便很多。
连接池配置#
GORM 底层使用的是标准库 database/sql,可以配置连接池:
1
2
3
4
5
6
7
8
| sqlDB, err := db.DB()
if err != nil {
panic(err)
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
|
常见含义:
SetMaxIdleConns:最大空闲连接数。SetMaxOpenConns:最大打开连接数。SetConnMaxLifetime:连接最大复用时间。
线上项目一般需要根据数据库性能、服务并发量和部署实例数量设置连接池,避免连接数过多压垮数据库。
常见注意点#
不要忽略 Error#
1
2
3
| if err := db.Create(&user).Error; err != nil {
return err
}
|
数据库操作一定要检查错误,否则可能出现写入失败但业务继续执行的问题。
结构体更新默认忽略零值#
1
| db.Model(&user).Updates(User{Age: 0})
|
这类写法不会更新 Age,如果要更新零值,使用 map 或 Select。
查询单条记录要处理 ErrRecordNotFound#
1
2
3
4
| err := db.First(&user, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
|
复杂 SQL 不要强行 ORM#
ORM 适合常规 CRUD,但复杂统计、复杂 join、窗口函数等场景,直接写 SQL 通常更直观,也更容易优化。
生产环境谨慎使用 AutoMigrate#
AutoMigrate 很适合开发阶段快速建表,但生产环境表结构变更应该可审计、可回滚,建议使用迁移工具。
一句话总结#
GORM 是 Go 语言常用的 ORM 框架,通过结构体映射数据库表,提供连接数据库、自动迁移、CRUD、事务、关联查询、软删除、原生 SQL 等能力;它能提高常规业务开发效率,但使用时要注意错误处理、零值更新、连接池配置和复杂 SQL 的性能问题。