单一职责原则要求一个函数只做一件事:清晰命名、可独立测试、无副作用;若混入验证+保存+日志等多类逻辑,即违反srp,必须拆分。

Go 里写高内聚低耦合的函数,核心就一条:一个函数只做一件事,且这件事必须能被清晰命名、独立测试、不带副作用。不是“尽量拆分”,而是“不拆分就违反 SRP”——只要函数里混了验证 + 保存 + 日志 + 错误转换,它就已经是耦合炸弹。
怎么判断函数是否违反单一职责?
看它有没有同时干多类事:
- 既校验输入,又操作数据库(
ValidateUser和db.Save不该出现在同一个函数里) - 既处理业务逻辑,又构造 HTTP 响应(
http.ResponseWriter出现在 service 层函数签名里就是信号) - 既计算结果,又打日志或发监控事件(
log.Printf或metrics.Inc是副作用,不该藏在纯业务函数中) - 参数类型太宽泛,比如用
interface{}接收业务对象——这说明函数根本不知道自己要处理什么,职责已经模糊
拆分函数时,哪些逻辑必须剥离?
凡是和「当前业务规则无关」或「可能随环境/配置/下游变化」的部分,一律抽出成独立函数或类型:
- I/O 操作:数据库读写、HTTP 调用、文件读取 → 提取为
UserRepository或PaymentClient类型 - 密码哈希、JWT 签发、加解密 → 抽成
PasswordService、TokenService,通过接口注入 - 时间获取:别直接调
time.Now(),封装成Clock接口,方便测试 mock - 错误包装:不要在业务函数里写
fmt.Errorf("failed to save user: %w", err),统一由上层协调器(如 handler)做语义化包装
函数签名怎么设计才利于解耦?
签名是契约,暴露什么、依赖什么,决定了耦合程度:
- 参数尽量用具体结构体或领域类型(如
User),而非map[string]interface{}或interface{} - 返回值避免裸
error,优先用自定义错误类型(如ErrUserNotFound)或明确的struct(如type CreateUserResult struct { ID int; Token string }) - 不接收全局变量、单例、或未抽象的实现类型(如
*sql.DB);要用接口,且接口定义在调用方包里(比如domain包定义UserRepo,infra包实现) - 函数本身不 new 任何依赖项——所有依赖都由外部注入,哪怕只是传参
容易被忽略的耦合点:错误处理与日志位置
很多人以为把逻辑拆开了就解耦了,但错误处理和日志写在哪,一样会悄悄绑定上下文:
- 在
ValidateUser里返回errors.New("name is required")是 OK 的,但若返回fmt.Errorf("user validation failed: %w", err),就混入了非领域语义 - 日志不能出现在
CreateUser函数内部;应该由 handler 或 use case 层统一记录「谁在什么时候触发了什么操作」 - panic/recover 不该用于业务错误(如用户不存在),仅用于真正不可恢复的程序异常(如空指针解引用)
- 测试时若发现要 patch
log或time才能跑通,说明函数已隐式依赖了外部状态
真正难的不是拆函数,而是忍住不把“顺手加一行日志”“顺便格式化下错误”写进业务逻辑里——这些看似微小的粘连,会在三个月后让整个模块无法单独测试、无法替换存储、无法迁移到新协议。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











