go中备忘录模式需手动实现值拷贝与只读快照,避免指针共享和不可序列化字段;推荐用可导出字段struct+构造函数/恢复方法,配合sync.pool优化高频创建。

备忘录模式在 Go 里没有“标准实现”,因为 Go 没有类和访问修饰符
Go 语言里没法靠 private 字段天然隔离备忘录对原对象内部状态的访问——这是最常被忽略的前提。所谓“备忘录”在 Go 中本质是手动控制的一组只读快照数据,靠约定而非语法保障封装性。
典型错误是直接把指针塞进备忘录结构体,导致恢复时改的是同一块内存;或者用 json.Marshal 做深拷贝却漏掉 unexported 字段(首字母小写字段),恢复后状态丢失。
- 必须用值拷贝或显式深拷贝,不能传
*T - 如果结构体含
sync.Mutex、chan、func等不可序列化字段,得在备忘录中排除或替换成标记值 - 推荐用
struct{}匿名字段 + 可导出字段组合,避免意外暴露内部细节
用 struct{} + 只读字段定义备忘录类型最稳妥
别用 map 或 interface{} 存状态,类型越明确,后期维护越不容易出错。备忘录本身不参与业务逻辑,只是个“时间点切片”,所以字段全用可导出名,但只提供构造函数和恢复方法,不开放字段直写。
示例:
立即学习“go语言免费学习笔记(深入)”;
type EditorMemento struct {
Content string
CursorPos int
Timestamp int64
}
func (e *Editor) Save() *EditorMemento {
return &EditorMemento{
Content: e.content, // 注意:这里必须是值拷贝
CursorPos: e.cursorPos,
Timestamp: time.Now().Unix(),
}
}
func (e *Editor) Restore(m *EditorMemento) {
e.content = m.Content // 直接赋值,不是引用
e.cursorPos = m.CursorPos
}
-
Save()返回指针没问题,但内部字段必须是值拷贝,不能是&e.content - 如果
content是[]byte或大 struct,考虑用copy()或clone工具库避免浅拷贝陷阱 - 别在
Restore()里做校验(比如检查Timestamp),那是调用方的责任;备忘录只管存取
用 sync.Pool 管理高频创建的备忘录能省 GC 压力
如果编辑器每秒保存几十次,每次都 new 一个 *EditorMemento,GC 会明显抖动。这时候 sync.Pool 就不是优化项,而是必要项。
注意:Pool 中的对象可能被任意回收,所以不能依赖其内容残留;每次 Get 后必须显式重置字段。
- 在
Save()前先pool.Get(),再清空字段并赋新值 - 不要把带指针的字段(如
*bytes.Buffer)直接塞进 Pool,容易引发悬垂引用 - Pool 的 New 函数应返回已初始化的零值对象,而不是 nil 指针
恢复失败往往是因为状态不一致,而不是代码写错了
常见现象是调用 Restore() 后界面没变、光标跳错、或 panic 报 nil pointer dereference。这时候大概率不是备忘录逻辑有 bug,而是调用时机不对:比如在异步 goroutine 里恢复了主线程对象,或恢复前对象已被 free(如 struct 被重新赋值)。
- 确保
Restore()和Save()操作的是同一个实例,不是副本 - 如果对象含
context.Context或io.Closer,备忘录不该存它们——这些是运行期资源,不是状态 - 调试时打印
fmt.Printf("%p", e)对比保存/恢复时的地址,能快速定位是否操作了不同实例
真正难的不是怎么存,是怎么界定“哪些算状态、哪些算上下文”。这个边界一旦划错,后面所有拷贝和恢复都会无声失效。










