rows。err := db.QueryContext(ctx,"select id,name from users where status =?","active")
if err!= nil {
return err
}
defer rows.Close
for rows.Next {
var id int64
var name string
if err := rows.Scan;err,其实,= nil {
return err
}
fmt.Println
}
优点这方面。SQL 完全可控,说到缺点,样板代码多,容易遗漏错误检查。
2️⃣ ORM —— GORM
var users User
err := db.WithContext.
Where.
Find.Error
go get gorm.io/gorm # 主要包
go get gorm.io/driver/sqlite # SQLite 示例
go get gorm.io/driver/mysql # MySQL 驱动
go get gorm.io/driver/postgres # PostgreSQL 驱动
go get gorm.io/driver/sqlserver # SQL Server 驱动
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primaryKey"`
Code string `gorm:"uniqueIndex;not null"`
Price uint `gorm:"not null"`
}
func main {
db。err := gorm.Open,&gorm.Config{})
if err!= nil {
log.Fatal
}
// 自动迁移——只在 demo / 原型阶段使用
if err := db.AutoMigrate;err,= nil {
log.Fatal
}
// 创建记录
db.Create
var product Product
if err := db.First.Error;err,= nil {
log.Fatal
}
fmt.Printf
}
Pain point 对应解释:
No boilerplate:`AutoMigrate` + `Create` + `First` 一行搞定模型创建与查询。
Error handling is explicit:`if err!= nil` 确保每一步都被捕获,避免隐藏异常。
*gorm.DB 并非单纯的数据库连接。而是 GORM 的操作入口对象 ,包含:
: 连接池与驱动交互。
建立中的 SQL 片段、占位符和值集合。
Logger、Session 参数等全局选项。
错误信息 .Error。受影响行数 .RowsAffected,回调链等元数据。
result := db.Where.Find // result 一样是 *gorm.DB
if result.Error!= nil {
// 错误处理…}
fmt.Println
SQLite 示例
db。err := gorm.Open,&gorm.Config{})
if err!= nil { log.Fatal }
...
\* 注意:若想自定义表名,只需实现 TableName 方法或使用 tag。go
func TableName string { return "app_users" }
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:user_name"` // 自定义列名
}
### 常用 Tag
| Tag | 含义 |
|-----|------|
| `primaryKey` | 主键 |
| `column:` | 指定列名 |
| `size:` | 字段长度 |
| `not null` | 非空约束 |
| `default:` | 默认值 |
| `uniqueIndex` | 唯一索引 |
| `index` | 普通索引 |
| `autoCreateTime` / `autoUpdateTime` | 自动维护时间戳 |
## gorm.Model 与自定义基类
go
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time // 自动填充创建时间
UpdatedAt time.Time // 自动填充更新时间
DeletedAt gorm.DeletedAt `gorm:"index"` // 支持软删
}
**使用方式**
go
type User struct {
gorm.Model // 嵌入后拥有 ID、CreatedAt…等字段
Name string // 自己的业务字段
}
如果项目对主键类型或审计字段有特殊需求,可自行声明基类:
go
type BaseModel struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
type User struct { BaseModel;Name string }
## AutoMigrate:快速原型 VS 正式迁移
go
err := db.AutoMigrate
- **适用**:本地 demo、小型内部工具。- **不适用**:生产环境需要审计、回滚和灰度发布时建议使用专门迁移工具。---
## CRUD 操作精选
### Create – 新增数据
go
user := User{Name:"张三",Email:""}
if err:=db.Create.Error;err,=nil { return err }
fmt.Println // 主键自动回填
#### 批量插入
go
说到users。=User{{Name:"张三"},{Name:"李四"}}
if err:=db.Create.Error;err,=nil {return err}
#### 部分列插入
go
db.Select.Create // 只插入指定列
db.Omit.Create // 忽略指定列
### Read – 查询族
#### 单条查询差异
| 方法 | 用途 |
|------|------|
| **First** | 按主键升序取第一条;不过,找不到返回 `Error=ErrRecordNotFound` .|
| **Last** | 按主键降序取第一条;其实,同上.|
| **Take** | 不指定排序直接取一条;找不到返回 `Error=ErrRecordNotFound` .|
| **Find** | 查询多条;即使为空也不返回错误.|
示例的观点是,go
var u User
至于err,=db.First.Error // 按 ID 查找
再看err,=db.Where.Take.Error // 任意条件取一条
err的观点是。=db.Where.Find.Error // 列表查询
#### 条件构造
go
// 字符串 + 参数占位符
db.Where.Find
// map 条件
db.Where.Find
// IN 查询 & LIKE 示例:
db.Where.Find
db.Where.Find
// 范围查询:
db.Where.Find
### Update – 更新族
go
// 单字段更新:
db.Model.Update
// 使用结构体更新多字段
db.Model.Updates // Age 为零不会被更新!老实说,// 必须更新零值时用 map 或 Select 明确:
db.Model.Updates
db.Model.Select.Updates
### Delete 与软删除
go
// 普通物理删除:
db.Delete
// 含 DeletedAt 字段时自动软删:
type User struct{ ID uint;DeletedAt gorm.DeletedAt `gorm:"index"` }
db.Delete // 实际执行 UPDATE SET deleted_at=NOW
db.Unscoped.Delete // 强制物理删除
// 查询时排除软删记录:
var users User;db.Find
// 包含软删记录:
db.Unscoped.Find
---
## 字段控制:Select 与 Omit
go
// 查询仅返回 ID 与 Name:
var users User;db.Select.Find
// 更新仅修改指定列:
db.Model.Select.Updates
// 插入时忽略密码:
db.Omit.Create
---
## 分页技巧
go
page。size := 1,20 // 从请求拿到的分页参数
offset := *size
从query来看,= db.Model.Where
var total int64;query.Count // 总数统计
var list User;
query.Order.
Limit.
Offset.
Find
⚠️ **注意**:链式调用会保留之前的状态,分页前优先使用 `.Session` 创建干净会话。---
## Scopes – 重复查询抽象
go
func ActiveOnly*gorm.DB{ return db.Where }
func ByKeyword func*gorm.DB{
return func*gorm.DB{
if k==""{return db}
return db.Where
}}
...
db.Scopes).Find
分页也可以抽象为 Scope:
go
func Paginate func*gorm.DB{
return func*gorm.DB{
if page<1{page=1}
if size<=0||size>200{size=20}
return db.Offset*size).Limit
}}
...
db.Scopes).Find
---
## 大批量遍历 – FindInBatches
适用于一次性加载会导致 OOM 的场景:
go
var batch User
err这方面。= db.Where.
FindInBatches error{
for _,u:=range batch{ fmt.Println}
return nil })
---
## 关联关系实战
### 一对多 – 使用者 ↔️订单
go
type User struct{ ID uint;Name string,Orders Order }
type Order struct{ ID uint;UserID uint,No string }
# 创建带关联的数据:
u:=User{Name:"张三",Orders:Order{{No:"O001"}。{No:"O002"}}}
db.Create
# Preload 加载关联
var users User;db.Preload.Find
### 多对多 – 文章 ↔️标签
go
type Article struct{ ID uint;按理说,Title string;Tags Tag `gorm:"many2many:article_tags;"`}
type Tag struct{ ID uint;Name string }
article:=Article{Title:"GORM 入门",Tags:Tag{{Name:"Go"}。{Name:"ORM"}}}
db.Create
# 查询并预加载标签:
var arts Article;老实说,db.Preload.Find
*当中间表需要额外属性时请自行声明对应模型。而不是仅靠 many‑many 自动生成。*
---
## Transaction 使用教程
### 简单事务模板
go
至于err,= db.Transaction error{
if err:=tx.Create.Error;err,=nil {return err}
if err:=tx.Model.
Where.
Update).Error;err,=nil{
return err}
return nil // 返回 nil 提交事务。否则回滚
})
### 手动事务 + 保存点
go
再看tx,= db.Begin
defer func{ if tx.Error!=nil { tx.Rollback } }
if err:=tx.Create;err,=nil{ tx.Rollback;return err }
tx.SavePoint
if err:=tx.Create.Error;err,=nil{
tx.RollbackTo // 日志失败不影响订单提交
}
return tx.Commit.Error // 最终统一提交
*日志类非主要数据推荐异步写而非阻塞在主事务内。不过,*
---
## Context 跨层传播
go
func FindByID{
var u User
if err:= r.db.WithContext.First.Error;err,=nil{return u,err}
return u,nil
}
// 调用方:
至于ctx,_,=context.WithTimeout。5*time.Second)
repo.FindByID
*所有 DB 调用均应附带请求上下文,以实现取消、超时及链路追踪。*
---
## Logger 与 Debug
- 开发阶段可直接打开 Debug 模式:
go
dbg := db.Debug // 每次执行都会打印完整 SQL + 参数
- 推荐在生产环境通过 logger 配置细粒度输出:
go
import "golang.org/x/time/rate"
db。_ = gorm.Open,&gorm.Config{
再看Logger,logger.Default.
LogMode.
SetIgnoreRecordNotFoundError,})
*避免将所有 SQL 日志直接打印到标准输出,建议设置慢查询阈值或脱敏过滤后再记录。*
---
## DryRun – 查看生成的 SQL 而不执行
go
stmt := db.Session.
Where.
First.Statement
fmt.Println) // 完整 SQL 文本
fmt.Println // 对应占位符参数数组
*非常适合调试复杂链式构造还有验证 GORM 自动生成的 JOIN / 子查询是否符合业务需求。*
---
## Hook – 模型生命周期钩子
go
func BeforeCreate error{
if u.Email=="" {return errors.New}
u.CreatedAt=time.Now
return nil // 返回 error 将阻止 INSERT 操作
}
*仅用于模型级别的轻量逻辑,如默认值补齐或审计信息填充;话说回来,业务流程请放到 Service 层,以免产生不可预期的副作用。*
---
## 实战 Demo:订单程序 Repository
**目录结构**
gorm‑shop/
├─ go.mod
└─ main.go # 演示代码,一键跑通
**完整代码**
点击展开源码 ↓
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"golang.org/x/sync/errgroup"
"g orm.io/driver/sqlite"
"g orm.io/g orm"
g orm."logg er"
)
/*** 基础模型 ***/
type BaseModel struct {
ID uint `g orm :"primaryKey"`
Create d At time.Time
Update d At time.Time
Delete d At g orm.DeletedAt `
g orm :"index"`
}
/*** 使用者 ***/
type User struct {
BaseModel //
Name string ``g orm :"size:64;" `
Email str ing ``g orm :"size :128;" `
Orders Order //
}
/*** 商品 ***/
type Product struct {
BaseModel //
Name string ``g orm :"size :128;" `
Stock int ``g orm :"not null;" `
PriceCent int64 ``g orm :"not null;" `
}
/*** 订单 ***/
type Order struct {
BaseModel //
OrderNo string ``g orm :"size :64;" `
UserID uint ``g orm :"index;" `
User User //
Status str ing ``g orm :"size :32;" `
TotalCent int64 ``g orm :"not null;" `
Items OrderItem //
}
/*** 订单明细 ***/
type OrderItem struct {
BaseModel //
OrderID uint ``g orm :"index;" `
ProductID uint ``g orm :"index;" `
Product Product //
Quantity int ``g orm :"not null;" `
PriceCent int64 ``g orm :"not null;" `
}
/*** DTOs ***/
type CreateOrderInput struct {
UserID ui nt
ProductI D int
Quantity i nt
}
/*** Repository 封装 ***/
t ype OrderRepo s truct { db *gmor.D B }
f unc NewOrderRepo Or derRepo { re turn Or derRepo{d b:d b}}
f unc Cre ateOr der(
Or der,error ){
if in pu t.Quantity <=0{return Or der{},fmt.Er ro rf }
var ord er Or de r
err : =r .d b.Wi thCon tex t.Tr ansactioner r or{
v ar us er U ser
i f er r : =t .Fi rs t .Er ro r ;
i f er r ! = n il && erro rs.Is{
ret urn fmt.Errorf
}else i f er r ! = n il &&er r ! =n il {ret urn er r}
v ar pro duct P rod u ct
i f er r : =t .Fi rs t .Er ro r ;
i f er r ! = n il && erro rs.Is{
ret urn fmt.Errorf
}else i f er r ! = n il &&er r ! =n il {ret urn er r}
i f pro duct.S toc k 200{s iz e=20}
q ue ry :=>r.d b.Wi thCo ntex tc.Mo del .
Wh ere
v ar tot al in tallint6
i f err::=q ue ry.Co unt .Er ror;if err,==n il{return ni l,nil,e rr}
v ar or dersOr deR
e rr :: q ue ry.OrDer .Lim it.
Off set *si ze ).
Fi nd .Er ror
return ors ers,t ot al,nil
}
/*** 打开内存 SQLite 数据库 *** */
f unc op enD B {re turn gmor.Op en。&gmor.C onfig{}) }
/*** AutoMigrate 示例,仅用于 Demo 环境 *** */
f unc mig rateerror{return d b.Auto Migrate }
/*** 种子数据 *** */
f unc see derror{
us erU ser={Nam:e:“Zhang san”,Em ail:“zhan gs an@ex ample.com ”}
i fe rr:g m or .Cr ea te.E rr or!==n il{return eer}
pr od uc ts=P rod uc ts {{Nam:e:“Me cha nc al”,Sto ck::10,Pri ceCe nt::1999},{Nam:e:“Wi reless mou se”。Sto ck::5,Pri ceCe nt::1499}}
r eturn dm.or C rea te .Err or
}
/*** 主函数演示完整业务流 *** */
f unc ma in {
d b,e rtun op enD B
i fe rr!={lo.g.Fata l}
i fe_rr:mig rate;e rr,={log.Fata l}
i fe_rr:s ee d;e rr,={log.Fata l}
cx,t canc el ): con text.Wit hTimeout,5*t ime.Se con ds)
de fer_ defer canc el
rep o :N ew Or der R epo
o rd ier。o_err:r ep.o CreateOrd erf.ID,Pro duct I D:dB.Fir stPr od uc T .Id,Qu antity.:5 })
i fe_rr!={lo.g.Fata l}
fmt.Println
or ders。tot al,_:,e_rR:r ep.L ist Or de rs
i fe_rR!={ lo.g.Fata l}
fmt.Printf
f OR _,ord ert.It em:s=:ord.er.It emS
fm.t.Println )
}
// 執行指令:
// go run main.go
---
### 主要思路拆解
#### ✅ 扣库存必须判断 RowsAffected
只有当 **WHERE stock>= qty** 成功匹配时才会更新。若并发扣减导致库存不足,RowsAffected 为 0 即可直接回滚,避免“先查后改”的竞态问题。#### ✅ Preload 多级关联
一次性把「订单 → 明细 → 商品」全部拉进内存,避免循环中
发起 N 次 SELECT。#### ✅ Transaction 内部只能使用 tx
任何 DB 操作都必须走同一个 Tx 实例,否则失去原子性。---
常见错误与规避技巧
E 错误场景
症状
E 根因分析
为什么出现?
E 推荐做法
如何改进?
No Error Check
Result 没有检查 Error 字段
Coding习惯导致忽略链式调用最终返回值
Avoid chaining without assigning result.
Always write:
.
"Not Found is not an error"
Using Find 而 Expect ErrRecordNotFound
The semantics of Find deliberately treat empty result as success.
If you need existence check use First/Take/Last,and handle ErrRecordNotFound explicitly.
"Save overwrites zero values"
Using Save for partial updates
"Save" writes all fields including zero values.
Avoid Save for partial updates.
Use Update / Updates with Select/Omit/
map arguments that explicitly list fields you intend to modify.
"DTO directly passed to Updates"
Zero-value fields become NULL in DB
Your request DTO uses value types -> zero means omitted but becomes actual zero when persisted.
- Use pointer fields in DTO to distinguish omitted vs zero.
- Build update map manually based on non-nil pointers.
N+1 problem with manual association queries
Lack of Preload leads to per‑row secondary query.
Avoid looping over parent rows and issuing new queries inside loop.
Use Preload or Join when you know you’ll need related data.
"Transaction mixing outer DB"
Using global DB inside Transaction closure
"Tx variable is ignored"You must keep all DB calls within same transaction object . Orwise operation won't be rolled back toger.
Use的观点是。`tx.xxx` everywhere inside closure.
"AutoMigrate in production"
Blindly let GORM change schema at runtime.
Will skip version control and rollback capability.
"Schema changes must be auditable"
"Use dedicated migration tool like Goose/Migrate/Liquibase."
Reserve AutoMigrate only for dev/prototype phases.
...
---