go中memento模式应避免暴露字段,改用闭包捕获不可变状态并由对象自身恢复,确保校验逻辑生效;多快照用slice管理undo/redo,序列化需专用结构体与校验重建。

Go 语言没有类和继承,也不支持访问控制符(如 private),所以直接照搬 GoF 的 Memento 模式会水土不服。关键不是“怎么写一个 Memento 结构体”,而是“如何在 Go 中安全、可控地保存和恢复对象状态”。
为什么不能直接用结构体字段暴露做 Memento
典型错误是把目标对象的字段全导出,再让 Memento 直接存一份副本:
type Editor struct {
Content string // 导出字段,外部可读可写
Cursor int
}
type Memento struct {
Content string
Cursor int
}
这会导致两个问题:
-
Editor状态不再受控 —— 外部可随意修改Memento.Content后再塞回去 - 违反封装意图 ——
Memento本应是“只读快照”,但 Go 里没有语言级只读保障 - 一旦
Editor增加校验逻辑(比如Cursor不能越界),Memento恢复时就绕过了校验
用闭包 + 不可变数据构造真正受控的 Memento
核心思路:把状态快照封装进闭包,只暴露恢复方法,不暴露原始字段。
立即学习“go语言免费学习笔记(深入)”;
- 用
func() *Editor代替Memento结构体,避免字段泄漏 - 快照捕获的是当时的状态值,不是引用,天然不可变
- 恢复动作由
Editor自己完成,确保校验逻辑生效
示例:
type Editor struct {
content string
cursor int
}
func (e *Editor) Save() func() {
content, cursor := e.content, e.cursor // 捕获当前值
return func() {
e.content = content // 恢复由 Editor 内部控制
e.cursor = cursor
}
}
func (e *Editor) SetContent(s string) {
e.content = s
if e.cursor > len(s) {
e.cursor = len(s)
}
}
调用方只能执行 restore(),无法窥探或篡改中间状态。
用 slice 管理多个快照实现 Undo/Redo 链
真实场景需要历史栈,不是单个快照。注意别用指针存 *Editor —— 那只是存了引用,后续修改会影响快照。
- 每次
Save()返回一个闭包,追加到history []func() -
Undo()执行栈顶闭包,再pop;Redo()可额外维护一个redoStack - 避免无限增长:限制
history长度,或定期清理(比如只保留最近 50 步)
片段示意:
type Editor struct {
content string
cursor int
history []func()
redoStack []func()
}
func (e *Editor) Undo() {
if len(e.history) == 0 {
return
}
restore := e.history[len(e.history)-1]
e.history = e.history[:len(e.history)-1]
restore()
e.redoStack = append(e.redoStack, restore)
}
JSON 序列化不是备忘录,除非你控制反序列化逻辑
有人用 json.Marshal 存状态,再 json.Unmarshal 恢复 —— 这看似简单,但隐患明显:
- 字段名变更或类型调整后,旧快照可能静默失败或错位赋值
- 反序列化绕过所有构造逻辑和字段校验(比如
cursor被设为负数) - 敏感字段(如 token、临时缓存)可能被意外序列化出去
如果必须持久化快照(比如存文件或 DB),建议:
- 定义专用的、稳定的序列化结构体(如
EditorSnapshotV1),与运行时结构分离 - 反序列化后走完整构造函数或
FromSnapshot()方法,重新校验 - 绝不直接
json.Unmarshal到业务结构体
状态恢复的边界必须清晰:快照只负责提供原始数据,校验和重建必须发生在领域对象内部。











