享元对象必须不可变以保证线程安全,字段初始化后禁止修改,上下文数据应通过方法参数传入;推荐用 sync.Pool 替代带锁 map 实现工厂;键宜用可比较 struct 而非拼接 string。

享元对象必须是不可变的,否则线程不安全
Go 语言没有原生的“final”或“immutable”修饰符,享元对象一旦被复用,多个 goroutine 同时读写其字段就会引发数据竞争。常见错误是把 sync.Mutex 塞进享元结构体里试图“保护可变状态”,这反而破坏了享元本意——它不是缓存容器,而是共享的只读数据载体。
正确做法是:所有字段在 NewFlyweight() 初始化后就不再修改。若需携带上下文变化的数据(如位置、颜色),应通过方法参数传入,而非存在享元内部。
- 享元结构体所有字段声明为导出(首字母大写)但不提供 setter 方法
- 构造函数返回指针,且内部不做深拷贝;调用方拿到后只读访问
- 如果字段含
map或slice,必须用make创建并立即填充,禁止后续append或delete
用 sync.Pool 替代 map 缓存享元实例更高效
很多人习惯用 map[string]*Flyweight 加 sync.RWMutex 实现享元工厂,但在高并发场景下,锁争用和 GC 压力会抵消复用收益。Go 标准库的 sync.Pool 是专为短期对象复用设计的,无锁、按 P 局部缓存、自动清理,更适合享元模式的生命周期特征。
注意:不能把长期存活的对象(比如全局配置类享元)丢进 sync.Pool,它会在 GC 时清空;真正适合的是高频创建/销毁的轻量对象,如字符样式、网络协议头模板等。
立即学习“go语言免费学习笔记(深入)”;
var fontPool = sync.Pool{
New: func() interface{} {
return &FontStyle{Family: "sans-serif", Size: 14, Bold: false}
},
}
func GetFont(family string, size int, bold bool) *FontStyle {
f := fontPool.Get().(*FontStyle)
// 复位关键字段(仅限可变字段,享元主体仍需只读)
// 注意:此处仅适用于“伪享元”场景,即复用+重置;纯享元应避免 reset
f.Family, f.Size, f.Bold = family, size, bold
return f
}
func PutFont(f *FontStyle) {
fontPool.Put(f)
}
字符串键拼接易引发内存分配,改用 struct 作 map key
当享元工厂基于多个参数(如 fontFamily, fontSize, isBold)生成唯一键时,用 fmt.Sprintf("%s-%d-%t", f, s, b) 每次都会分配新字符串,GC 频繁。Go 允许导出结构体作为 map key,只要所有字段可比较(不含 slice/map/func)。
struct key 不仅零分配,还能天然防止键格式错误(比如漏掉分隔符),也便于调试时直接打印字段值。
- 定义
type FontKey struct { Family string; Size int; Bold bool } - 工厂 map 类型改为
map[FontKey]*FontStyle - 注意:
string字段长度不受限,但实际中字体名通常较短,哈希性能足够
享元不是万能的,小对象复用可能得不偿失
Go 的内存分配器对小对象(new(FontStyle) 的开销远低于查 map + 锁 + 接口转换。实测表明:单个享元对象大小超过 200 字节、且每秒复用超 10 万次时,优化才开始明显。
典型误用是给 Point{x,y int} 或 RGBA{r,g,b,a uint8} 这种 8–16 字节结构套享元——不仅没省内存,还因指针间接访问和缓存未命中拖慢速度。
判断是否该上享元,先跑 pprof:go tool pprof -alloc_space 看堆分配热点,再确认对象是否真被高频重复创建。否则,老老实实让编译器帮你做逃逸分析和栈分配。











