享元对象必须不可变以确保线程安全,Go中宜用sync.Pool管理只读结构体实例,避免可变struct复用;字符串和rune本身即天然享元,无需封装;工厂需合理设计key与去重粒度。

享元对象必须是不可变的,否则线程不安全
Go 语言没有内置的享元模式支持,但它的 sync.Pool 和结构体值语义天然适合实现轻量级享元。关键前提是:享元对象本身不能被外部修改,否则在并发场景下会出问题。常见错误是把一个可变的 struct 实例放进池子,然后在多 goroutine 中复用并修改其字段。
正确做法是让享元只暴露只读字段,或用构造函数返回新实例(而非复用后修改)。例如:
type CharFlyweight struct {
char rune
font string
size int
}
// ✅ 安全:构造时定死,无 setter 方法
func NewCharFlyweight(c rune, f string, s int) *CharFlyweight {
return &CharFlyweight{char: c, font: f, size: s}
}
- 不要提供
SetFont()或直接写f.font = "bold" - 如果需要不同样式,应新建另一个
*CharFlyweight,而不是复用+修改 - 若字段较多且初始化成本高,可用
sync.Pool缓存已构造好的只读实例
用 sync.Pool 管理享元实例比 map 查找更高效
很多人第一反应是用 map[string]*Flyweight 做缓存,按 key(如字体+字号+字符)查表复用。这可行,但不是最优——sync.Pool 在高频、短生命周期对象场景下延迟更低,且自动处理 GC 友好回收。
典型误用是把 sync.Pool 当成全局缓存长期持有对象,结果对象被意外回收;或者没设置 New 字段,导致 Get 返回 nil。
-
sync.Pool适用于“创建开销大 + 生命周期短 + 不跨 goroutine 持久化”的享元,比如日志格式器、临时缓冲区、渲染字符单元 - 必须设置
Pool.New字段,否则Get()可能返回 nil - 不要假设
Put(x)后x还能被下次Get()拿到——Pool 不保证强引用
字符串和 rune 是天然享元,别重复封装
Go 中 string 是只读底层数组 + 长度,rune 是 int32 别名,它们本身就是轻量、不可变、可共享的“享元”。很多初学者会为单个字符写 CharFlyweight{rune: 'a'},反而增加内存和间接访问开销。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
真正该用享元的是带状态的复合对象,比如:
- 富文本中“字符 + 字体 + 颜色 + 下划线标记”的组合
- 游戏里“敌人类型 + 行为策略 + AI 配置”的模板
- 解析器中“token 类型 + 关键字映射表指针”的只读上下文
纯数据如 "hello" 或 'x' 直接用原生类型即可,无需包装。
享元工厂要控制唯一性边界,避免过度去重
工厂函数(如 GetFlyweight(key string))常被设计成全局单例,但实际中“唯一性”粒度很重要。例如按字体名去重,但忽略了 DPI 缩放因子,导致高清屏下文字模糊;或按 UTF-8 字节串做 key,却没考虑 Unicode 归一化,导致相同字符因组合符差异被当成两个享元。
- key 应覆盖所有影响行为/表现的参数,缺一不可
- 避免用原始输入直接作 key——先 normalize、trim、转小写等预处理
- 若 key 组合爆炸(如 10 种字体 × 5 种大小 × 65536 个 unicode 码点),要考虑降级策略:只缓存高频字符,其余 new-on-demand
享元模式的价值不在“绝对复用”,而在“可控复用”——你得清楚哪些变体值得缓存,哪些不值得,以及缓存失效的代价是否可接受。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










