接口暴露实现细节即设计缺陷,如含*sql.tx、sql.等具体类型或包路径,应只依赖抽象类型和标准库接口。

看接口定义是否暴露实现细节
Go模块设计缺陷最常藏在接口与实现的边界上。如果一个interface里出现了具体类型名、包路径或带下划线的私有字段,基本可以判定它耦合了实现。比如:type UserService interface { SaveUser(*sql.Tx, User) error }——把*sql.Tx塞进接口,等于把数据库驱动细节泄漏出去。
真正可测试、可替换的接口应该只依赖抽象:输入输出用值或标准库类型(如io.Reader),方法不暴露底层技术栈。检查时直接搜interface块里有没有sql.、redis.、http.这类前缀,有就是信号。
- 接口方法参数/返回值中出现具体结构体指针(如
*gorm.DB)→ 违反抽象原则 - 接口名带“Impl”“Concrete”等后缀 → 说明设计者自己都意识到这不是纯接口
- 同一包内多个
struct实现同一个接口,但接口只被本包使用 → 接口无存在必要,删掉更干净
查函数签名是否违反单一职责
评审时快速扫一遍函数签名,重点看参数数量和类型组合。Go里超过4个参数的函数大概率职责过重;若同时出现context.Context、log.Logger、*sql.DB、config.Config这四类,基本是“上帝函数”——它既管流程、又管日志、还连数据库、还要读配置。
这种函数无法单元测试(mock成本高),也无法复用。修复方向不是加注释,而是拆:把DB操作抽成repo层函数,把日志封装进logger实例,让主逻辑只接收业务参数。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 函数参数含多个指针类型(尤其是不同包的)→ 职责分散,建议按领域拆分
- 返回值里混用
error和*http.Response等框架类型 → 暴露传输层细节,应转为领域错误 - 函数名含“And”“Then”“With”等连接词(如
CreateUserAndSendEmail)→ 明确违反单一职责
验包依赖是否存在循环引用
模块级设计缺陷最致命的是包间循环依赖,它会让编译失败或导致隐式初始化顺序错乱。不要等go build报错才发现——用go list -f '{{.Deps}}' ./pkg/a手动展开依赖树,或者更直接:在IDE里点开任意一个import语句,看它最终会不会绕回自己所在的包。
常见陷阱是model包被handler和service同时依赖,结果service又偷偷引入handler里的工具函数。只要发现pkgA → pkgB → pkgA这样的链路,就必须打断。
-
go mod graph | grep搜关键词,比对两端包名是否形成闭环 - 某个包的
init()函数调用了其他包的导出变量 → 隐式依赖,极易引发初始化死锁 -
internal/目录下的包被外部模块直接import→ 打破封装边界,后续重构会踩坑
盯错误处理是否掩盖控制流意图
Go里if err != nil不是问题,问题是错误处理和业务逻辑搅在一起。典型症状是:一个函数里出现3次以上if err != nil { return ..., err },且每次return前都夹着状态变更(如counter++、cache.Set()),这时错误分支实际已修改了局部状态,但调用方根本不知道。
这种写法会让retry逻辑失效、事务回滚困难、并发安全崩塌。正确做法是把副作用操作推迟到所有校验通过之后,或者用defer注册清理动作。
- 错误分支里调用了非幂等操作(如发MQ消息、改DB)→ 必须重构为先校验再执行
- 同一个
err变量被多次fmt.Errorf包装,堆栈信息被层层截断 → 改用errors.Join或保留原始cause - 函数结尾统一
return nil, err,但err可能来自前面某次忽略的defer关闭失败 → 应该显式检查每个资源释放
context deadline exceeded却找不到源头。评审时别只盯着语法,得顺着数据流和控制流多问一句:“这个包被别人引用时,最可能怎么误用?”golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










