登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go unique 包什么时候不适合替代业务缓存

来源:17golang原创

时间:2026-10-06 15:33:33 467浏览 收藏

unique 不适合替代需要 TTL、容量上限、主动失效、加载函数、错误处理、命中统计或跨进程共享的业务缓存。它解决的是另一件事:把相等的可比较值规范化,并返回可高效比较的 Handle[T]。

官方资料:https://pkg.go.dev/unique、https://go.dev/blog/unique 与 https://go.dev/doc/go1.23

如果需求是“同值共用规范身份”,考虑 unique;如果需求是“按键保存可失效的计算结果”,继续使用业务缓存。两者可以协作,但职责不能互换。

一个看似能省掉缓存的场景

服务发现代码里经常出现重复的服务名,同时还要缓存服务配置。看到 unique.Make 能去重值后,很容易产生一个误解:既然它内部会管理规范副本,是不是可以把原来的配置缓存删掉?

答案是否定的。服务名规范化只解决“多个相等字符串如何共享身份”;配置缓存还要回答“结果何时过期、失败是否重试、最多保存多少项、配置更新后如何失效”。这些都不在 unique 的公开契约里。

unique 的能力边界只有规范化与句柄

Go 1.23 引入 unique。公开 API 很小:Make[T comparable] 返回句柄,Value() 取回产生句柄的值的浅拷贝。两个句柄相等,当且仅当创建它们的值相等;Make 与 Value 都允许多 goroutine 并发调用。

这组 API 没有提供下列业务缓存能力:

  • 按时间过期或滑动续期;
  • 容量上限、LRU/LFU 等淘汰策略;
  • 主动删除某个业务键;
  • 缓存未命中时加载结果并返回错误;
  • 命中率、回源次数和驱逐次数统计;
  • 磁盘持久化或跨进程共享。

Go 官方博客说明,规范副本的保留与句柄是否仍存在相关:当某个值的句柄都消失后,内部条目会具备后续回收条件。这是内存生命周期策略,不是可配置的业务 TTL。

unique 规范化能力与业务缓存职责的静态边界图
图1:unique 只覆盖可比较值、规范副本与 Handle;TTL、容量、失效和加载错误属于业务缓存。

正确拆分:句柄层与配置缓存层并存

更稳妥的实现是让 unique.Handle[string] 只承担内部服务标识,缓存仍保存配置结果和过期时间。

package configcache

import (
	"sync"
	"time"
	"unique"
)

type ServiceID = unique.Handle[string]

type Config struct {
	Endpoint string
	Headers  map[string]string
}

type entry struct {
	value     Config
	expiresAt time.Time
}

type Cache struct {
	mu    sync.RWMutex
	items map[ServiceID]entry
}

func New() *Cache {
	// 业务缓存显式拥有容量与生命周期策略的落点
	return &Cache{items: make(map[ServiceID]entry)}
}

func Service(name string) ServiceID {
	// 重复服务名在进程内得到相等句柄
	return unique.Make(name)
}

这里的 Config 含有 map 字段,本身不满足 comparable,也不能直接交给 unique.Make。这恰好说明两层职责不同:服务名适合规范化,配置对象适合由缓存持有和更新。

TTL 与主动失效必须由缓存实现

package configcache

import "time"

func (c *Cache) Get(id ServiceID, now time.Time) (Config, bool) {
	c.mu.RLock()
	item, ok := c.items[id]
	c.mu.RUnlock()

	// 过期判断是业务缓存策略,不属于 unique
	if !ok || !now.Before(item.expiresAt) {
		return Config{}, false
	}
	return item.value, true
}

func (c *Cache) Put(id ServiceID, value Config, ttl time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// TTL 明确写在缓存条目中,便于测试和观测
	c.items[id] = entry{value: value, expiresAt: time.Now().Add(ttl)}
}

func (c *Cache) Invalidate(id ServiceID) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// 配置变更后可以主动删除,行为不依赖垃圾回收
	delete(c.items, id)
}

这个最小实现还没有容量淘汰和单飞回源,但至少把关键语义放在了可见代码中。生产系统可以继续加入最大条目数、后台清理、指标和加载器;句柄只作为可比较键,不参与策略决策。

服务句柄层与配置缓存层的静态依赖图
图2:服务名通过 unique.Make 得到 ServiceID,配置缓存以句柄为键并独立管理 Config、TTL 与主动失效。

这五类需求不要用 unique 顶替

真实需求为什么 unique 不够应保留的组件
接口结果 30 秒过期没有 TTL API带过期时间的内存缓存
最多保存 10000 项没有容量或淘汰策略有界 LRU/LFU 缓存
配置发布后立即失效没有业务删除接口支持 Invalidate 的缓存
未命中时调用远端不管理加载结果与错误缓存加载器与并发合并
多个实例共享数据规范身份限于当前进程远程缓存或数据库

性能检查应先看数据形态

unique 适合高重复、较长生命周期、频繁比较的可比较值。如果服务名几乎都不同,或者只在一次请求中短暂出现,额外的全局规范化查找可能没有收益。反过来,即使服务名高度重复,也不能据此删除 TTL 缓存,因为“键是否重复”和“结果何时失效”是两个独立问题。

评估时建议分别记录:键的重复率、句柄存活时间、原值比较热点;以及缓存的命中率、回源延迟、过期数量和容量压力。只有前一组指标支持规范化,后一组指标决定缓存策略。

边界检查清单

  1. 要保存的是规范值身份,还是可失效计算结果?
  2. 是否需要 TTL、容量限制、手工失效或命中统计?
  3. 值是否满足 comparable,重复率是否足够高?
  4. 数据是否需要跨进程、跨重启或持久化?
  5. 句柄消失后的回收语义,是否被误当成业务过期时间?

相关问题

unique.Make 能缓存函数计算结果吗?

它只能规范化传入的可比较值,不接受加载函数,也不记录计算错误。结果缓存仍需单独实现。

Handle 可以作为业务缓存的 map 键吗?

可以,这正是两层协作方式之一。但缓存值、TTL、锁和失效逻辑仍由业务缓存负责。

为什么不能把 Config 直接放进 unique.Make?

类型参数要求 comparable。包含 map、slice 或 function 字段的结构体不满足这个约束。

句柄都消失是否等于缓存过期?

不等于。那只表示规范副本可以在之后被回收,没有业务截止时间、通知或主动失效保证。

什么时候只用 unique 就够了?

当需求仅是规范化重复值、便宜地比较相等性或把句柄作为内部索引键,并且不需要保存另一份可失效结果时。

最简单的判断是:unique 管“这个值是谁”,业务缓存管“这个键对应什么结果、还能用多久”。把两句话分开,设计就不容易走偏。

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