享元对象必须不可变,Go中需通过小写字段、只读方法和工厂函数保障;外蕴状态须由调用方传入,不可塞入享元;高频场景用sync.Map替代map防并发panic;低频或对象唯一时不宜强行享元化。

享元对象必须是不可变的,否则共享会出问题
Go 里没有语言级的“不可变”约束,Flyweight 的核心前提是你不能让外部修改它的内部状态。一旦多个地方共享同一个结构体实例,而某个调用方偷偷改了 name 或 color 字段,其他使用者就全被污染了。
实操建议:
- 把享元结构体字段全设为小写(未导出),只提供只读方法,比如
func (f *CharFlyweight) Glyph() rune - 构造时用私有字段 + 公共工厂函数,禁止直接
&CharFlyweight{...} - 如果字段本身是引用类型(如
*sync.Map),更要小心——哪怕字段名小写,也得确保它不被外部拿到指针后篡改
用 sync.Map + 字符串做键,比 map[string]*Flyweight 更安全
高频创建字符享元(比如渲染文本时逐字解析)时,普通 map 并发读写会 panic,而 sync.Map 虽然读性能略低,但省去了手动加锁的复杂度,且避免了 map iteration modified concurrently 这类错误。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 用全局
var pool = make(map[string]*CharFlyweight),多 goroutine 写入直接崩溃 - 用
sync.RWMutex包裹 map,但忘记在读路径上加RLock(),导致数据竞争(go run -race会报)
推荐写法:
var charPool = &sync.Map{}
func GetCharFlyweight(ch rune) *CharFlyweight {
if val, ok := charPool.Load(ch); ok {
return val.(*CharFlyweight)
}
f := &CharFlyweight{glyph: ch}
charPool.Store(ch, f)
return f
}
不要把上下文状态塞进享元,那是 FlyweightFactory 的职责
享元模式的关键是“内蕴状态”(intrinsic state)和“外蕴状态”(extrinsic state)分离。比如字体渲染中,字符形状(glyph)是内蕴的,可共享;而位置(x/y)、字号、颜色是外蕴的,必须由调用方传入。
容易踩的坑:
- 在
CharFlyweight里加fontSize int字段,以为能复用——结果不同字号混用导致显示错乱 - 工厂函数签名写成
GetCharFlyweight(ch rune, size int),把外蕴参数当成构造条件,破坏享元唯一性 - 用 struct{} 作为 map 键来组合多个参数(如
struct{ch rune; size int}),这已经不是享元,是缓存策略了
正确做法:享元只管“是什么”,不管“在哪、多大、什么色”。所有动态信息走方法参数传入,例如:
func (f *CharFlyweight) Render(x, y int, size float64, color Color) { ... }
内存节省效果取决于重复率,别在低频场景硬套
享元不是银弹。如果对象种类太多(比如支持 Unicode 全量字符),sync.Map 本身就有额外内存开销;如果每种对象只创建一次,还强行池化,反而增加间接寻址和 GC 扫描负担。
判断是否该用:
- 对象字段高度重复(如日志系统中固定几十个
level字符串)→ 适合 - 对象生命周期极短,且创建/销毁频繁(如 HTTP 请求中临时 token 解析器)→ 不适合,用
sync.Pool更合适 - 需要按需加载或带懒初始化逻辑(如字体文件延迟解压)→ 享元工厂里加
sync.Once,但得测清初始化锁争用
一个常被忽略的点:Go 的 struct 在栈上分配很快,真正吃内存的是指针逃逸和堆上大对象。先用 pprof 确认 heap_inuse 里真有大量重复小对象,再动手重构。










