Go unique.Make 怎么复用大量重复字符串值
当大量对象反复保存同样的服务名、区域、状态或标签值时,可以把这些字符串先交给 unique.Make,并在对象里保存 unique.Handle[string]。相等字符串会得到相等句柄,业务代码可以直接比较句柄;只有在打印、序列化或调用旧接口时,才通过 Value() 取回字符串。
官方资料:https://pkg.go.dev/unique 与 https://go.dev/blog/unique
unique适合“值高度重复、对象活得较久、比较较频繁”的场景。关键不是每次调用Make后立刻取出字符串,而是让业务对象持续保存句柄;否则规范副本的长期保留就失去了明确依托。
unique.Make 解决的是值规范化,不是字符串压缩
Go 1.23 把 unique 加入标准库,用于规范化(canonicalize,也常称 interning)任何满足 comparable 约束的值。unique.Make(v) 返回一个全局唯一身份的 Handle[T]:两个句柄相等,当且仅当创建它们的值相等。
这项能力带来两个直接结果:
- 大量相同值可以指向同一个规范副本,而不是让每个业务对象各自保存一份;
- 比较两个句柄是简单操作,通常比逐字节比较长字符串更便宜。
不过,unique 不会把内容相似但不相等的字符串合并,也不会压缩字符串内部字符。"api-prod" 与 "api-prod " 是两个不同值,大小写不同也不会自动归一。因此,是否要先做去空格、大小写或别名规范化,是业务层自己的决定。
先判断这批字符串是否值得规范化
最合适的候选通常是低基数、高重复的字段。例如一百万条事件只包含几十个服务名、几个区域和少量状态值。相反,如果字段是请求 ID、用户自由文本或随机令牌,几乎每个值都不同,建立规范身份通常得不到复用收益。
| 字段特征 | 是否适合 | 原因 |
|---|---|---|
| 服务名、区域、状态 | 通常适合 | 重复率高,常参与分组和比较 |
| 长生命周期配置键 | 可能适合 | 相同值会被许多对象长期引用 |
| 请求 ID、哈希、随机串 | 通常不适合 | 几乎没有相等值可复用 |
| 短生命周期的小字符串 | 先测量 | 额外的规范化成本可能高于收益 |

在对象里保存 Handle,而不是继续保存原字符串
下面的原创示例把日志事件中的服务名和区域改为句柄。构造对象时统一调用 unique.Make,后续比较、分组和索引都可以使用句柄:
package event
import "unique"
type Event struct {
// 高重复标签保存为句柄,避免每个事件继续持有原始字符串字段
Service unique.Handle[string]
Region unique.Handle[string]
Count int
}
func New(service, region string, count int) Event {
return Event{
// 相等字符串会得到可直接比较的相等句柄
Service: unique.Make(service),
Region: unique.Make(region),
Count: count,
}
}
func SameRoute(a, b Event) bool {
// 热路径只比较两个句柄,不重复扫描字符串内容
return a.Service == b.Service && a.Region == b.Region
}
如果连续创建很多 New("gateway", "cn-east", 1),这些对象的两个标签会对应相同句柄。句柄本身仍是一个值,复制 Event 时可以正常复制;官方文档也明确说明 Make 可被多个 goroutine 并发调用。
这里最重要的建模变化是:对象字段类型真的变成了 unique.Handle[string]。如果对象仍保存原始 string,只在旁边临时创建句柄,那么原有字段占用并不会因此消失。
分组和查找用句柄,输出边界再取 Value
Handle[T] 是可比较的,因此可以直接作为 map 键。下面按服务名汇总计数:
package event
import "unique"
func CountByService(events []Event) map[unique.Handle[string]]int {
totals := make(map[unique.Handle[string]]int)
for _, item := range events {
// 句柄直接作为键,避免反复比较长字符串内容
totals[item.Service] += item.Count
}
return totals
}
func PrintableName(handle unique.Handle[string]) string {
// 日志、JSON 或旧接口边界需要文本时再取回字符串
return handle.Value()
}
官方 API 说明,Value() 返回创建句柄时那个值的浅拷贝,并且可以安全地被多个 goroutine 调用。对字符串来说,业务代码得到的仍是普通 string,可以交给编码器、日志库或模板。
工程上可以把句柄留在内部领域模型,把 Value() 放在边界层:例如 DTO 转换、JSON 组装、日志字段生成或数据库写入。这样既不要求外部组件理解 unique.Handle,也避免内部热路径过早退回字符串比较。

