高阶函数可替代条件分支实现灵活查询构造、闭包封装状态、纯函数数据转换及函数式配置。核心是将易变逻辑抽象为函数,提升可维护性与扩展性,避免硬编码和结构体膨胀。

用高阶函数替代条件分支做查询构造
当 DAO 层出现大量 GetByXxx 方法,本质是把「查询逻辑」硬编码进分支判断里。这种写法在字段增加、查询类型变化(如 IN、BETWEEN、模糊匹配)时立刻崩坏。
正确做法是把「如何查」抽成函数,让调用方决定组合方式:
-
Where条件不写死在 DAO 里,而是由外部传入func(*gorm.DB) *gorm.DB - 每个查询条件封装为独立函数,比如
ByUserID(id int64)返回一个闭包,只负责拼接user_id = ? - DAO 方法变成统一入口:
GetUserInfo(ctx, opts ...func(*gorm.DB) *gorm.DB),内部用db = db.Scopes(opts...)或逐个调用
这样新增一个 ByStatusIn(statuses []int),只需加函数,不用改 DAO 主逻辑。
闭包封装状态避免冗余参数传递
需要跨多层传递「配置上下文」或「临时状态」时,别靠层层传参,用闭包捕获更干净。
例如日志中间件中需注入 trace ID,与其在每个 handler 参数里加 traceID string,不如:
- 定义
withTraceID(traceID string) func(http.Handler) http.Handler - 闭包内捕获
traceID,返回的 middleware 函数直接用它打日志 - 调用时写
http.Handle("/api", withTraceID("xxx")(myHandler)),无侵入、无额外参数
注意:闭包引用的变量生命周期要清晰——若捕获了局部指针或 map,且该闭包被长期持有(如注册到全局 registry),可能引发内存泄漏。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
纯函数处理数据转换,拒绝就地修改
对切片做 filter、map、reduce 时,坚持返回新副本,不修改原数据。
常见错误是写 for i := range arr { arr[i] *= 2 },这破坏了调用方对原始数据的预期。
- 正确写法:
double := func(xs []int) []int { res := make([]int, len(xs)); for i, x := range xs { res[i] = x * 2 }; return res } - 配合泛型可复用:
Map[T, U](xs []T, f func(T) U) []U,避免重复造轮子 - 性能敏感场景下,若确定原切片不再使用,可用
res := xs[:0]复用底层数组,但必须文档注明「会修改输入」
函数作为配置项传入,比结构体字段更灵活
当一个结构体里塞满「开关型」字段(EnableCache bool、Timeout time.Duration、OnFailure func(error)),其实是在模拟函数式配置。
直接暴露函数接口更直白:
- 把
type Config struct { OnRetry func() }拆成WithRetry(f func()) Option类型的选项函数 - 初始化时链式调用:
NewClient(WithTimeout(5*time.Second), WithRetry(backoff)) - Option 函数内部修改私有 config 结构体字段,对外零暴露,既保持封装又免去空值判断
这种方式天然支持组合、复用,也规避了结构体字段膨胀后难以维护的问题——新增行为无需改结构体定义,只加一个 WithXXX 函数即可。
真正难的不是写出高阶函数,而是判断哪些逻辑值得抽象成函数。过度抽象会让调用方困惑,而完全不抽象会让 DAO 层堆满 if xxx != nil。关键在「变更频率」:如果某段逻辑未来三个月内大概率要加新分支,它就该是一个函数。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










