context.value仅适用于传递轻量、只读的请求元数据(如requestid、userid),不可传数据库实例等可变依赖,否则会导致运行时panic、ide无法补全、测试难替换且破坏依赖可见性。

为什么别用 context.Keys 存数据库实例
因为 context.Keys 是 map[string]interface{},取出来必须显式类型断言,比如 c.Keys["DB"].(*models.DB)。一旦断言失败,运行时 panic;IDE 无法补全方法,pc.DB.GetVotePack 这类调用在编辑器里直接报错或灰掉;更关键的是,它把请求上下文(超时、用户身份)和业务依赖(DB、Cache)混在一起,测试时根本没法干净替换。
- 常见错误现象:
db.GetVotePack undefined或panic: interface conversion: interface {} is nil, not *models.DB - 使用场景:仅限携带请求元数据,如
c.Set("user_id", "123"),绝不该放服务实例 - 性能影响:无额外开销,但破坏了依赖的可见性与可追踪性
NewXXX() 函数该怎么写才安全
每个组件只暴露一个 NewXXX 函数,参数全是接口,返回具体结构体指针。例如 NewUserService(repo UserRepo, logger Logger),而不是 NewUserService(db *sql.DB, log *zap.Logger)。
- 参数必须是接口,且由调用方定义(比如
UserRepo放在service包,实现放在repo包) - 函数内部不做全局变量赋值、不调用
init()、不读配置——所有依赖都靠参数传入 - 避免返回
interface{}或泛型包装,保持类型明确,便于测试时直接传入 mock 实例
Wire 生成器容易踩的三个硬坑
Wire 不是魔法,它只做代码生成,所有规则都得手动对齐。最常出问题的地方不在逻辑,而在文件结构和注释格式。
-
// +build wireinject必须独占第一行,前面不能有空格,后面不能跟任何字符(包括空格、逗号、注释) -
wire.go文件必须和目标初始化函数同目录,比如你要生成cmd/myapp/InitializeApp(),那wire.go就得放在cmd/myapp/下,不能放根目录 - 同类型多个实例(如主从 DB)会直接 panic,必须用
wire.Value(primaryDB)或wire.Struct(&App{}, "primaryDB", "replicaDB")显式区分字段名
接口定义到底该放在哪一层
接口不是为了“看起来解耦”而存在,它是测试和替换的边界。所以接口必须按调用方需要来定义,且越小越好——通常就 1~2 个方法。
- 错误做法:在
repo包里定义UserRepo接口,塞进CreateTx、UpdateEmailWithLock等带实现细节的方法 - 正确做法:在
service包里定义UserRepo,只留GetByID(ctx, id)和Save(ctx, u)—— 具体要不要事务、加锁,由实现决定 - 本地缓存、简单工具函数(如
strings.ToUpper包装)不需要接口,直接传值更清晰
NewApp,而是每次加新依赖时,能不能立刻判断出这个依赖该由谁定义接口、该传给谁、测试时 mock 哪一块。这需要反复在接口粒度和职责边界上做取舍,而不是套模板。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











