函数级缓存需与调用生命周期对齐,否则导致无效、内存泄漏或竞态;闭包封装适用于纯函数且参数固定,如正则编译;局部map仅限小基数无并发短生命周期场景;递归缓存map须定义在外层;singleflight解决并发去重而非结果复用。

函数级缓存不是加个 map 就完事——它必须和调用生命周期对齐,否则缓存无效、内存泄漏、甚至引发竞态。
闭包封装缓存:适合纯函数 + 固定参数
当你有一个纯计算函数(无副作用、输入确定输出),且参数在单次初始化或请求中不变时,用闭包捕获缓存变量最轻量、最安全。
- 缓存变量(如
result和once sync.Once)必须定义在闭包外层,不能放在函数体内,否则每次调用都新建 - 典型场景:正则编译、配置解析、常量映射构建——比如
regexp.MustCompile开销大,但结果可复用 - 错误写法:
func() { var r *regexp.Regexp; r = regexp.MustCompile(...) }→ 每次都重编译 - 正确写法:用
once.Do保证只执行一次,返回一个无参函数,内部直接返回缓存值
局部 map 缓存:仅限小基数、无并发、短生命周期
在函数内部用 map 缓存最近几次结果,适用于参数组合有限(如枚举、小整数、固定字符串)且不跨 goroutine 的场景。
- 必须控制大小,比如用 LRU 策略或简单计数淘汰,否则 map 持续增长导致内存泄漏
- 绝对禁止在 HTTP handler 或 goroutine 循环里无清理地使用局部
map缓存用户数据 - 若参数是
[]byte或结构体,别直接当 key——[]byte不可比较,会 panic;推荐转成string或用fmt.Sprintf构造稳定 key - 并发读写必须加锁,但会抵消缓存收益;真需要多 goroutine 安全,请改用
sync.Map或外部库(如lru-cache)
递归函数加缓存:map 必须在函数外,键要可哈希
斐波那契、树路径求和这类指数级递归,不加缓存根本不可用——fib(40) 原始递归调用上亿次,加缓存后仅需 40 次。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 缓存
map必须定义在递归函数外部(如闭包变量或结构体字段),否则每次递归都新建空 map,完全失效 - 键推荐用结构体 + 实现
Hash方法,比fmt.Sprintf("%d-%s", a, b)更安全、更高效;避免用指针或切片作 key - 注意:缓存的是中间结果,不是最终业务逻辑;若递归依赖外部状态(如时间、全局变量),缓存会出错
- 如果递归深度大、参数组合多,要考虑内存占用——不是所有递归都适合缓存,有时迭代+栈更稳
别把 singleflight 当函数缓存用
singleflight.Group 解决的是「并发请求去重」,不是「函数结果复用」。它不存储结果,也不跨调用生命周期保留数据。
- 每次
Do("key", fn)都是独立的合并动作,fn 执行完结果就丢,下次调用仍会重新触发 - 如果你希望多次调用
fib(40)都直接返回,该用闭包缓存;用singleflight只会让并发调用不重复算,但串行调用仍反复算 - 常见误用:在
fn里又调Group.Do→ 嵌套导致去重失效;或拿指针地址当 key → 表面不同实则相同,意外共享结果 - 真正需要的是:先用
singleflight防击穿,再把成功结果写进本地缓存(如map或sync.Map),二者配合才完整
最容易被忽略的一点:缓存是否值得,取决于「计算开销」和「复用频率」的比值。一次 time.Now().Unix() 不值得缓,而一次 http.Get 在高频循环里就一定得缓——别为了“有缓存”而缓存,要看真实调用链路里的热区在哪。

















