享元对象必须完全不可变,所有字段在NewFlyweight中初始化后禁止修改;外在状态须通过参数传入,不可存于结构体;键应使用可比较结构体而非字符串拼接;sync.Pool适用于短期可重置对象,sync.Map适用于长期不可变享元。

享元对象必须不可变,否则并发读写直接崩溃
Go 没有 final 或 immutable 关键字,但享元一旦被多个 goroutine 复用,字段若可变,就会触发数据竞争——不是“可能出错”,而是 go run -race 一跑就报。常见错误是把 sync.Mutex 塞进享元结构体里,试图“保护可变字段”,这等于在共享对象里埋定时炸弹:锁本身不解决状态污染,反而让问题更隐蔽。
- 所有字段必须在
NewFlyweight()中初始化完毕,之后禁止任何修改(包括通过方法、反射或指针赋值) - 若字段含
map或slice,必须用make构造并立即填满;禁止后续append、delete或重新赋值 - 颜色、字体名、图标路径这类值,用
string、int等值类型最安全;避免用*string或unsafe.Pointer引入意外可变性
用 sync.Pool 还是 sync.Map?看对象生命周期
很多人一上来就写 map[string]*Flyweight 加 sync.RWMutex,结果高并发下锁争用严重,GC 反而更忙。选池子的关键不在“听起来高级”,而在对象活多久、变不变:
-
sync.Pool:适合短期、可重置、高频创建销毁的对象,比如每次 HTTP 请求生成的*json.Encoder、渲染用的*bytes.Buffer。它无锁、按 P 局部缓存,但 GC 会清空——所以不能放长期配置类享元 -
sync.Map:适合长期存活、完全不可变的享元,比如全局字体样式FontStyle{Family: "Roboto", Size: 16, Bold: true}。用结构体做 key(FontKey{Family:"Roboto",Size:16,Bold:true}),零分配、可比较、易调试 - 别混用:把需
Reset()的对象丢进sync.Map,下次取出来就是脏数据;把带版本号的配置丢进sync.Pool,GC 清理后就丢失一致性
外在状态绝不能塞进享元结构体,必须走参数传入
最常踩的坑是把本该属于调用上下文的数据硬塞进享元里,比如把 userID、requestID、timestamp 放进 LogFormatter 字段。结果一个实例被 10 个 goroutine 同时改,日志全串了,还查不出原因。
- 享元方法签名必须显式接收外在状态:
func (f *LogFormatter) Format(msg string, ts time.Time, userID string),而不是在结构体里存userID string - 如果方法内部需要组合多个外在参数(如坐标+缩放+透明度),建议封装成轻量
struct传入,而非展开成 5 个参数——但这个 struct 绝不能被享元持有或缓存 - 检查点:删掉所有 setter 方法(
SetX()、SetColor()),如果代码还能编译且逻辑不变,说明你真的分离干净了
键设计用 struct 而非拼接字符串,省内存也防错
用 fmt.Sprintf("%s-%d-%t", f, s, b) 当 map key,每次调用都分配新字符串,GC 频繁;更糟的是漏个分隔符或顺序错,就生成重复享元。Go 允许导出结构体作 key,只要字段可比较(不含 slice/map/func)。
立即学习“go语言免费学习笔记(深入)”;
- 定义
type FontStyleKey struct { Family string; Size int; Bold bool },直接当map[FontStyleKey]*FontStyle的 key - 结构体字段顺序、类型、大小写必须严格一致;
Size int和Size int32是不同 key - 别用指针地址做 key:
&FontStyle{...}地址每次 new 都不同,起不到复用效果
真正难的不是写对那几行 sync.Pool 或 sync.Map,而是每次加字段前,能下意识问一句:“这字段会变吗?会被多少 goroutine 同时读写?它属于这个对象,还是属于这次调用?”——没想清楚就动笔,后面花十倍时间 debug 也未必找得到根因。