不要把 Make(s).Value() 当成长期透明驻留
一种看似省改动的写法是:
func CanonicalText(s string) string {
// 这种写法立即丢弃句柄,不适合作为长期复用契约
return unique.Make(s).Value()
}
它可能在一段时间内得到复用效果,但不能替代“业务对象保留句柄”的设计。Go 官方博客特别说明:句柄存在期间,内部映射会保留规范副本;当指向某个值的句柄都不再存在后,对应条目会被标记为可删除,并在之后由垃圾回收相关机制清理。
因此,Make(s).Value() 更像一个有生命周期限制的折中办法,不应该被描述成永久字符串池。需要稳定复用时,让对象、缓存键或索引真正持有 Handle[string];需要普通字符串时再调用 Value()。
并发使用时不需要再套一层全局锁
unique.Make 与 Handle.Value 的官方契约都允许多 goroutine 并发使用。业务代码通常不需要为标准库内部表再加全局互斥锁,也不要另建一个永不清理的 map[string]string 与它重复工作。
但业务容器本身仍遵循普通并发规则。例如多个 goroutine 同时写入 map[unique.Handle[string]]int 仍需要锁、分片或其他并发结构;线程安全的是 unique 的 API,不是你外层的普通 map。
什么时候应该保持普通 string
- 值几乎都唯一:随机 ID、完整 URL、自由文本通常缺少可复用副本。
- 数据很快释放:一次函数调用内就结束的短字符串,改造字段类型可能不划算。
- 只偶尔比较:如果字段只是展示,句柄比较优势难以体现。
- 外部 API 全部要求 string:频繁在句柄与字符串边界转换会增加模型复杂度。
- 还没有测到问题:先用真实数据统计重复率、对象数量和存活时间,再决定是否引入。
评估时至少记录三项:不同值数量、总对象数量、对象平均存活时间。不同值只有几十个、对象却有数十万且长期驻留,是很强的候选;两者接近一比一时通常不是。
改造检查清单
- 确认项目最低 Go 版本不低于 1.23。
- 选择高重复且经常比较的字符串字段,不要一次改造所有文本。
- 把内部字段改为
unique.Handle[string],在统一构造入口调用unique.Make。 - 让分组键、去重键和相等判断继续使用句柄。
- 只在日志、序列化、持久化等边界调用
Value()。 - 用真实负载比较改造前后的内存占用和热点,不凭对象数量直接下结论。
相关问题
unique.Make 只能处理字符串吗?
不是。泛型参数 T 只要求满足 comparable,字符串只是最常见示例。由可比较字段组成的结构体也可以规范化。
两个相同字符串得到的 Handle 一定相等吗?
是。官方契约规定,两个句柄相等,当且仅当创建它们的值相等;不同具体类型的句柄不能混用。
调用 Value 会复制整段字符串内容吗?
API 描述是返回产生句柄的值的浅拷贝。对字符串值来说,调用方得到普通字符串值;本文不把它描述成一次新的逐字节深拷贝。
句柄会让规范值永远留在内存吗?
不会无限期强留所有历史值。官方说明是:只要某个值的句柄仍存在,规范副本会被保留;相关句柄都消失后,内部条目可在之后被回收。
可以把 Handle 写入数据库吗?
不建议把句柄内部表示当成持久化格式。写库或传输时应使用 Value() 还原业务值,读取后再调用 unique.Make 建立当前进程内的规范身份。
unique.Make 的正确用法可以概括为一句话:内部模型保留句柄,热路径比较句柄,外部边界取回值。只要重复率与生命周期确实匹配,这种建模就能把大量重复字符串从“每个对象都保存一份”变成“对象共享规范身份”。
Redis 8.6 Streams 幂等生产怎么减少重复消息
- 上一篇
- Redis 8.6 Streams 幂等生产怎么减少重复消息
- 下一篇
- Java HttpClient 怎么把响应体按行异步消费
-
- Golang · Go教程 | 49分钟前 | 标准库 · go · slog · 日志排查 · 结构化日志 slog.Handler LogAttrs Go slog.GroupAttrs 空日志分组
- Go slog.GroupAttrs 怎么避免空日志分组
- 455浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go 项目用 docker init 生成容器配置后哪些默认值必须改
- 167浏览 收藏
-
- Golang · Go教程 | 2小时前 | Go教程 · Go 结构化日志 日志脱敏 slog ReplaceAttr
- Go slog.ReplaceAttr 怎么统一脱敏日志字段
- 100浏览 收藏
-
- Golang · Go教程 | 2小时前 | 迭代器 · Go教程 · Go 切片 iter.Seq2 slices.Backward 反向遍历
- Go slices.Backward 怎么反向遍历而不复制切片
- 501浏览 收藏
-
- Golang · Go教程 | 3小时前 | Go教程 · 批处理 Go 切片 迭代器 slices.Chunk
- Go slices.Chunk 怎么按固定大小遍历切片
- 487浏览 收藏
-
- Golang · Go教程 | 3小时前 | go · 泛型 ·
- Go maps.EqualFunc 怎么比较值类型不同的映射
- 284浏览 收藏
-
- Golang · Go教程 | 5小时前 | 标准库 · go · 工程实践 · Go迭代器 iter.Seq2 Go maps.Collect slices.All map构造
- Go maps.Collect 怎么从键值序列构造 map
- 236浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 347次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 410次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 411次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 369次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 195次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览

