Go中备忘录模式应避免类式设计,采用值语义、显式拷贝和紧贴业务的只读快照结构体,关键在隔离引用、控制深拷贝、防止状态污染。

Go 里没有类,所以别硬套 Memento 的 UML 类图
备忘录模式在 Go 里不是“实现一个 Memento 接口再写三个结构体”,而是用组合 + 不可变数据 + 显式拷贝来达成状态快照目的。Go 的值语义和结构体嵌套天然适合做轻量快照,但容易误以为只要 struct{} 就算备忘录——其实关键在「谁持有原始状态」「谁负责深拷贝」「恢复时是否破坏原有引用」。
常见错误现象:Restore() 后原对象字段没变、或变了不该变的字段;快照里包含 map/slice/*T 导致恢复时改了旧状态;用 json.Marshal/Unmarshal 做快照却忽略私有字段或未导出字段丢失。
- 快照结构体必须只含可导出、可序列化、无指针/引用共享风险的字段
- 不要把
Memento设计成通用泛型容器——它应该紧贴业务对象结构,比如EditorMemento就只存content和cursorPos - 生成快照时,对
map、slice、嵌套结构体做显式浅拷贝(make+copy)或深拷贝(递归或encoding/gob),不能直接赋值
用 struct 字面量 + 值拷贝做最简快照
多数场景下,不需要独立的 Memento 类型,直接用原结构体的字段子集构造一个只读快照结构体更清晰、零分配、无反射开销。
使用场景:编辑器光标位置+内容快照、配置临时回滚、状态机中间态保存。
立即学习“go语言免费学习笔记(深入)”;
type Editor struct {
content string
cursorPos int
undoStack []EditorSnapshot
}
type EditorSnapshot struct {
content string
cursorPos int
}
func (e *Editor) Save() EditorSnapshot {
return EditorSnapshot{
content: e.content, // string 是值类型,自动拷贝
cursorPos: e.cursorPos,
}
}
func (e *Editor) Restore(s EditorSnapshot) {
e.content = s.content
e.cursorPos = s.cursorPos
}
参数差异:Save() 返回值是值类型,调用方拿走的是独立副本;Restore() 接收值类型参数,不关心来源,也不影响旧快照。
性能影响:无内存分配、无反射、无序列化开销;兼容性上,所有 Go 版本都适用。
当需要深拷贝 slice/map 时,避免直接赋值
如果业务对象里有 []byte、map[string]int 这类引用类型字段,直接结构体赋值会让快照和原对象共享底层数据,Restore() 可能意外污染历史状态。
常见错误现象:Save() 后修改原对象的 tags 切片,再 Restore() 发现快照里的 tags 也变了。
- 对
slice:用make+copy,例如tags: append([]string(nil), orig.tags...) - 对
map:遍历复制键值,m := make(map[string]int); for k, v := range orig.m { m[k] = v } - 对嵌套结构体字段:确保其内部也不含引用类型,否则需递归处理
- 不要依赖
gob或json做快照——它们会忽略私有字段、无法处理func或unsafe.Pointer,且有运行时开销
UndoStack 管理要控制容量,别让快照无限增长
快照本身是值类型,但大量保存仍会吃内存,尤其含大 []byte 或长字符串时。Go 没有析构函数,不会自动清理旧快照。
使用场景:频繁操作需撤销(如文本编辑、表单多步填写)、资源受限环境(CLI 工具、嵌入设备)。
- 在
Save()前检查undoStack长度,超限时用undoStack = undoStack[1:]弹出最老快照 - 避免用
append(undoStack, s)无节制追加;考虑用环形缓冲区([N]EditorSnapshot+ 索引)替代切片 - 如果快照含大字段,可只存差异(diff)而非全量,但会增加
Restore()复杂度——多数情况全量更稳妥
容易被忽略的地方:快照结构体字段名和类型必须和原始对象严格一致,否则字段对不上;恢复时若字段是接口类型(如 io.Reader),快照里存不了,得换设计。










