memento 应为不可变值类型,只存关键只读字段并深拷贝,避免循环引用和并发污染;originator.save() 返回值类型更安全。

为什么不用 json.Marshal 直接存结构体快照
因为编辑器文档常含循环引用(比如节点互相持有父/子指针)、未导出字段、或带 sync.Mutex 等不可序列化字段,json.Marshal 会直接 panic 或静默丢数据。备忘录模式的核心是“可还原的轻量状态”,不是完整对象拷贝。
- 优先只提取关键业务字段:如
content、cursorPos、selection,而非整个*Document - 用
struct{}匿名结构体临时封装快照,避免污染主结构体定义 - 如果必须深拷贝,改用
gob(支持私有字段)或第三方库如copier,但注意gob不跨语言、不兼容 Go 版本升级
如何让 Memento 类型不破坏编辑器的并发安全
备忘录本身应是不可变值对象 —— 一旦创建就不能被修改,否则多个撤销操作可能相互污染。Golang 没有原生 immutable 支持,得靠约定+结构设计来守住这条线。
- 把快照字段全设为只读:用首字母小写的字段名(如
content),不提供 setter 方法 - 在构造函数里做一次深拷贝(比如用
strings.Clone处理string,用append([]T(nil), src...)复制切片),防止外部修改原始数据影响已存快照 - 不要在
Memento里存指针、map、chan或任何可能被外部修改的引用类型
Originator.Save() 该返回指针还是值类型
返回值类型(Memento)更安全。返回指针容易让人误以为可以复用或修改,也增加 GC 压力;而值类型天然不可变,且小结构体(
- 典型
Memento定义应类似:type Memento struct { content string cursorPos int timestamp int64 } - 如果快照数据较大(比如含几百 KB 的富文本 diff),才考虑返回
*Memento,但必须加注释强调“只读” - 别为了“看起来像经典设计模式”而强行返回指针 —— Go 的惯用法是值语义优先
撤销栈用 []*Memento 还是 []Memento
用 []Memento。只要 Memento 是纯值类型且构造时已做必要拷贝,切片存值完全没问题,还省去 nil 检查和指针解引用开销。
立即学习“go语言免费学习笔记(深入)”;
- 常见错误:往栈里 append 同一个
Memento变量的地址,导致所有快照指向同一块内存 - 正确做法:每次
Save()都生成新值,然后undoStack = append(undoStack, doc.Save()) - 限制栈大小时,用
undoStack = undoStack[max(0, len(undoStack)-maxSize):]截断,别用make+copy增加复杂度










