享元对象必须不可变,内在状态只读且由外部传入外在状态;键需具备强聚合性,用sync.Map实现无锁池时须避免LoadOrStore副作用,核心是正确划分状态边界。

享元对象必须是不可变的,否则复用会出问题
Go 里没有语言级的“不可变对象”约束,Flyweight 类型一旦被多个地方共享,任何一处修改它的字段(比如 state 字段),其他使用者立刻看到副作用。这不是线程安全问题,而是设计错误。
常见错误现象:GetFlyweight("A") 返回的对象,在某处调用了 f.Color = "red",下一次 GetFlyweight("A") 拿到的就不是初始状态了。
- 所有内部字段应设为只读语义:用首字母小写的字段 + 只读 getter 方法,或直接声明为
type Flyweight struct{ name string }并禁止导出可变字段 - 需要变化的数据(如坐标、渲染状态)必须由外部传入,不能存在
Flyweight实例中 —— 这就是「内在状态」和「外在状态」的分界线 - 如果真要带少量可变字段,只能靠文档强约定 + 单元测试覆盖,但不推荐
用 sync.Map 实现线程安全的享元池,别自己锁整个 map
多个 goroutine 同时调用 GetFlyweight(key) 时,若用普通 map + 全局 sync.Mutex,会成为性能瓶颈;而用 sync.Map 能避免锁竞争,但要注意它不支持原子性“查+存”操作。
使用场景:高并发创建大量相似对象(比如日志格式器、HTTP 客户端配置模板、图形渲染中的字体/纹理句柄)。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:先
Load,命中则返回;未命中则新建,再用Store写入 —— 多个 goroutine 可能同时新建,但最多只有一个写入成功,其余丢弃,这是可接受的冗余 - 错误做法:用
map[string]*Flyweight+mu.Lock()包裹整个Get流程,吞吐量直线下滑 -
sync.Map的LoadOrStore看似方便,但它要求构造函数无副作用(不能含 I/O、阻塞、随机数等),否则重复执行可能引发意外
享元模式在 Go 中常被误用成“单例工厂”,其实它解决的是内存爆炸
很多人写了个 NewFlyweightFactory(),缓存一堆 *Flyweight,就以为实现了享元。但没控制对象粒度,反而让内存更糟 —— 比如为每个用户 ID 创建一个 Flyweight,那跟不缓存没区别。
性能影响:享元真正起效的前提是「键空间远小于实例总数」。例如:100 种字体样式,但要渲染 10 万段文本;或 5 种数据库连接配置,却要建立 2 万次 session。
- 检查你的
key是否真的具备强聚合性 —— 如果key基本唯一(如时间戳、UUID、自增 ID),享元不仅无效,还增加哈希查找开销 - 用
runtime.ReadMemStats对比启用前后Alloc和HeapObjects,才能确认是否真减少了堆对象数量 - Go 的 GC 效率高,小对象复用收益有限;重点应放在大结构体(> 1KB)或频繁分配的类型上
不要把指针当成享元,interface{} 或反射擦除类型信息后无法复用
有人把 func() interface{} 当作享元工厂,返回 interface{} 包装的值,结果发现每次调用都新分配 —— 因为 interface{} 底层仍会拷贝底层数据,且类型信息丢失导致无法做 key 判断。
错误现象:cache.Store("A", SomeStruct{X: 1}) 和 cache.Store("A", SomeStruct{X: 1}) 存了两次,因为 SomeStruct 是值类型,两次传参生成两个独立实例。
- 享元必须是引用类型(
*Flyweight),且 key 必须能稳定映射到同一地址 - 避免用
fmt.Sprintf拼接 key,字符串拼接本身有分配;优先用预定义常量或结构体字段组合(如struct{ family, size int })作为 key - 如果 key 类型复杂,记得实现
Equal和Hash(用golang.org/x/exp/maps或自定义哈希函数),否则map查找失效










