Go中备忘录模式需手动实现快照,核心是值拷贝或显式深拷贝避免指针污染,用小写字段+构造函数模拟Memento封装,历史管理推荐带游标的slice而非链表,并注意不可快照类型需剥离。

备忘录模式在 Go 中没有语言级支持,需手动实现状态快照
Go 语言本身不提供 clone、deep copy 或内置的不可变类型机制,所以「保存历史状态」这件事必须由开发者显式控制。核心难点不在结构设计,而在「什么该存、怎么存、何时存」——比如直接保存指针会导致后续修改污染历史,而盲目深拷贝又可能引发性能或循环引用问题。
典型错误是这样写:
type Editor struct {
content string
history []*Editor // 错误:存的是当前实例地址,不是快照
}
func (e *Editor) Save() {
e.history = append(e.history, e) // 所有历史项最终都指向同一块内存
}
正确做法是让 Save() 返回一个独立的、只读的快照结构(Memento),且内部字段应为值类型或显式拷贝后的引用类型。
用结构体 + 构造函数模拟 Memento,避免暴露内部字段
Go 没有私有字段语法,但可通过首字母小写 + 不导出字段 + 只提供构造函数和只读访问方法来模拟封装。关键不是“防黑客”,而是防止调用方误改快照数据。
立即学习“go语言免费学习笔记(深入)”;
-
Memento结构体所有字段必须小写,不导出 - 仅提供
NewMemento()创建,不提供 setter 方法 - 若需恢复,由原对象(
Originator)负责从Memento中提取字段赋值,而非让Memento提供可变接口
示例:
type Editor struct {
content string
}
type memento struct {
content string
}
func (e Editor) Save() memento {
return &memento{content: e.content} // 值拷贝,安全
}
func (e Editor) Restore(m memento) {
if m != nil {
e.content = m.content // 恢复动作由 Originator 主导
}
}
历史栈管理建议用 slice 而非自定义链表
多数场景下,回滚只需「上一步」「回到某步」,不需要随机插入或频繁中间删除。用 []*memento 配合 index 游标即可满足,比手写双向链表更轻量、更符合 Go 的惯用法。
常见陷阱:
- 未限制历史长度,导致内存持续增长 → 应设置最大容量,满时
append前copy覆盖旧项 - 执行
Undo()后继续编辑,再Redo()失效 → 需清空游标之后的历史(即「分支截断」) - 用
len(history) - 1当前索引,但未处理空切片 panic → 每次操作前检查len(history) == 0
简化版历史管理:
type HistoryManager struct {
states []*memento
index int // 当前生效状态在 states 中的索引
}
func (h HistoryManager) Push(m memento) {
h.states = append(h.states[:h.index+1], m)
h.index++
}
func (h HistoryManager) Undo() memento {
if h.index <= 0 {
return nil
}
h.index--
return h.states[h.index]
}
func (h HistoryManager) Redo() memento {
if h.index >= len(h.states)-1 {
return nil
}
h.index++
return h.states[h.index]
}
复杂状态需谨慎处理指针/切片/Map 字段
如果 Editor 内部含 []byte、map[string]int 或嵌套结构体指针,直接赋值仍会共享底层数组或哈希表。此时必须显式深拷贝:
-
[]byte:用append([]byte(nil), src...)或copy(dst, src) -
map:遍历 key-value 重建新 map - 含指针的结构体:逐字段判断是否需拷贝,或使用
github.com/jinzhu/copier等库(注意其对循环引用的支持有限)
例如:
type Editor struct {
content []byte
tags map[string]bool
}
func (e Editor) Save() memento {
contentCopy := append([]byte(nil), e.content...)
tagsCopy := make(map[string]bool)
for k, v := range e.tags {
tagsCopy[k] = v
}
return &memento{
content: contentCopy,
tags: tagsCopy,
}
}
真正麻烦的从来不是模式本身,而是业务状态的「可快照性」——如果一个结构体里混着 sync.Mutex、net.Conn 或 os.File,它本质上就不该被放进备忘录。这类字段必须在设计阶段就剥离到外部上下文,或标记为「不可回滚」。










