unique.Make 长期运行时会不会无限保留值
前段时间我把一个服务里的复合键从字符串改成了 unique.Handle。比较变快、键也更紧凑,但上线前我突然想到一个问题:unique.Make 背后有一张全局规范值表,服务又可能连续运行几个月,这张表会不会只增不减,最后把所有见过的值都留在内存里?
先给结论:按设计不会永久保留所有已经失去 Handle 的值,但它也不是带容量上限的缓存。只要某个 Handle[T] 仍然可达,对应的规范副本就必须存活;最后一个 Handle 消失后,内部弱引用机制才有机会清理条目,而且清理时机受垃圾回收影响,并不承诺立刻发生。更重要的是,Go 官方目前仍有一个高基数 Linux 负载下内存持续增长的开放问题,所以不能把“理论上可回收”直接等同于“任何长期服务都有稳定内存上界”。
Go unique 官方文档:https://pkg.go.dev/unique
Go 官方设计说明:https://go.dev/blog/unique
当前已知问题:https://github.com/golang/go/issues/71772
先把“会不会无限保留”拆成两层
第一层是语义问题:一个值曾经传给 unique.Make,是不是从此就永远不能回收?答案是否定的。unique 的实现使用运行时支持的弱引用来维护规范值;如果程序已经没有任何对应 Handle,规范值可以在后续回收过程中变成可清理对象。
第二层是运行问题:服务不断接收从未重复的新值时,堆内存是否一定能迅速回落?这里不能给绝对保证。GC 何时发生、弱引用表何时整理、程序是否仍把 Handle 放在某个容器中、运行平台是否命中已知问题,都会改变实际曲线。也就是说,unique 提供的是身份规范化和可回收机制,不是 TTL、LRU、最大条目数或固定内存预算。
为什么普通 map 会永久积累
如果我们自己用 map[T]*T 做驻留表,map 的值会形成强引用。哪怕业务已经不再使用这些键,除非显式删除,规范副本仍然被 map 指着,GC 无法判断它已经过期。高基数输入下,这就是一张典型的只增不减表。
unique.Make 解决的核心并不是“帮我缓存所有值”,而是“相等的 comparable 值得到相等的 Handle”。Handle 可以直接比较,适合把体积较大的可比较值压缩成稳定身份。Go 1.23 引入这个包后,调用方式很简单:
package main
import (
"fmt"
"unique"
)
func main() {
// 相同值会得到相等的 Handle,比较成本很低
a := unique.Make("ap-southeast-1")
b := unique.Make("ap-southeast-1")
fmt.Println(a == b)
}
这段代码里,两个输入字符串相等,因此两个 Handle 也相等。Make 可以被并发调用,Value() 则返回原始值的浅拷贝。浅拷贝意味着结构体中的指针、切片头或其他引用型字段仍可能让其指向的数据独立存活,但保存一个 Value() 的结果并不等于保存规范身份本身。
Handle 才是保留规范副本的关键

我最初误以为只要调用过 unique.Make(v),全局表就会永久记住 v。更准确的理解是:内部表需要保证所有仍存活的 Handle 指向同一个规范值,因此可达的 Handle 才是保留关系的业务起点。最常见的长期保留并不是运行时泄漏,而是应用把 Handle 放进了一个没有淘汰策略的 map、切片或全局对象。
例如,活跃会话表保存 Handle 是合理的,但会话结束后必须删除对应项:
package session
import "unique"
type SessionKey struct {
Tenant string
User string
}
var active = map[string]unique.Handle[SessionKey]{}
func addSession(id string, key SessionKey) {
// 业务 map 保存 Handle 时,规范副本必须继续存活
active[id] = unique.Make(key)
}
func removeSession(id string) {
// 删除最后一份 Handle 后仅表示可回收,不表示立刻释放
delete(active, id)
}
如果 active 的条目持续增长,内存增长首先应该归因于业务容器,因为 Handle 明确仍在使用。只有确认业务侧已经释放 Handle,才需要继续观察弱引用表和 GC 行为。
低基数复用和高基数流入不是一回事

