登录
首页 >  Golang >  Go教程

Go weak.Pointer 实战:缓存别越跑越胖,先搞懂弱引用和 AddCleanup

来源:Go Blog

时间:2026-06-01 21:38:16 134浏览 收藏

如果你写过本地缓存、对象去重池、元数据索引,大概率遇到过这种尴尬:缓存是为了省资源,结果跑久了自己变成最大内存户。Go 1.24 加进来的 weak.Pointerruntime.AddCleanup,就是专门给这类高级场景准备的工具。但它不是银弹,用错了会比普通 map 更难排查。

这篇我按生产代码的视角来讲:弱引用到底弱在哪里、为什么 Value() 一定要判空、缓存清理怎么和 GC 配合,以及什么时候我宁愿不用它。

Go weak.Pointer 思维导图
思维导图:弱引用不是缓存框架,它只是给 GC 留出回收对象的空间。

先说人话:弱引用不是“更轻的指针”

普通指针会把对象“留住”。只要还有强引用指向这个对象,GC 就不能回收它。弱引用不一样,weak.Pointer 指向对象,但不会阻止 GC 回收对象。

这意味着一件很重要的事:你从弱指针里取值时,可能拿到对象,也可能拿到 nil。所以它的使用姿势不是“我存了就一定有”,而是“如果对象还活着,我就复用;如果对象已经没了,我就重新创建”。

它适合什么场景

我会优先想到三类场景:对象规范化、去重池、辅助缓存。比如同一个配置快照、同一个解析后的 schema、同一个大对象的共享表示,很多请求都可能用到,但业务上又不值得强行永久留在内存里。

如果你只是想做一个有 TTL、有容量、有淘汰策略的业务缓存,那弱引用不是首选。该用 ristretto、groupcache、bigcache 或者你自己的 LRU,就老老实实用缓存。弱引用解决的是“不要因为缓存 map 的引用让对象永远活着”这个问题。

Go weak.Pointer 缓存流程图
流程图:取弱引用、判空、创建对象、包装 weak.Pointer,再用 AddCleanup 做索引清理。

一个最小的弱引用缓存骨架

下面这段代码只是骨架,重点是看流程:先从 map 里拿弱指针,再 Value() 判空;对象不存在时重新创建,再用 weak.Make 放回去。

package cache

import (
    "sync"
    "weak"
)

type Schema struct {
    Name string
}

type Cache struct {
    mu sync.Mutex
    m  map[string]weak.Pointer[Schema]
}

func (c *Cache) Get(key string) *Schema {
    c.mu.Lock()
    defer c.mu.Unlock()

    if wp, ok := c.m[key]; ok {
        if v := wp.Value(); v != nil {
            return v
        }
    }

    v := &Schema{Name: key}
    c.m[key] = weak.Make(v)
    return v
}

这里最关键的就是这句:if v := wp.Value(); v != nil。弱引用里的对象可能已经被 GC 回收,取出来以后不能直接用。很多 bug 就来自“我以为缓存命中了,所以对象一定在”。

map 里的 key 谁来清理

弱引用还有一个现实问题:对象被 GC 回收了,但你的 map key 还在。值虽然取出来是 nil,但索引本身会越积越多。这个时候就轮到 runtime.AddCleanup 出场。

可以把它理解成对象被回收后的清理钩子。对象生命周期结束时,我们顺手把缓存里的 key 删除掉,避免 map 变成“空壳索引仓库”。

func (c *Cache) Get(key string) *Schema {
    c.mu.Lock()
    defer c.mu.Unlock()

    if wp, ok := c.m[key]; ok {
        if v := wp.Value(); v != nil {
            return v
        }
    }

    v := &Schema{Name: key}
    c.m[key] = weak.Make(v)

    runtime.AddCleanup(v, func(k string) {
        c.mu.Lock()
        delete(c.m, k)
        c.mu.Unlock()
    }, key)

    return v
}

这段代码表达的是思路,不建议你不加测试直接搬到生产。清理函数里能做什么、不能做什么,要结合官方文档认真看。我的习惯是让 cleanup 尽量短,只做删除索引、释放辅助资源这类动作,不在里面塞复杂业务逻辑。

Go weak.Pointer 代码案例图
代码案例:弱引用的三个重点,强引用会留住对象,弱引用不阻止 GC,Value 必须先判空。

它解决不了什么

弱引用不能替你设计缓存策略。它不知道热点 key 是什么,不知道哪些对象应该保留,也不知道什么时候应该主动淘汰。它只是告诉 GC:如果除了弱引用没人需要这个对象了,你可以回收。

所以它不适合拿来做强一致缓存,也不适合保存必须命中的业务数据。比如用户会话、权限规则、订单状态,这些东西不能因为 GC 心情好就突然没有。弱引用适合“能复用就复用,没了就重建”的派生对象。

生产里我会加哪些保护

  • 所有 Value() 后面都必须判空,review 时看到直接解引用我会拦下来。
  • 缓存构建逻辑必须可重入,因为弱引用失效后会重新创建对象。
  • map 访问仍然要加锁,weak.Pointer 不会让普通 map 变成并发安全。
  • cleanup 函数尽量短,不做网络调用、复杂日志和可能阻塞的操作。
  • 上线前看 heap、对象数量、GC 次数和缓存重建次数,不只看 QPS。

一个容易踩的坑:临时强引用会影响观察结果

你在测试 weak.Pointer 时,经常会发现“为什么 GC 后对象还在”。很多时候不是 weak 失效,而是你自己还拿着强引用,比如局部变量、闭包、slice、日志参数,甚至调试打印都可能延长对象生命。

写测试时最好把创建对象和触发 GC 的阶段隔开,避免无意间留住对象。否则你会误判 weak.Pointer 的行为,进而在生产代码里做出错误假设。

我的建议

weak.Pointerruntime.AddCleanup 是高级工具,不是日常业务 CRUD 的标配。它们适合内存敏感、对象可重建、缓存只是辅助优化的场景。用之前先确认:对象丢了能不能重建?缓存 miss 会不会影响正确性?cleanup 失败会不会影响主流程?

如果这些问题都能答清楚,它们会很有价值。尤其是那些“缓存引用把大对象一直挂住”的老问题,weak.Pointer 给了我们一个更像运行时原生能力的解法。我的态度是:可以用,但要带着指标、测试和 review 清单一起用。

参考资料:Go Blog《Cleanups and weak pointers》、Go 1.24 Release Notes。

声明:本文转载于:Go Blog 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>