go-zero 为什么要在 model 目录下放一个 vars.go

一个只有 3 行代码的文件,却决定了整个 model 层能不能编译。它从哪来、干什么用、为什么必须放在这个位置——一次讲清楚。

先给结论 文件里有什么 它从哪来 解决什么问题 为什么单独一个文件 为什么放这里 你项目的情况 为什么要包一层 怎么用 同类文件对比 注意事项

它是 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 model
import "…/stores/sqlx"
var ErrNotFound = sqlx.ErrNotFound
容易混淆的一点:模板名叫 err.tpl,但生成出来的文件叫 vars.go(goctl 里的变量名是 varFilename,拼成 vars.go)。模板名和文件名不一致,所以在源码里搜 vars.tpl 是搜不到的——搜 err.tpl 才能找到。
另一个易混淆点:模板目录里还有一个 var.tpl(注意是 var 不是 vars),它生成的是每张表的字段名变量tasksFieldNamestasksRows 这些),这部分内容被写进了 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.ErrNotFoundsqlx.ErrNotFoundsqlc.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 包钉下的一个错误契约锚点。它内容极简,但位置被生成代码硬编码引用——理解了这一点,就不会再觉得它多余,也不会随手删掉它。