在我的服务里,区域、协议、资源类型等值总数很少,而且会被大量重复比较,这正是 unique.Handle 擅长的负载。即使这些 Handle 长期存在,保留的规范副本数量也接近业务基数,成本容易估算。
反过来,如果输入是请求 ID、一次性令牌、随机路径或不断增长的用户生成字符串,重复率很低,每次 Make 都可能创建新的规范值。即使短生命周期 Handle 最终都消失,峰值仍会跟分配速率、GC 周期和清理能力相关。此时 unique 的比较收益很小,却把高基数压力放进了进程级规范化设施。
| 场景 | 判断 | 原因 |
|---|---|---|
| 区域、状态、协议等稳定值集合 | 适合 | 基数低、复用率高,Handle 比较能持续获益 |
| 有明确删除路径的活跃会话表 | 有条件适合 | 需要保证业务容器释放 Handle,并验证实际平台内存曲线 |
| 请求 ID、随机串、不断新增的用户输入 | 不推荐 | 基数接近调用次数,规范化收益低,回收压力高 |
| 要求 TTL、容量上限或按成本淘汰 | 不适合 | 应使用真正的有界缓存,而不是把 unique 当缓存 |
临时去重技巧也不等于有界缓存
Go 官方文章提到过一种技巧:立即调用 unique.Make(v).Value(),不保存 Handle,只暂时利用规范副本完成去重。因为 Handle 随即失去引用,旧条目之后可以被回收;官方说明至少要等下一次 GC 完成后,相关旧项才可能删除。
这个技巧适合解决短期分配重复,不适合需要稳定规范身份的业务。它没有 TTL,没有容量契约,也没有“调用结束立即释放”的承诺。若程序还需要在未来用 Handle 比较身份,就不应该把 Handle 提前丢掉。
当前 Linux 高基数场景要额外谨慎
截至本文写作时,Go 官方问题 #71772 仍处于开放状态,并标记为需要修复,目标里程碑指向 Go 1.28。报告描述了一个高变化、高基数工作负载:在 Linux/amd64 上使用 Go 1.23.6 和 Go 1.24.0 时观察到内存持续增长,而同一报告中的 macOS 环境没有复现。
这并不能证明所有 Linux 服务都会泄漏,也不能说明 unique 的设计已经失效;它说明平台、工作负载和具体 Go 版本会影响实际结果。只要服务会连续接收大量新值,就应把该问题当成上线评估条件,而不是只凭 API 文档推断内存一定会回落。
我会把采用标准定得更务实:低基数、重复率高的集合可以优先尝试;高基数、外部可控输入先不要全量切换,至少要在与生产相同的操作系统、架构和 Go 小版本上压测。
上线前怎么观察和回滚
观察时不要只看某一刻的 RSS。至少同时记录堆分配量、堆对象数、GC 周期、输入去重前后的基数、业务容器中的 Handle 数量,并在真实负载下查看 pprof。关键判断是:输入停止增长、业务 Handle 已释放并经历多轮自然 GC 后,堆是否趋于稳定。
隔离测试里可以在固定阶段主动触发 GC,帮助区分“尚未回收”和“持续保留”;生产代码不应为了压低曲线频繁调用 runtime.GC(),那会把延迟和 CPU 成本转嫁给请求。测试还要覆盖实际部署平台,因为当前开放问题已经展示了平台差异。
回滚方案最好在改造时就准备好。如果 Handle 只用于进程内 map 键,可以保留原始 comparable 键类型的实现,通过配置切换;灰度阶段还可以双路计算并校验查找结果,但只让一条路径承担正式读写。这样一旦发现内存曲线异常,通常无需数据格式迁移,就能退回原始键。
适合谁,不适合谁
unique.Handle 最适合“值可比较、重复很多、身份比较频繁”的场景,例如结构化标签、配置键、协议元组和稳定资源类型。它的价值是用规范身份换取便宜比较,而不是替代缓存系统。
如果你的真正目标是限制内存、过期用户数据或按访问热度淘汰,应该使用有容量和生命周期策略的缓存。如果输入几乎从不重复,直接保存原值往往更简单。我的最终判断是:把 unique 当身份工具时很好用;把它当无限容量缓存时,风险就来自对语义的误解。
相关问题
只保存 Value,不保存 Handle,会怎样?
Value() 返回浅拷贝。没有 Handle 后,规范身份本身可以进入可回收状态,但浅拷贝内部引用的对象仍可能因为这个副本而继续存活。
最后一个 Handle 删除后,内存会马上下降吗?
不会承诺马上下降。它只表示对象满足了后续回收的必要条件,实际变化取决于 GC、内部清理和分配器行为。
怎样确认适合自己的服务?
先统计值基数和重复率,再在生产同平台上运行长时间压测,观察 Handle 容器大小与堆曲线是否同步增长。低基数复用是积极信号,高基数持续新增则应优先选其他方案。
CNCF 安全控制目录机器可读化解决了什么问题
- 上一篇
- CNCF 安全控制目录机器可读化解决了什么问题
- 下一篇
- PHP readonly 类继承时有哪些属性限制
-
- Golang · Go问答 | 16分钟前 | go · error · 迭代器 · Go迭代器 error处理 Go iter.Seq
- 迭代器里的错误应该通过什么方式返回
- 171浏览 收藏
-
- Golang · Go问答 | 37分钟前 | go · 迭代器 ·
- 单次迭代器被重复 range 会发生什么
- 270浏览 收藏
-
- Golang · Go问答 | 59分钟前 |
- iter.Seq 提前停止后为什么生产者仍在运行
- 269浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Handle 值相等是否意味着原始对象地址相同
- 390浏览 收藏
-
- Golang · Go问答 | 2小时前 |
- unique.Handle 能否用于包含切片的值
- 388浏览 收藏
-
- Golang · Go问答 | 2小时前 | GC · Go问答 · Go 垃圾回收 weak.Pointer runtime.KeepAlive 对象可达性
- 对象仍被使用时 weak 指针为什么可能已经失效
- 195浏览 收藏
-
- Golang · Go问答 | 3小时前 |
- weak.Pointer.Value 偶尔返回零值是不是数据竞争
- 153浏览 收藏
-
- Golang · Go问答 | 3小时前 | 并发 · 基准测试 · go · RunParallel B.Loop Go并行基准测试 PB.Next testing.Benchmark
- 并行基准测试能否直接改用 B.Loop
- 142浏览 收藏
-
- Golang · Go问答 | 3小时前 |
- testing.B.Loop 为什么不再需要手动读取 b.N
- 497浏览 收藏
-
- Golang · Go问答 | 4小时前 | go · testing · Go问答 · testing.B.Loop Go基准测试 循环外变量 benchmark状态
- B.Loop 中修改循环外变量为什么会影响基准结果
- 105浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 386次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 468次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 475次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 416次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 242次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览

