享元对象必须不可变以保证线程安全,Go中需通过设计约束实现:字段导出且只读、无setter、上下文差异外置、工厂用sync.RWMutex保护map。

享元对象必须是不可变的,否则线程不安全
Go 没有内置的“享元模式”关键字或标准库支持,它完全靠设计约束实现。核心前提是:共享的对象(Flyweight)在创建后不能被修改。一旦允许 SetXxx() 或直接赋值字段,多个 goroutine 并发访问时就会出现数据竞争——go run -race 很快能抓到问题。
实操建议:
- 把享元结构体的所有字段都设为导出且只读(即用大写字母开头,但不提供 setter 方法)
- 构造函数返回新实例时,用字面量或
&Struct{...}初始化,避免传入可变指针 - 如果需携带上下文差异(如颜色、位置),这些应作为参数传给
Operation(context)方法,而非存进享元本身 - 用
sync.Pool管理享元对象池时,务必在Put前重置内部状态(但更推荐彻底不用可变状态)
用 map + sync.RWMutex 实现线程安全的享元工厂
常见错误是直接用 map[string]*Flyweight 但没加锁,导致并发写 panic:fatal error: concurrent map writes。也不能全用 sync.Mutex,因为读多写少,RWMutex 更合适。
示例工厂结构:
type FlyweightFactory struct {
mu sync.RWMutex
pool map[string]*CharacterFlyweight
}
func (f *FlyweightFactory) Get(char rune) *CharacterFlyweight {
f.mu.RLock()
if fw, ok := f.pool[string(char)]; ok {
f.mu.RUnlock()
return fw
}
f.mu.RUnlock()
f.mu.Lock()
defer f.mu.Unlock()
// 双检锁防止重复初始化
if fw, ok := f.pool[string(char)]; ok {
return fw
}
fw := &CharacterFlyweight{Char: char, Font: "Arial", Size: 12}
if f.pool == nil {
f.pool = make(map[string]*CharacterFlyweight)
}
f.pool[string(char)] = fw
return fw
}
什么时候不该用享元模式?
享元不是银弹。当对象本身很小(比如仅含 1–2 个 int 字段)、生命周期短、或复用率极低时,引入 map 查找、锁开销、额外指针间接访问,反而比直接 new 更慢、更占内存。
判断依据:
- 对象大小 > 64 字节,且字段中存在大量重复值(如字体名、图标 ID、协议类型)
- 运行时存在成千上万个同类对象,但实际唯一变体不超过几十种
- GC 压力明显,pprof 显示大量相同结构体在堆上堆积
- 你已经在用
sync.Pool,但发现对象初始化成本高、且复用逻辑复杂——这时享元工厂更可控
字符串字面量和 rune 可天然当享元键,别转成 []byte
常见误区:为了“统一类型”,把 rune 转成 []byte{byte(r)} 当 map key。这不仅多分配内存,还破坏了 Go 字符串常量池的优化——"a"、"b" 在编译期就驻留,而 []byte 每次都 new。
正确做法:
- 键优先用
string(如string(r))或基础类型(rune、int) - 若需复合键(如字体+大小+粗细),用结构体并确保可比较(字段都是可比较类型),不要用指针或 slice
- 避免用
fmt.Sprintf("%s-%d", font, size)拼接键——分配多、慢;改用预定义结构体或哈希组合










