当前位置:首页 > 文章列表 > Golang > Go教程 > Go unique.Make 怎么复用大量重复字符串值

Go unique.Make 怎么复用大量重复字符串值

来源:17golang原创 2026-10-06 14:54:13 0浏览 收藏

当大量对象反复保存同样的服务名、区域、状态或标签值时,可以把这些字符串先交给 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、哈希、随机串通常不适合几乎没有相等值可复用
短生命周期的小字符串先测量额外的规范化成本可能高于收益
重复字符串、unique.Make 与规范句柄的静态关系图
图1:多个重复输入通过 unique.Make 对应到规范句柄;图中表达静态身份关系,不是运行流程或内存测量结果。

在对象里保存 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,也避免内部热路径过早退回字符串比较。

领域对象、句柄索引与字符串输出边界的静态关系图
图2:领域对象和聚合索引保留句柄,日志与序列化边界通过 Value 取得字符串;各连接表示依赖关系,不表示执行时序。

不要把 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:频繁在句柄与字符串边界转换会增加模型复杂度。
  • 还没有测到问题:先用真实数据统计重复率、对象数量和存活时间,再决定是否引入。

评估时至少记录三项:不同值数量、总对象数量、对象平均存活时间。不同值只有几十个、对象却有数十万且长期驻留,是很强的候选;两者接近一比一时通常不是。

改造检查清单

  1. 确认项目最低 Go 版本不低于 1.23。
  2. 选择高重复且经常比较的字符串字段,不要一次改造所有文本。
  3. 把内部字段改为 unique.Handle[string],在统一构造入口调用 unique.Make。
  4. 让分组键、去重键和相等判断继续使用句柄。
  5. 只在日志、序列化、持久化等边界调用 Value()。
  6. 用真实负载比较改造前后的内存占用和热点,不凭对象数量直接下结论。

相关问题

unique.Make 只能处理字符串吗?

不是。泛型参数 T 只要求满足 comparable,字符串只是最常见示例。由可比较字段组成的结构体也可以规范化。

两个相同字符串得到的 Handle 一定相等吗?

是。官方契约规定,两个句柄相等,当且仅当创建它们的值相等;不同具体类型的句柄不能混用。

调用 Value 会复制整段字符串内容吗?

API 描述是返回产生句柄的值的浅拷贝。对字符串值来说,调用方得到普通字符串值;本文不把它描述成一次新的逐字节深拷贝。

句柄会让规范值永远留在内存吗?

不会无限期强留所有历史值。官方说明是:只要某个值的句柄仍存在,规范副本会被保留;相关句柄都消失后,内部条目可在之后被回收。

可以把 Handle 写入数据库吗?

不建议把句柄内部表示当成持久化格式。写库或传输时应使用 Value() 还原业务值,读取后再调用 unique.Make 建立当前进程内的规范身份。

unique.Make 的正确用法可以概括为一句话:内部模型保留句柄,热路径比较句柄,外部边界取回值。只要重复率与生命周期确实匹配,这种建模就能把大量重复字符串从“每个对象都保存一份”变成“对象共享规范身份”。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Redis 8.6 Streams 幂等生产怎么减少重复消息Redis 8.6 Streams 幂等生产怎么减少重复消息
上一篇
Redis 8.6 Streams 幂等生产怎么减少重复消息
Java HttpClient 怎么把响应体按行异步消费
下一篇
Java HttpClient 怎么把响应体按行异步消费
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    347次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    410次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    411次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    369次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    195次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码