先给结论
它是 model 包的「公共错误契约文件」
全文件就干一件事:定义一个包级变量 ErrNotFound,供该目录下所有 model(含子包) 共用。
一句话回答"为什么放这里": 因为 goctl 生成的 model 代码里,硬编码写死了 import "你的模块/internal/model" 并返回 model.ErrNotFound。这个文件不在 internal/model/ 下,你所有的 model 子包都会编译失败 。
是什么
goctl 生成的包级变量文件,内容是 var ErrNotFound = sqlx.ErrNotFound
干什么
把"查不到数据"这个错误统一出口,让 logic 能区分 没查到 和 真出错
为什么放这
生成物硬编码引用这个路径;且它属于 model 包,不能被任何单张表独占
文件内容
先看它到底长什么样
以你项目里的 internal/model/vars.go 为例,完整内容就是这 3 行有效代码。
package model
import "github.com/zeromicro/go-zero/core/stores/sqlx"
var ErrNotFound = sqlx.ErrNotFound
逐行解释:
① package model —— 它属于 model 这个包,不是 某张表的 model 文件;
② 引入 go-zero 的 sqlx 包(底层数据库封装);
③ 把 sqlx.ErrNotFound 赋值给本包的 ErrNotFound,相当于给它起了个别名、换了个出口。
它没有任何业务逻辑 ,不包含表名、字段、SQL。纯粹是一个"错误常量的定义文件"。这也是很多人第一次看到会发懵的原因——内容太短,看不出存在的必要。
来源
它是 goctl 生成的,不是手写的
来自 go-zero 源码里的模板 tools/goctl/model/sql/template/tpl/err.tpl。
模板 err.tpl(原文)
package {{.pkg}}import "…/stores/sqlx"var ErrNotFound = sqlx.ErrNotFound
渲染后的 vars.go
package modelimport "…/stores/sqlx"var ErrNotFound = sqlx.ErrNotFound
容易混淆的一点: 模板名叫 err.tpl,但生成出来的文件叫 vars.go(goctl 里的变量名是 varFilename,拼成 vars.go)。模板名和文件名不一致 ,所以在源码里搜 vars.tpl 是搜不到的——搜 err.tpl 才能找到。
另一个易混淆点: 模板目录里还有一个 var.tpl(注意是 var 不是 vars),它生成的是每张表的字段名变量 (tasksFieldNames、tasksRows 这些),这部分内容被写进了 xxxmodel_gen.go 里,跟 vars.go 完全是两码事 。
goctl 一次会生成哪些文件
internal/model/
├── vars.go # ← 由 err.tpl 生成,全包共享(本文主角)
└── zuowenpigai/ # 子包(按目录/风格生成时)
├── tasks.go # 可定制:接口 + customModel
├── tasks_gen.go # 生成物 DO NOT EDIT(含 var.tpl 的字段变量)
├── score_review.go / _gen.go
├── polish_review.go / _gen.go
├── thought_review.go / _gen.go
├── detail_review.go / _gen.go
└── task_images.go / _gen.go
它解决什么
没有它,"查不到"和"数据库炸了"就分不清
看一段你项目里真实生成的 tasks_gen.go 代码。
// internal/model/zuowenpigai/tasks_gen.go(goctl 生成,DO NOT EDIT)
import (
"context"
"mvp-service/internal/model" // ← 注意:import 了父包
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
func (m *defaultTasksModel ) FindOne (ctx context.Context, id int64) (*Tasks , error) {
query := fmt.Sprintf ("select %s from %s where `id` = ? limit 1" , tasksRows, m.table)
var resp Tasks
err := m.conn.QueryRowCtx (ctx, &resp, query, id)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, model.ErrNotFound // ← 统一返回父包定义的错误
default :
return nil, err // 连接超时、语法错误等真错误
}
}
关键就在那个 switch: go-zero 把查询结果分成三类——成功 、没查到 (返回 model.ErrNotFound)、其他真错误 (原样返回)。
这样 logic 层就能把"用户查了个不存在的 ID"(该返回 404 或走业务分支)和"数据库连不上"(该返回 500 并告警)区分开。
如果不做这个区分会怎样
// ❌ 不区分:把"查不到"当成系统错误
task, err := l.svcCtx.TasksModel.FindOne (l.ctx, req.Id)
if err != nil {
return nil, err // 用户查了不存在的 id → 直接 500,还触发告警
}
// ✅ 区分后:语义正确
task, err := l.svcCtx.TasksModel.FindOne (l.ctx, req.Id)
switch {
case err == nil:
// 正常流程
case errors.Is (err, model.ErrNotFound):
return nil, errorx.New (CodeTaskNotFound, "任务不存在" ) // 友好提示
default :
return nil, err // 才需要告警
}
设计原因一
为什么非要单独一个文件,不能写在 model 里
答案很硬:Go 的包级作用域不允许重复声明 。
你的 zuowenpigai 子包里有 7 张表、7 个 model 文件 ,每一个的 FindOne 都要返回 ErrNotFound。
❌ 各写各的:重复声明
✅ 抽到 vars.go:一份共享
tasks.go: var ErrNotFound
score_review.go: var ErrNotFound
polish_review.go: var ErrNotFound
… 还有 4 个文件
编译失败
ErrNotFound redeclared
in this block
vars.go
var ErrNotFound = sqlx.ErrNotFound
tasks.go → 直接引用
score_review.go → 直接引用
… 其余 5 个 → 直接引用
同一包内唯一声明,编译通过
图 1:包级变量必须"一处声明、多处使用",这就是 vars.go 存在的第一性原因
那塞进某一个 model 文件(比如 tasks.go)行不行?
① 耦合
删掉 tasks.go,其他 6 个 model 全部编译失败。公共资产不该被某个具体表绑架。
② 会被覆盖
model 文件是 goctl 生成物,重新生成会重置内容,你加的东西保不住。
③ 语义不对
它不属于任何一张表,放进某张表的文件里,读代码的人会误以为它只服务于那张表。
设计原因二(核心)
为什么必须放在 internal/model/ 这个位置
这才是真正的硬约束:goctl 生成的代码把这个路径写死了 。
你的 7 个 model 文件里,每一个都有一模一样的这一行:
// internal/model/zuowenpigai/tasks_gen.go
import (
"mvp-service/internal/model" // ← 硬编码:指向 internal/model 这个包
)
// 以及 FindOne / FindOneByXxx 里的这一句:
case sqlc.ErrNotFound:
return nil, model.ErrNotFound // ← 用的是 model 包(父包)的 ErrNotFound
package zuowenpigai(子包)
tasks.go / tasks_gen.go
score_review.go / _gen.go
polish_review.go / _gen.go
thought_review.go / _gen.go
detail_review.go / _gen.go
task_images.go / _gen.go
↓ 每个都 import 父包并引用 model.ErrNotFound
package model(父包)
vars.go
var ErrNotFound = sqlx.ErrNotFound
import
依赖方向:子包 → 父包(单向)。父包不认识子包,子包也不互相依赖
图 2:所有 model 子包同向依赖 internal/model,vars.go 就是这个依赖的汇聚点
如果把它挪走会怎样? 比如挪到 internal/errs/errors.go——那么 model.ErrNotFound 这个标识符就不存在了,7 个子包里的 return nil, model.ErrNotFound 全部编译失败 ,报 undefined: model.ErrNotFound。
除非你手动去改每一个 *_gen.go 里的 import 和引用——但那违反了 DO NOT EDIT 的约定,下次 goctl 重新生成又会全部打回原形。
所以答案是"约定优于配置": go-zero 定死了「model 根包放错误契约」这个约定,生成物按这个约定硬编码引用。你遵守约定,一切自动工作;你想改,就得跟代码生成器对抗。
为什么是 model 目录,而不是 internal 根下
语义归属正确
"查不到记录"是数据访问层 的语义,不是全局业务错误。放在 model 包下,logic 通过 model.ErrNotFound 引用,读起来就知道来源。
依赖方向干净
如果放 internal/errs,那 model 要 import errs,errs 又可能被 logic 引用,容易绕成环。放在 model 内部,依赖是单向向下的。
你的项目
king-club-mvp-service 里的实际情况
顺便说一个你这个项目里值得注意的细节。
internal/model/
├── vars.go # package model —— 只有 ErrNotFound 一行定义
└── zuowenpigai/ # package zuowenpigai —— 子包
├── tasks.go / tasks_gen.go
├── score_review.go / _gen.go
├── polish_review.go / _gen.go
├── thought_review.go / _gen.go
├── detail_review.go / _gen.go
└── task_images.go / _gen.go # ← 子包里没有自己的 vars.go
注意这个现象: 子包 zuowenpigai/ 里没有 vars.go,7 个 model 文件全部 import "mvp-service/internal/model",共用父包那一个 ErrNotFound。
这是 goctl 在按目录生成子包 时的行为:错误契约统一提到父包,子包只负责引用。好处是全项目的 ErrNotFound 是同一个值 ,跨子包判断不会出岔子。
对你的实际影响:
① 在 logic 里判断"查不到"时,要 import 的是 mvp-service/internal/model,用 model.ErrNotFound——不是 zuowenpigai.ErrNotFound;
② 别手贱删 vars.go,删了整个 model 层直接编译不过;
③ 你项目里 internal/repository/zuowenpigai/task_completed_repository.go 用的是 sqlx.ErrNotFound,而 model 层用的是 model.ErrNotFound——两者是同一个值,判断都能成立 ,下面解释为什么。
设计原因三
既然值都一样,为什么还要包这一层
先说一个反直觉的事实:model.ErrNotFound、sqlx.ErrNotFound、sqlc.ErrNotFound 是同一个东西 。
// go-zero 源码 core/stores/sqlc/cachedsql.go
const (
// ErrNotFound is an alias of sqlx.ErrNotFound.
ErrNotFound = sqlx.ErrNotFound
)
// 所以链条是:
// sqlx.ErrNotFound ──别名──> sqlc.ErrNotFound ──赋值──> model.ErrNotFound
// 三者 == 比较为 true,errors.Is 也为 true
既然如此,直接在代码里用 sqlx.ErrNotFound 不就行了?为什么要绕一圈?
① 隔离底层依赖
logic 层不该 import sqlx 这种底层库。它只 import 业务的 model 包——换 ORM、换驱动时不用改调用方。
② 单点收敛
哪天 go-zero 改了错误定义,或你想换成自定义错误类型,只改 vars.go 一行,全项目生效。
③ 屏蔽缓存/非缓存差异
生成物里 case sqlc.ErrNotFound 是缓存版写法。统一转成 model.ErrNotFound 后,调用方不用管底层走没走缓存。
// ❌ 不推荐:logic 直接依赖底层库
import "github.com/zeromicro/go-zero/core/stores/sqlx"
if err == sqlx.ErrNotFound { ... } // 业务代码和框架绑死
// ✅ 推荐:只依赖自己的 model 包
import "mvp-service/internal/model"
if errors.Is (err, model.ErrNotFound) { ... } // 换框架也不用动
一句话: 这层包装的价值不在于"值不同",而在于"依赖方向不同" 。它把「框架的错误」翻译成「你的项目的错误」,让项目代码不用认识框架。
实践
正确的使用姿势
在 logic 里判断查不到
import (
"errors"
"mvp-service/internal/model"
)
func (l *GetTaskLogic ) GetTask (req *types.GetTaskReq) (*types.GetTaskResp, error) {
task, err := l.svcCtx.TasksModel.FindOne (l.ctx, req.Id)
switch {
case err == nil:
// 命中,走正常流程
return &types.GetTaskResp{Task: convert (task)}, nil
case errors.Is (err, model.ErrNotFound):
// 没查到:这是业务的正常分支,不是故障
return nil, errorx.New (code.TaskNotFound, "任务不存在" )
default :
// 真错误:记录日志 + 返回 500
l.Errorf ("查询任务失败 id=%d err=%v" , req.Id, err)
return nil, errorx.New (code.InternalError, "服务异常" )
}
}
用 errors.Is 而不是 ==: 现在两者等价,但 errors.Is 能在错误被 fmt.Errorf("%w", err) 包装后依然判断成功,更抗未来变化。
常见错误用法
写法 评价 问题
if err != nil { return err }✗ 把"查不到"当 500 抛出去,用户看到系统错误,还污染告警
err == model.ErrNotFound△ 当前能work,但错误一旦被包装就失效,建议改 errors.Is
errors.Is(err, model.ErrNotFound)✓ 推荐写法,抗包装
err == sql.ErrNoRows✗ 直接用标准库的,绕过了 go-zero 的封装,缓存场景下可能判断失败
if task == nil 判断✗ go-zero 的 FindOne 出错时返回 nil 指针,但不知道是没查到还是报错 ,必须看 err
横向对比
model 目录下几个文件各自是干嘛的
文件 来源 能改吗 内容
vars.go
err.tpl 生成
不建议
包级共享变量 ErrNotFound。全包一份 ,被所有 model 引用
xxxmodel.go 如 tasks.go
model.tpl 生成
✓ 可以
接口声明 + customXxxModel 结构。官方指定的扩展位 ——自定义方法、事务方法都加在这
xxxmodel_gen.go 如 tasks_gen.go
多个 tpl 拼成 (含 var.tpl)
✗ 不能
标着 DO NOT EDIT。CRUD 实现、字段变量(tasksRows 等)、缓存逻辑
分工记忆: _gen.go 是机器写的默认实现 (别碰);xxxmodel.go 是给人写的扩展位 (随便加);vars.go 是全包共用的常量 (别删)。三者分工明确,互不重叠。
注意事项
关于 vars.go 的几条实践建议
事项 说明
别删它 它是 goctl 生成物,被所有 model 子包硬编码引用。删掉 = 整个 model 层编译失败
别在里面加自定义变量 它是生成物,重新跑 goctl model 时可能被覆盖回初始内容。自定义的错误变量建议另建文件 (如 internal/model/errors.go,package 同为 model),这样既能在包内共享,又不会被生成器覆盖
子包不要各建一份 多个子包各自定义 ErrNotFound,虽然包名不同不会冲突,但会导致跨包判断失效(errors.Is 拿不到同一个值)。统一用父包那一份
缓存场景注意 带缓存的 model,查不到时命中的是 sqlc.ErrNotFound,生成物会转成 model.ErrNotFound 返回——所以你用 model.ErrNotFound 判断,缓存/非缓存两种场景都成立
只适用于 FindOne 类查询 FindOne / FindOneByXxx 查不到才返回 ErrNotFound。查询列表(FindAll)为空是正常返回空切片,不报错 ,别混为一谈
Delete / Update 不影响 删除或更新 0 行不会返回 ErrNotFound,只会 RowsAffected() == 0,需要自己判断
总结一句: vars.go 是 go-zero 用「约定优于配置」的思路,给 model 包钉下的一个错误契约锚点 。它内容极简,但位置被生成代码硬编码引用——理解了这一点,就不会再觉得它多余,也不会随手删掉它。