通用逻辑封装需判断可复用性、状态需求及依赖上下文;闭包适用于携带配置或轻量状态;结构体+方法适合多字段、生命周期管理或接口实现;泛型应避免过度抽象,优先业务约束类型;通用函数须显式暴露行为边界、不吞错误、不隐式转换。

通用逻辑封装不是“抽成一个函数就完事”,而是要判断它是否具备可复用性、是否需要状态、是否涉及依赖或上下文——错判会导致后续改起来像刮漆。
什么时候该用闭包,而不是普通函数
当你需要捕获并携带配置、限流器、重试策略或某个固定参数时,闭包是自然选择。比如限流装饰器必须持有 *rate.Limiter 实例,否则每次调用都新建一个,限流失效。
- 闭包适合无状态或轻量状态的包装:如
WithRateLimitCtx、WithLogging - 别在闭包里捕获大对象(如
*http.Client或未清理的 map),否则 GC 不掉 - 循环中创建闭包要小心变量捕获:写
for _, v := range items { fns = append(fns, func() { println(v.ID) }) }会全打印最后一个v.ID;应写成func(v Item) func() { return func() { println(v.ID) } }(v)
什么时候该用结构体 + 方法,而不是函数或闭包
当逻辑开始需要多个字段、生命周期管理、或要实现接口时,结构体是唯一合理选择。比如一个重试控制器,既要存最大次数、退避间隔,又要支持重置、统计失败数——硬塞进闭包会很快失控。
- 结构体方法天然支持组合:
type Retrier struct { max int; backoff time.Duration },再定义Do(ctx context.Context, fn func() error) error - 结构体可实现接口,便于 mock 和替换:比如
type Cache interface { Get(key string) (any, bool) },测试时直接传内存实现 - 避免把结构体变成“配置容器”:如果
Retrier里只有字段没方法,或方法全是GetMax()这种 getter,说明它还没承担起职责,不如用 plain struct
泛型函数封装的边界在哪
泛型不是万能胶水。Go 1.26 下,[T any] 是最危险的起点——它让你编译通过,但运行时可能 panic 或类型流断裂。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 优先用业务约束替代
any:比如type ID interface { ~int64 | ~string },再写FindByID[T ID](id T) error - 嵌套泛型(如
func MapSlice[T any, R any])容易让调用方困惑,且 GORM/validator 等库不穿透第二层泛型;能用MapSlice[User, string]显式实例化就别抽象 - 数据库操作、HTTP 响应、高频小对象(如分页元数据)场景,直接写具体类型更稳;泛型只用于真正重复、且类型差异明确的工具逻辑(如 JSON 安全解码)
为什么你的“通用函数”最后没人敢用
因为它们悄悄引入了隐式依赖、不可控错误、或违反调用方预期。比如一个 SafeJSONUnmarshal 函数,内部用了 json.Unmarshal 却没暴露 UnmarshalTypeError,上游无法区分是格式错还是字段缺失;又或者它默认用了全局 Decoder,导致并发时 panic。
- 通用函数必须显式暴露其行为边界:是否处理
nil、是否忽略未知字段、是否兼容空字符串转零值 - 错误不能吞:
fmt.Errorf("unmarshal failed: %w", err)是底线,别用errors.New("bad json") - 避免“自动修复”逻辑:比如自动把
"true"字符串转truebool,这种隐式转换会让调用方调试抓狂
最常被忽略的一点:通用逻辑的“通用”是动态的。今天封装的 WithPagination 可能下周就要支持游标分页,而你当初把它写成闭包+固定 SQL,现在就得重写整个链路。一开始就留好扩展点——比如让分页参数走结构体、让查询修饰器返回 func(*gorm.DB) *gorm.DB 而非直接拼 SQL——比追求“一次封装永久复用”实在得多。

















