第42章:GORM 面试宝典与总复习
本章目标
学完本章后,你将能够:
- 系统回顾 GORM 所有知识点
- 自信回答 GORM 相关面试题
- 根据场景选择合适的技术方案
42.1 知识体系回顾
GORM v2 知识体系
├── 基础概念
│ ├── ORM 思想:struct ↔ 表
│ ├── 约定优于配置
│ └── 链式调用(Chain Method vs Finisher Method)
├── 模型定义(Phase 2)
│ ├── gorm.Model(ID/CreatedAt/UpdatedAt/DeletedAt)
│ ├── 字段标签(column/type/size/default/not null/index 等)
│ ├── 命名约定(表名复数蛇形、列名蛇形)
│ ├── AutoMigrate(只做加法不做减法)
│ └── Scanner/Valuer 接口
├── CRUD(Phase 3)
│ ├── Create/批量/UPSERT/FirstOrCreate
│ ├── First/Take/Last/Find(找不到行为差异)
│ ├── Where(字符串/结构体/Map)零值陷阱
│ ├── Update/Save/UpdateColumn 的选择
│ └── Delete/软删除/Unscoped
├── 高级查询(Phase 4)
│ ├── Order/Limit/Offset/分页
│ ├── Select/Group/Having/聚合
│ ├── Joins/InnerJoins
│ ├── 子查询/Raw/Exec/Scan
│ └── Scopes(可复用查询片段)
├── 关联关系(Phase 5)
│ ├── BelongsTo:外键在自己
│ ├── HasOne:外键在对方 + UNIQUE
│ ├── HasMany:外键在对方
│ ├── Many2Many:中间表
│ ├── Preload:解决 N+1(2条SQL)
│ └── Association 模式
├── 事务与钩子(Phase 6)
│ ├── Transaction(func){}/手动事务
│ ├── 悲观锁(FOR UPDATE)/乐观锁(版本号)
│ ├── 钩子生命周期
│ └── 密码加密/UUID 生成
└── 高级特性(Phase 7-8)
├── Gen 代码生成器
├── DBResolver(读写分离)
├── 连接池配置
├── SQL 注入防护
└── 测试(SQLite :memory:)42.2 高频面试题 30 题
基础题
Q1:GORM 零值陷阱是什么?如何避免?
A:GORM 在 Updates 和 Where 中会跳过零值字段(int 的 0、string 的 ""、bool 的 false)。解决方案:①用指针类型;②用 Map;③用 Select 强制指定字段。
Q2:Save vs Update vs Updates 的区别?
A:Save 全字段保存(根据主键判断 Insert/Update);Update 更新单列;Updates 更新多列(struct 零值跳过,map 不跳过)。日常推荐 Update/Updates,Save 有覆盖风险。
Q3:First vs Take vs Last vs Find 的区别?
A:First 按主键升序第一条;Take 随机一条;Last 按主键降序最后一条;Find 取所有。找不到时前三者返回 ErrRecordNotFound,Find 返回空切片不报错。
Q4:软删除原理?如何查询已删除记录?
A:软删除设置 deleted_at 时间戳而非真正 DELETE。普通查询自动过滤(WHERE deleted_at IS NULL)。
Unscoped()查询所有(包括已删除)。Unscoped().Delete()硬删除。
Q5:AutoMigrate 能做什么、不能做什么?
A:能创建表、添加列、添加索引。不能删除列、修改列名、删除索引。设计原则:只做加法不做减法,保护数据安全。
查询题
Q6:N+1 问题是什么?如何解决?
A:查询 N 条主记录后,循环中逐条查关联 → N+1 条 SQL。解决方案:Preload(2条SQL)或 Joins(1条SQL)。
Q7:Preload 和 Joins 的区别?
A:Preload 用多条 SQL 加载关联,自动填充结构体,适合纯展示。Joins 用一条 SQL(JOIN),适合需要关联条件过滤的场景。一对多/多对多必须用 Preload。
Q8:如何实现分页查询?
A:
db.Limit(pageSize).Offset((page-1)*pageSize).Find()+db.Count(&total)。大偏移量(翻页太深)可用游标分页(WHERE id > lastID)。
Q9:Where(struct) 和 Where(map) 有什么区别?
A:struct 零值被跳过(int 的 0、string 的 ""),map 不跳过。查询包含零值的条件用 map 或字符串条件。
Q10:如何防止 SQL 注入?
A:始终使用
?占位符(参数化查询),不拼接字符串。GORM 的 Where/Find/Create 等方法默认安全。Raw/Exec 也要用占位符。ORDER BY 等无法占位的参数要白名单校验。
关联题
Q11:HasOne vs BelongsTo 选哪个?
A:谁"归属"于对方 → BelongsTo(外键在自己);谁"拥有"对方 → HasOne(外键在对方)。User BelongsTo Company(外键 company_id 在 users),User HasOne CreditCard(外键 user_id 在 credit_cards)。
Q12:多对多中间表有额外字段怎么办?
A:手动定义中间表 Model +
SetupJoinTable,然后直接操作中间表。
Q13:Association 模式的常用操作?
A:Append(追加)、Delete(删除关联)、Replace(替换)、Clear(清空)、Count(统计)。注意 Delete 只解除关联不删除记录。
事务题
Q14:如何正确使用事务?
A:推荐
db.Transaction(func(tx *gorm.DB) error { ... return nil }),return nil 提交,return error 回滚。闭包内必须用 tx 不能用 db。
Q15:悲观锁 vs 乐观锁?
A:悲观锁(SELECT FOR UPDATE)锁住行,适合冲突多的场景(如库存扣减)。乐观锁(版本号 + RowsAffected 判断)不锁行,适合冲突少、读多写少的场景。
性能题
Q16:如何优化批量插入?
A:CreateInBatches 分批插入 + 事务包裹 + SkipDefaultTransaction + PrepareStmt。
Q17:连接池怎么配置?
A:MaxOpenConns(25-100)、MaxIdleConns(~10)、ConnMaxLifetime(3-5min)、ConnMaxIdleTime(1min)。生产必须配置上限,避免连接数爆炸。
42.3 场景设计题
场景1:设计博客系统 GORM 模型
User (HasMany Articles, HasMany Comments), Article (BelongsTo User, HasMany Comments, Many2Many Tags), Comment (BelongsTo User, BelongsTo Article), Tag (Many2Many Articles)
场景2:转账操作的事务实现
事务中:A 扣钱 → B 加钱 → 记录日志。任何一步失败 → 全部回滚。加悲观锁防并发。
场景3:千万级数据的分页优化
OFFSET 过大 → 用游标分页(WHERE id > lastID LIMIT 10),或子查询定位 + 覆盖索引。
42.4 选型指南
| 方案 | 适用场景 |
|---|---|
| GORM | 大多数 Go Web 项目,功能全、社区大 |
| sqlx | 喜欢手写 SQL,只需要薄封装 |
| sqlc | 强 SQL 能力团队,需要极高类型安全 |
| ent | 大型项目,需要 Schema 驱动的代码生成 |
42.5 进阶学习路线
- 阅读 GORM 源码(
gorm.Open→*gorm.DB→ Callback → Clause) - 学习 Go
database/sql标准库 - 对比学习 ent、sqlc
- 参与 GORM 开源贡献(Good First Issue)
- 在实际项目中深度使用 GORM
最后的话
你从零开始,走完了 42 章的学习旅程。你掌握了:
- GORM 的模型定义、CRUD、查询、关联、事务、钩子
- 性能优化、安全防护、测试方法
- 三个完整的实战项目
- 30 道面试题的答案
GORM 只是一个工具,真正重要的是你对数据库和业务的理解。工具帮你少写模板代码,但思考和分析永远是你自己的。
时刻保持好奇心,永不停下学习的脚步。祝你成为一名优秀的后端工程师!
练习题
- 默写 GORM 知识体系树(参考 42.1)。
- 从 30 道面试题中随机抽 10 题,不看答案独立回答。
- 找一个实际项目(或自己设计一个),用 GORM 从头实现数据库层。
- 阅读 GORM 官方文档中你还没看过的部分。