GORM 使用教程:从入门到实战
在 Go 项目中,如果你需要操作 MySQL、PostgreSQL、SQLite、SQL Server 等关系型数据库,除了直接使用标准库 database/sql,还可以使用 ORM 框架来提高开发效率。
GORM 是 Go 生态中非常流行的 ORM 库,它提供了:
- 模型映射
- 自动迁移
- CRUD 操作
- 关联关系
- 事务
- 钩子函数
- 软删除
- 预加载
- 链式查询
- Context 支持
本文以 GORM v2 为基础,从安装、连接数据库、定义模型、CRUD、关联查询到事务和最佳实践,带你完整入门 GORM。
一、GORM 是什么?
ORM 的全称是 Object Relational Mapping,也就是对象关系映射。
简单来说,ORM 可以让我们用 Go 结构体来表示数据库表,用结构体对象来表示数据库记录。
例如一张 users 表:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
age INT
);
在 GORM 中可以定义为:
type User struct {
ID uint
Name string
Email string
Age int
}
之后就可以通过 Go 代码完成查询、插入、更新和删除,而不需要手写大量 SQL。
二、安装 GORM
创建 Go 项目:
mkdir gorm-demo
cd gorm-demo
go mod init gorm-demo
安装 GORM 核心库:
go get -u gorm.io/gorm
根据你使用的数据库安装对应驱动。
SQLite
SQLite 适合本地学习和小型项目:
go get -u gorm.io/driver/sqlite
MySQL
go get -u gorm.io/driver/mysql
PostgreSQL
go get -u gorm.io/driver/postgres
三、连接数据库
1. 连接 SQLite
SQLite 不需要单独启动数据库服务,最适合入门演示。
package main
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库失败:", err)
}
_ = db
}
运行后,当前目录会生成一个 test.db 文件。
2. 连接 MySQL
package main
import (
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("连接 MySQL 失败:", err)
}
_ = db
}
MySQL DSN 常见参数说明:
| 参数 | 说明 |
|---|---|
root:123456 | 用户名和密码 |
tcp(127.0.0.1:3306) | 数据库地址和端口 |
gorm_demo | 数据库名称 |
charset=utf8mb4 | 支持完整 Unicode,包括 emoji |
parseTime=True | 自动解析时间类型 |
loc=Local | 使用本地时区 |
3. 连接 PostgreSQL
package main
import (
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
dsn := "host=localhost user=postgres password=123456 dbname=gorm_demo port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("连接 PostgreSQL 失败:", err)
}
_ = db
}
四、定义模型
GORM 使用 Go 结构体表示数据库表。
type User struct {
ID uint
Name string
Email string
Age int
}
默认情况下:
- 结构体
User会映射到表users - 字段
Name会映射到列name - 字段
CreatedAt、UpdatedAt会自动维护时间 - 字段
ID默认会作为主键
使用 gorm.Model
GORM 提供了一个内置结构体 gorm.Model:
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
你可以直接嵌入它:
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string
Email string
Age int
}
这样 User 表中会自动拥有:
idcreated_atupdated_atdeleted_at
其中 deleted_at 用于软删除。
五、常用字段标签
GORM 通过结构体标签控制字段映射。
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
Age int `gorm:"default:18"`
Password string `gorm:"column:password_hash"`
}
常见标签:
| 标签 | 作用 |
|---|---|
primaryKey | 主键 |
column:name | 指定列名 |
type:varchar(100) | 指定数据库类型 |
size:100 | 指定字段长度 |
not null | 非空 |
default:18 | 默认值 |
uniqueIndex | 唯一索引 |
index | 普通索引 |
autoIncrement | 自增 |
- | 忽略该字段 |
例如忽略字段:
type User struct {
ID uint
Name string
TempData string `gorm:"-"`
}
TempData 不会映射到数据库。
六、自动迁移 AutoMigrate
AutoMigrate 可以根据模型自动创建或更新表结构。
err := db.AutoMigrate(&User{})
if err != nil {
log.Fatal(err)
}
完整示例:
package main
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
Age int
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
if err := db.AutoMigrate(&User{}); err != nil {
log.Fatal(err)
}
}
需要注意:
AutoMigrate会创建不存在的表、字段、索引和约束AutoMigrate会尽量安全地更新字段结构- 为了保护数据,
AutoMigrate不会自动删除数据库中已经存在但模型里删除了的字段 - 生产环境中建议使用专门的数据库迁移工具管理复杂变更
七、新增数据 Create
1. 插入一条记录
user := User{
Name: "张三",
Email: "zhangsan@example.com",
Age: 20,
}
result := db.Create(&user)
if result.Error != nil {
log.Fatal(result.Error)
}
fmt.Println("新用户 ID:", user.ID)
fmt.Println("影响行数:", result.RowsAffected)
GORM 插入成功后,会把数据库生成的主键回填到 user.ID。
2. 批量插入
users := []User{
{Name: "张三", Email: "zhangsan@example.com", Age: 20},
{Name: "李四", Email: "lisi@example.com", Age: 22},
{Name: "王五", Email: "wangwu@example.com", Age: 25},
}
result := db.Create(&users)
if result.Error != nil {
log.Fatal(result.Error)
}
也可以指定批次大小:
db.CreateInBatches(users, 100)
八、查询数据 Query
1. 根据主键查询
var user User
result := db.First(&user, 1)
if result.Error != nil {
log.Fatal(result.Error)
}
fmt.Println(user.Name)
First 会按照主键升序取第一条记录。
常用查询方法:
| 方法 | 说明 |
|---|---|
First | 查询第一条记录,按照主键升序 |
Last | 查询最后一条记录,按照主键降序 |
Take | 查询一条记录,不指定排序 |
Find | 查询多条记录 |
2. 处理记录不存在
var user User
err := db.First(&user, "email = ?", "notfound@example.com").Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("用户不存在")
} else if err != nil {
log.Fatal(err)
}
需要导入:
import (
"errors"
"gorm.io/gorm"
)
3. 条件查询
var users []User
db.Where("age >= ?", 18).Find(&users)
多个条件:
db.Where("age >= ? AND name LIKE ?", 18, "%张%").Find(&users)
结构体条件:
db.Where(&User{Name: "张三"}).Find(&users)
Map 条件:
db.Where(map[string]any{
"name": "张三",
"age": 20,
}).Find(&users)
4. 排序和分页
var users []User
db.Where("age >= ?", 18).
Order("created_at DESC").
Limit(10).
Offset(20).
Find(&users)
这通常用于分页:
func ListUsers(db *gorm.DB, page, pageSize int) ([]User, error) {
if page < 1 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
var users []User
offset := (page - 1) * pageSize
err := db.Order("id DESC").
Limit(pageSize).
Offset(offset).
Find(&users).Error
return users, err
}
5. 只查询部分字段
var users []User
db.Select("id", "name", "email").Find(&users)
九、更新数据 Update
1. 更新单个字段
var user User
db.First(&user, 1)
result := db.Model(&user).Update("age", 21)
if result.Error != nil {
log.Fatal(result.Error)
}
2. 更新多个字段
使用结构体:
db.Model(&user).Updates(User{
Name: "张三三",
Age: 22,
})
使用 map:
db.Model(&user).Updates(map[string]any{
"name": "张三三",
"age": 22,
})
3. 结构体更新的零值问题
使用结构体更新时,GORM 默认只更新非零值字段。
db.Model(&user).Updates(User{
Name: "",
Age: 0,
})
上面代码中的 Name 和 Age 都是零值,默认不会被更新。
如果确实要更新零值,可以使用 map:
db.Model(&user).Updates(map[string]any{
"name": "",
"age": 0,
})
也可以使用 Select 指定字段:
db.Model(&user).
Select("name", "age").
Updates(User{Name: "", Age: 0})
十、删除数据 Delete
1. 根据主键删除
db.Delete(&User{}, 1)
也可以先查询再删除:
var user User
db.First(&user, 1)
db.Delete(&user)
2. 条件删除
db.Where("age < ?", 18).Delete(&User{})
3. 软删除
如果模型中包含 gorm.DeletedAt 字段,或者嵌入了 gorm.Model,GORM 默认会执行软删除。
type User struct {
gorm.Model
Name string
}
执行:
db.Delete(&user)
实际并不会立刻删除数据库记录,而是更新 deleted_at 字段。
普通查询会自动忽略软删除数据:
db.Find(&users)
如果要查询包含软删除的数据:
db.Unscoped().Find(&users)
如果要物理删除:
db.Unscoped().Delete(&user)
4. 防止全表删除
GORM 默认会阻止没有条件的批量删除:
db.Delete(&User{})
这类操作通常会返回错误,避免误删全表。
如果你确实要删除全部数据,应该显式写出条件:
db.Where("1 = 1").Delete(&User{})
十一、关联关系
实际项目中,表与表之间经常存在关联关系。
GORM 支持:
- Belongs To
- Has One
- Has Many
- Many To Many
1. Belongs To:用户属于公司
type Company struct {
ID uint
Name string
}
type User struct {
ID uint
Name string
CompanyID uint
Company Company
}
这里 User 中的 CompanyID 是外键,表示用户属于某家公司。
可以添加约束:
type User struct {
ID uint
Name string
CompanyID uint
Company Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
2. Has One:用户有一个资料页
type User struct {
ID uint
Name string
Profile Profile
}
type Profile struct {
ID uint
UserID uint
Bio string
}
3. Has Many:用户有多篇文章
type User struct {
ID uint
Name string
Articles []Article
}
type Article struct {
ID uint
UserID uint
Title string
Body string
}
4. Many To Many:用户拥有多个角色
type User struct {
ID uint
Name string
Roles []Role `gorm:"many2many:user_roles;"`
}
type Role struct {
ID uint
Name string
}
many2many:user_roles; 表示中间表名为 user_roles。
迁移时:
db.AutoMigrate(&User{}, &Role{})
GORM 会自动创建用户表、角色表以及中间表。
十二、预加载 Preload
默认情况下,GORM 查询主表时不会自动查询关联数据。
例如:
var user User
db.First(&user, 1)
此时 user.Articles 通常是空的。
如果要同时查询关联数据,可以使用 Preload。
1. 预加载一层关联
var user User
err := db.Preload("Articles").First(&user, 1).Error
if err != nil {
log.Fatal(err)
}
2. 预加载多个关联
var users []User
db.Preload("Articles").
Preload("Profile").
Preload("Roles").
Find(&users)
3. 预加载时添加条件
var users []User
db.Preload("Articles", "status = ?", "published").Find(&users)
4. 嵌套预加载
var users []User
db.Preload("Articles.Comments").Find(&users)
这表示查询用户时,同时加载文章和文章下的评论。
十三、事务 Transaction
涉及多步写入时,应该使用事务。
例如:创建订单时,需要同时创建订单和扣减库存。如果其中一步失败,所有操作都应该回滚。
1. 使用 db.Transaction
err := db.Transaction(func(tx *gorm.DB) error {
order := Order{
UserID: 1,
Amount: 19900,
}
if err := tx.Create(&order).Error; err != nil {
return err
}
if err := tx.Model(&Product{}).
Where("id = ? AND stock > 0", 1).
Update("stock", gorm.Expr("stock - ?", 1)).Error; err != nil {
return err
}
return nil
})
if err != nil {
log.Fatal("事务执行失败:", err)
}
在 Transaction 回调中:
- 返回
nil:提交事务 - 返回
error:回滚事务
2. 手动事务
tx := db.Begin()
if tx.Error != nil {
log.Fatal(tx.Error)
}
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
log.Fatal(err)
}
if err := tx.Create(&profile).Error; err != nil {
tx.Rollback()
log.Fatal(err)
}
if err := tx.Commit().Error; err != nil {
log.Fatal(err)
}
3. 保存点 SavePoint
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1")
tx.Commit()
RollbackTo("sp1") 会回滚到保存点,但不会回滚整个事务。
十四、Context 支持
在 Web 服务中,数据库操作通常需要支持超时和取消。
GORM 支持通过 WithContext 传入 context.Context。
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var users []User
err := db.WithContext(ctx).
Where("age >= ?", 18).
Find(&users).Error
if err != nil {
log.Fatal(err)
}
在 HTTP 服务中,通常使用请求自带的 context:
func GetUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var users []User
if err := db.WithContext(ctx).Find(&users).Error; err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 返回 JSON ...
}
十五、钩子 Hooks
GORM 支持在创建、查询、更新、删除前后执行钩子函数。
常见钩子:
| 钩子 | 执行时机 |
|---|---|
BeforeCreate | 创建前 |
AfterCreate | 创建后 |
BeforeUpdate | 更新前 |
AfterUpdate | 更新后 |
BeforeDelete | 删除前 |
AfterDelete | 删除后 |
AfterFind | 查询后 |
例如创建用户前自动设置 UUID:
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.UUID == "" {
u.UUID = uuid.NewString()
}
return nil
}
钩子中也可以读取 context:
func (u *User) BeforeCreate(tx *gorm.DB) error {
ctx := tx.Statement.Context
_ = ctx
return nil
}
注意:钩子适合处理和模型强相关的逻辑,不建议在钩子中写复杂业务流程。
十六、Scopes:封装可复用查询条件
Scopes 可以把常用查询条件封装成函数。
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "active")
}
func MinAge(age int) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("age >= ?", age)
}
}
使用:
var users []User
err := db.Scopes(ActiveUsers, MinAge(18)).Find(&users).Error
if err != nil {
log.Fatal(err)
}
Scopes 很适合封装:
- 分页
- 多租户条件
- 状态过滤
- 权限过滤
- 公共排序规则
分页也可以封装为 Scope:
func Paginate(page, pageSize int) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page < 1 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
使用:
db.Scopes(Paginate(2, 10)).Find(&users)
十七、日志和配置
1. 开启 SQL 日志
调试时可以开启 GORM 日志:
import (
"log"
"os"
"time"
"gorm.io/gorm/logger"
)
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})
2. 跳过默认事务
GORM 默认会在写操作时使用事务,以保证数据一致性。
如果你非常确定业务场景不需要默认事务,可以开启:
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
这样可能提升写入性能,但会降低默认安全性,生产环境需要谨慎开启。
3. 开启预编译语句缓存
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
PrepareStmt: true,
})
也可以按 Session 开启:
tx := db.Session(&gorm.Session{PrepareStmt: true})
tx.Find(&users)
十八、GORM 泛型 API 简介
较新的 GORM 版本提供了泛型 API,例如:
ctx := context.Background()
err := gorm.G[User](db).Create(ctx, &User{Name: "Alice"})
if err != nil {
log.Fatal(err)
}
user, err := gorm.G[User](db).
Where("name = ?", "Alice").
First(ctx)
if err != nil {
log.Fatal(err)
}
err = gorm.G[User](db).
Where("id = ?", user.ID).
Update(ctx, "age", 18)
if err != nil {
log.Fatal(err)
}
泛型 API 的特点是:
- 类型更明确
- 方法通常显式接收
context.Context - 返回值更贴近普通 Go 函数风格
不过目前很多项目仍然大量使用传统链式 API。入门阶段建议先掌握传统 API,再根据团队习惯选择是否使用泛型 API。
十九、完整示例:用户管理
下面是一个可以直接运行的 SQLite 示例。
package main
import (
"errors"
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
Age int
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库失败:", err)
}
if err := db.AutoMigrate(&User{}); err != nil {
log.Fatal("迁移失败:", err)
}
user := User{
Name: "张三",
Email: "zhangsan@example.com",
Age: 20,
}
if err := db.Create(&user).Error; err != nil {
log.Fatal("创建用户失败:", err)
}
fmt.Println("创建用户成功,ID:", user.ID)
var found User
err = db.First(&found, "email = ?", "zhangsan@example.com").Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("用户不存在")
return
}
if err != nil {
log.Fatal("查询失败:", err)
}
fmt.Println("查询用户:", found.Name, found.Age)
if err := db.Model(&found).Update("age", 21).Error; err != nil {
log.Fatal("更新失败:", err)
}
var users []User
if err := db.Where("age >= ?", 18).Order("id DESC").Find(&users).Error; err != nil {
log.Fatal("列表查询失败:", err)
}
fmt.Println("用户数量:", len(users))
if err := db.Delete(&found).Error; err != nil {
log.Fatal("删除失败:", err)
}
fmt.Println("删除用户成功")
}
运行:
go run main.go
二十、项目中推荐的分层方式
在真实项目中,不建议到处直接使用全局 db 操作数据库。更常见的方式是封装 Repository。
目录示例:
project/
├── main.go
├── internal/
│ ├── model/
│ │ └── user.go
│ ├── repository/
│ │ └── user_repository.go
│ └── service/
│ └── user_service.go
model/user.go
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string
Email string `gorm:"uniqueIndex"`
Age int
}
repository/user_repository.go
package repository
import (
"context"
"gorm.io/gorm"
"project/internal/model"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *UserRepository) FindByID(ctx context.Context, id uint) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) List(ctx context.Context, page, pageSize int) ([]model.User, error) {
var users []model.User
if page < 1 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
err := r.db.WithContext(ctx).
Order("id DESC").
Offset(offset).
Limit(pageSize).
Find(&users).Error
return users, err
}
这种写法的好处是:
- 数据库访问集中管理
- 业务层不直接依赖复杂 SQL 细节
- 方便单元测试
- 方便后续替换数据库实现
二十一、常见坑和最佳实践
1. 永远检查 Error
不要只写:
db.Create(&user)
推荐:
if err := db.Create(&user).Error; err != nil {
return err
}
如果需要影响行数:
result := db.Create(&user)
if result.Error != nil {
return result.Error
}
fmt.Println(result.RowsAffected)
2. 更新零值时注意 Updates 行为
结构体更新默认忽略零值。
db.Model(&user).Updates(User{Age: 0})
如果要把年龄更新为 0,用 map 或 Select。
db.Model(&user).Updates(map[string]any{"age": 0})
3. 谨慎使用 Save
Save 会保存所有字段,容易在不注意时覆盖数据。部分更新更推荐使用:
db.Model(&user).Updates(map[string]any{
"name": user.Name,
"age": user.Age,
})
4. 不要忽略 Context
在 Web 项目中,数据库操作应该使用请求的 context。
db.WithContext(r.Context()).Find(&users)
这样请求取消或超时时,数据库操作也有机会被取消。
5. 生产环境谨慎使用 AutoMigrate
AutoMigrate 很适合开发阶段快速迭代,但生产环境中,复杂表结构变更最好使用迁移工具,并经过审核和回滚设计。
6. 避免 N+1 查询
如果你在循环里查询关联数据:
for _, user := range users {
db.Where("user_id = ?", user.ID).Find(&user.Articles)
}
就可能产生 N+1 查询问题。
推荐使用 Preload:
db.Preload("Articles").Find(&users)
7. 明确选择字段
接口列表通常不需要查询所有字段,可以使用 Select 降低数据传输量。
db.Select("id", "name", "email").Find(&users)
8. 不要拼接用户输入到 SQL 字符串
错误示例:
db.Where("name = " + name).Find(&users)
推荐使用参数绑定:
db.Where("name = ?", name).Find(&users)
这样可以减少 SQL 注入风险。
二十二、总结
本文介绍了 GORM 的核心用法:
- 安装 GORM 和数据库驱动
- 连接 SQLite、MySQL、PostgreSQL
- 定义模型和字段标签
- 使用
AutoMigrate自动迁移 - 使用
Create、First、Find、Updates、Delete完成 CRUD - 使用关联关系和
Preload查询关联数据 - 使用事务保证多步写入一致性
- 使用
WithContext支持超时和取消 - 使用 Hooks 和 Scopes 封装通用逻辑
- 了解日志、性能配置和常见坑
如果你是 Go 初学者,建议先掌握传统链式 API;如果你已经熟悉 Go 泛型,也可以进一步尝试 GORM 的泛型 API。
GORM 的优势是开发效率高、功能完整、生态成熟;但在复杂查询、高性能写入和严格数据库变更场景下,仍然需要理解 SQL 本身,不能完全依赖 ORM。
参考资料
- GORM 官方文档:https://gorm.io/docs/
- GORM GitHub:https://github.com/go-gorm/gorm