如何使用Golang实现Memento模式_Golang备忘录模式与数据恢复机制

11次阅读

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

如何使用Golang实现Memento模式_Golang备忘录模式与数据恢复机制

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() 执行栈顶闭包,再 popRedo() 可额外维护一个 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 到业务结构体

状态恢复的边界必须清晰:快照只负责提供原始数据,校验和重建必须发生在领域对象内部。

text=ZqhQzanResources