go中备忘录模式核心是值拷贝+包级封装+显式字段控制,用小写字段的结构体实现可控快照;禁止导出字段、避免指针共享、过滤非状态字段,并通过reflect.deepequal验证隔离性。

Go 里实现备忘录模式,核心不是“照搬 GoF 类图”,而是用值拷贝 + 包级封装 + 显式字段控制,把状态快照变成可控、可测、不泄漏的普通结构体。其他所有设计(比如接口抽象、manager 管理栈)都是围绕它服务的。
备忘录结构体必须用小写字段且不导出
Go 没有 private 关键字,靠首字母小写约定实现封装边界。如果 Memento 字段导出(如 Content string),外部包就能直接读写,彻底破坏备忘录语义。
- 正确做法:定义为
type editorMemento struct { content string; cursor int }(小写字段),只在同包内使用 - 别用空接口或
map[string]interface{}存状态——类型丢失后,恢复逻辑没法做字段校验,后期改字段名或类型极易静默失败 - 不要为了“通用”而抽象出
Memento interface{},除非你真需要多态;多数场景下,*editorMemento就是唯一合法类型
Save() 必须做值拷贝,不能传指针或共享底层数据
常见错误是 content: &e.content 或 lines: e.lines(e.lines 是 []string)。这会导致快照和原对象共用同一片内存,Restore 看似成功,实际只是改了当前状态。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 字符串、基本类型直接赋值即可:
content: e.content - 切片需显式 copy:
lines: append([]string(nil), e.lines...)或copy(dst, e.lines) - map 要新建并遍历复制:
props: maps.Clone(e.props)(Go 1.21+)或手写 loop - 含
*bytes.Buffer、sync.Mutex、chan的字段,压根不该进备忘录——它们不是“可撤销状态”,而是运行时资源
Restore() 后记得清空无用引用,帮 GC 减负
用 []*editorMemento 做撤销栈很自然,但没人提醒你:只要 slice 还持有某个 *editorMemento,它引用的所有内容(比如一个 10MB 的 content 字符串)就一直无法被回收。
- 设上限防爆内存:
if len(h.mementos) > 200 { h.mementos = h.mementos[1:] } - Pop 后主动置空:
m := h.mementos[len(h.mementos)-1]; h.mementos = h.mementos[:len(h.mementos)-1]; h.mementos = append(h.mementos, nil)(最后一步可选,但推荐) - 高频保存场景(如编辑器每秒 Save 多次),用
sync.Pool复用*editorMemento,但每次Get()后必须重置字段,不能依赖旧值残留
测试时用 reflect.DeepEqual 验证隔离性,别信表面“恢复成功”
很多测试只检查 e.GetText() == m.Content,但没验证是否真的隔离。一旦字段含指针或 map,reflect.DeepEqual 会暴露问题——它比 == 更严格,能发现底层数据是否被共享。
- 写测试时加一句:
assert.True(t, reflect.DeepEqual(&before, &after))(before/after 是两次 Save 得到的快照) - 故意在 Restore 后修改原对象的某字段(如追加字符串),再检查老快照是否仍不变——这是最直接的指针污染验证
- 避免用 JSON 序列化做深拷贝来测试:它会跳过小写字段,且无法处理
func、chan,误导性极强
真正难的不是写 Save/Restore,而是判断哪些字段该进备忘录、哪些该过滤。业务字段(文本、光标、选区)要存;时间戳、ID、缓存句柄、日志器这些,存了反而让恢复行为变得不可预测。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










