在 Go 生态中,GORM 是最受欢迎的 ORM(对象关系映射)库之一。它以开发者友好的方式,提供了功能强大且易于使用的数据库操作接口,让开发者可以用更少的代码完成复杂的数据库交互。本文将从安装开始,带你快速掌握 GORM 的核心用法和实用技巧。
什么是 GORM
GORM 是一个全功能的 Go 语言 ORM 框架,它通过将结构体(struct)映射为数据库中的表,将结构体字段映射为表中的列,让开发者可以用面向对象的方式操作数据库。无需手写冗长的 SQL 语句,即可完成增删改查、关联查询、事务、钩子函数等高级操作。
官方仓库 :GitHub - go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly · GitHub
官方文档 :GORM - The fantastic ORM library for Golang, aims to be developer friendly.
快速开始
安装 GORM 和数据库驱动
在项目目录下执行以下命令,安装 GORM 和 MySQL 驱动(你也可以根据实际需求选择 PostgreSQL、SQLite 等):
bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
若使用其他数据库,替换对应的驱动即可,例如:
bash
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlite
建立数据库连接
Go
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 数据源名称格式:user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
dsn := "root:123456@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("数据库连接失败: " + err.Error())
}
// 获取通用数据库对象 sql.DB 以使用连接池功能
sqlDB, _ := db.DB()
// 设置连接池
sqlDB.SetMaxIdleConns(10) // 空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大连接数
// 后续操作使用 db 对象
}
定义模型(Model)
GORM 通过结构体来定义数据库表模型。约定优于配置:默认使用结构体名称的蛇形复数形式作为表名,字段名的蛇形形式作为列名。
Go
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:user_name;type:varchar(100);not null"`
Email string `gorm:"uniqueIndex;size:255"`
Age int `gorm:"default:18"`
CreatedAt time.Time
UpdatedAt time.Time
}
结构体标签(tags)可以详细定义列名、类型、索引、约束等。常见的标签选项:
-
primaryKey:主键 -
column:指定列名 -
type:列数据类型 -
not null:非空 -
uniqueIndex:唯一索引 -
default:默认值 -
autoIncrement:自增 -
size:字段长度
自动迁移(Auto Migration)
GORM 可以根据模型定义自动创建或更新数据库表结构,非常适合开发阶段的快速迭代。
Go
err := db.AutoMigrate(&User{})
if err != nil {
panic("迁移失败: " + err.Error())
}
注意:自动迁移不会删除或更改已有的列类型(出于安全),仅会添加缺失的字段、索引等。生产环境中建议使用专业的数据库迁移工具。
基本 CRUD 操作
创建记录
Go
user := User{Name: "张三", Email: "zhangsan@example.com", Age: 28}
result := db.Create(&user)
if result.Error != nil {
fmt.Println("插入失败:", result.Error)
}
fmt.Printf("插入记录的ID: %d, 影响行数: %d\n", user.ID, result.RowsAffected)
批量插入可以使用切片:
Go
users := []User{
{Name: "李四", Email: "lisi@example.com"},
{Name: "王五", Email: "wangwu@example.com"},
}
db.Create(&users)
查询记录
获取单条记录:
Go
var user User
// 根据主键获取
db.First(&user, 1) // SELECT * FROM users WHERE id = 1 ORDER BY id LIMIT 1;
// 根据条件获取
db.First(&user, "name = ?", "张三") // 占位符防注入
// 获取最后一条记录
db.Last(&user)
// 获取一条记录,不排序
db.Take(&user)
获取多条记录:
Go
var users []User
db.Where("age > ?", 20).Find(&users)
// 等价于 SELECT * FROM users WHERE age > 20;
链式条件查询:
Go
db.Where("name LIKE ?", "%张%").Or("email LIKE ?", "%@example.com").Find(&users)
db.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)
db.Where("name IN ?", []string{"张三", "李四"}).Find(&users)
选择特定字段:
Go
var names []string
db.Model(&User{}).Pluck("name", &names) // 提取 name 列到切片
更新记录
更新单个字段:
Go
db.Model(&User{}).Where("id = ?", 1).Update("age", 29)
// 或使用结构体实例
user := User{ID: 1}
db.Model(&user).Update("email", "newemail@example.com")
更新多个字段:
Go
db.Model(&User{}).Where("id = ?", 1).Updates(User{Name: "张小三", Age: 30}) // 非零值更新
db.Model(&User{}).Where("id = ?", 1).Updates(map[string]interface{}{"age": 31, "email": "zx@example.com"}) // 任意值更新
使用 Save 保存完整的结构体:
会更新所有字段,即使字段为零值也会更新。
Go
var user User
db.First(&user, 1)
user.Name = "新名字"
db.Save(&user)
删除记录
软删除 :如果模型包含 gorm.DeletedAt 字段,删除操作只会标记删除时间,不会真正删除数据。
Go
type User struct {
ID uint
Name string
DeletedAt gorm.DeletedAt // 引入软删除
}
// 删除 ID 为 1 的用户
db.Delete(&User{}, 1) // 实际执行 UPDATE users SET deleted_at = NOW() WHERE id = 1;
物理删除 :使用 Unscoped() 可以强制物理删除。
Go
db.Unscoped().Delete(&User{}, 1)
高级查询
分页与排序
Go
var users []User
db.Order("age desc").Offset(10).Limit(5).Find(&users) // 按年龄降序,跳过10条,取5条
分组与聚合
Go
type Result struct {
Dept string
Count int
}
var results []Result
db.Model(&User{}).Select("department, count(*) as count").Group("department").Having("count > ?", 2).Find(&results)
原生 SQL
GORM 也支持执行原生 SQL 并将结果映射到结构体。
Go
var user User
db.Raw("SELECT * FROM users WHERE name = ?", "张三").Scan(&user)
// 执行非查询 SQL
db.Exec("UPDATE users SET age = age + 1 WHERE id IN ?", []int{1,2,3})
关联关系
GORM 支持一对一、一对多、多对多等关联关系,并通过预加载(Preload)极大简化了多表查询。
假设有以下模型:一个用户拥有多张信用卡,一个信用卡属于一个用户。
Go
type User struct {
ID uint
Name string
CreditCards []CreditCard // 一对多
}
type CreditCard struct {
ID uint
Number string
UserID uint // 外键
}
自动关联查询
Go
var user User
db.Preload("CreditCards").First(&user, 1) // 查询用户同时加载其信用卡
关联模式
Go
// 添加关联
user := User{ID: 1}
card := CreditCard{Number: "4111111111111111"}
db.Model(&user).Association("CreditCards").Append(&card)
// 替换关联
db.Model(&user).Association("CreditCards").Replace([]CreditCard{card1, card2})
// 删除关联
db.Model(&user).Association("CreditCards").Delete(&card)
// 计数
db.Model(&user).Association("CreditCards").Count()
多对多
多对多需要通过中间表,GORM 会自动处理。
Go
type User struct {
ID uint
Name string
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
ID uint
Name string
}
操作同样方便:
Go
user := User{Name: "张三"}
lang := Language{Name: "Go"}
db.Model(&user).Association("Languages").Append(&lang)
事务处理
GORM 提供事务支持,确保数据操作的原子性。
Go
err := db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行操作,使用 tx 而不是 db
if err := tx.Create(&user1).Error; err != nil {
return err // 发生错误自动回滚
}
if err := tx.Delete(&user2).Error; err != nil {
return err
}
return nil // 提交事务
})
if err != nil {
// 处理事务失败
}
也可以手动控制事务:
Go
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
}
钩子函数(Hooks)
GORM 提供了丰富的生命周期钩子,允许在创建、查询、更新、删除等操作前后自定义逻辑。
常用的钩子:
-
BeforeCreate,AfterCreate -
BeforeUpdate,AfterUpdate -
BeforeDelete,AfterDelete -
AfterFind
Go
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
// 在插入前对密码加密等
u.Name = strings.ToUpper(u.Name)
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.ID == 1 {
return tx.Statement.Error // 可以终止操作
}
return
}
钩子中的 tx.Statement 会返回当前操作的上下文,如果钩子返回错误,GORM 会阻止后续操作并回滚事务(如果在事务内)。
数据库连接池与性能优化
实际项目中,合理配置连接池非常重要:
Go
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10) // 空闲连接池中的最大连接数
sqlDB.SetMaxOpenConns(100) // 数据库的最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接可被复用的最大时间
另外,建议开启 GORM 的预编译缓存,能提升重复查询的性能:
Go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
PrepareStmt: true, // 开启预编译语句缓存
})
实战技巧与注意事项
-
避免 N+1 查询 :查询关联数据时尽量使用
Preload或Joins,减少循环查询。 -
零值更新问题 :使用
Updates结构体时,零值字段(0、false、空字符串)会被忽略,若需更新零值,改用map或Select指定字段。 -
安全占位符 :永远使用
?占位符传递查询条件,避免拼接 SQL 造成注入风险。 -
表名自定义 :可以通过实现
Tabler接口或使用Scopes来动态修改表名。Gofunc (User) TableName() string { return "custom_users" } -
日志模式 :开发时可通过
db.Debug()查看实际执行的 SQL,方便排查问题。 -
连接重试 :GORM 本身不提供自动重连,建议结合
database/sql的连接健康检查和重试逻辑,或使用gorm.io/driver/mysql的WithConnPool自定义连接池。
总结
GORM 几乎涵盖了日常数据库开发中的所有场景,从简单的 CRUD 到复杂的关联、事务、钩子,都能用清晰简洁的 Go 代码实现。它的活跃社区和完善的中文文档,使得学习成本很低,非常适合中小型项目以及需要快速迭代的产品。
当然,任何 ORM 都无法完全替代原生 SQL,对于复杂报表、性能极致的场景,你可能仍需要手写 SQL。但在绝大多数业务逻辑中,GORM 能显著提升开发效率,并保持代码的可维护性。