Go函数高内聚低耦合的核心是单一职责:函数必须只做一件事,且所有代码都服务于同一明确目的;一旦混入校验+保存+日志、签名含http.ResponseWriter或*sql.Tx、调用log.Printf无因果关联、参数为interface{}或宽泛map,即内聚破裂,必须拆分并提取I/O、密码哈希、时间获取等逻辑为接口依赖。

一个 Go 函数只要混了校验 + 保存 + 日志,它就不是高内聚低耦合——不是“不够好”,而是已经违反单一职责,必须拆。
怎么一眼看出函数违反高内聚?
高内聚不是“写得紧凑”,而是“所有代码都在为同一个明确目的服务”。一旦出现以下任一现象,说明内聚已破裂:
-
ValidateUser和db.Save出现在同一函数里 - 函数签名里带
http.ResponseWriter或*sql.Tx—— 这是层泄漏,不是内聚 - 函数里调用
log.Printf或metrics.Inc,且该日志/监控和当前业务规则无直接因果关系 - 参数类型是
interface{}或太宽泛的 struct(比如传入整个map[string]interface{}),说明函数不知道自己真正要处理什么
哪些逻辑必须从函数里剥离?
不是“可以抽”,而是“不抽就无法测试、无法复用、无法独立演进”。这些逻辑天生不该出现在业务函数体内:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- I/O 操作:数据库读写、HTTP 调用、文件读取 → 提取为
UserRepository、PaymentClient等具体类型 - 密码哈希、JWT 签发、加解密 → 抽成
PasswordService、TokenService,通过接口注入 - 时间获取:别直接调
time.Now(),封装成Clock接口,方便测试 mock - 错误包装:不要在业务函数里写
fmt.Errorf("failed to save user: %w", err),统一由上层 handler 做语义化包装
函数签名怎么设计才真正低耦合?
签名是契约,暴露什么、依赖什么,决定了你以后改起来有多痛:
立即学习“go语言免费学习笔记(深入)”;
- 参数只接收**业务所需最小数据**,比如
SaveOrder(req OrderCreateReq),而不是SaveOrder(ctx context.Context, w http.ResponseWriter, db *sql.DB, req map[string]interface{}) - 返回值只暴露**业务结果或错误**,不返回
sql.Result、http.Status这类技术细节 - 避免在函数内部 new 具体实现,比如
new(MySQLUserRepo)—— 依赖应通过参数传入,且类型是窄接口,如UserRepository - 接口定义放在**使用方包里**(比如
domain),而不是实现方(比如infra)——否则还是倒置失败
为什么组合比“拆函数”更重要?
光把一个 200 行函数拆成 5 个 40 行函数没用。真正的低耦合靠的是组合方式:
- 业务函数只调用
repo.Save()、svc.HashPassword()、clock.Now(),每个都是接口 - 这些接口的具体实现,可以在测试时用内存版、上线时换 MySQL 版、压测时换 mock 版
- 如果某天要支持 Redis 缓存用户,只需新增
RedisUserRepo实现同一接口,业务函数完全不动 - 最容易被忽略的是:接口方法数超过 3 个,基本说明职责又混了;
io.Reader不等于“能读配置”,领域语义接口必须自己定义

















