Golang备忘录模式实现状态回滚

Go中备忘录模式的核心难点是值语义与指针语义混淆导致浅拷贝引发状态共享;推荐用json.Marshal/Unmarshal实现安全快照,或手动深拷贝关键字段,禁用unsafe.Pointer回滚。

备忘录模式在 Go 中的核心难点是值语义与指针语义的混淆

Go 没有内置的深拷贝机制,直接用结构体赋值(backup := current)只做浅拷贝。如果结构体含 []bytemapslice 或指向堆内存的指针,回滚时会意外共享状态,导致“看似回滚了,实际改了备份”。必须显式隔离可变数据。

json.Marshal/Unmarshal 实现安全快照(适合中小对象)

这是最稳妥的默认方案:序列化天然切断引用关系,且对嵌套结构、切片、map 透明支持。缺点是性能开销和不支持未导出字段或函数类型。

  • 确保所有字段都是导出的(首字母大写)
  • 避免在结构体中嵌入 sync.Mutex 等不可序列化字段
  • 若需保留时间戳等元信息,可在备忘录结构中额外加 Timestamp time.Time 字段
type Editor struct {
    Content string
    Cursor  int
}

type Memento struct {
    Content string
    Cursor  int
    Timestamp time.Time
}

func (e *Editor) Save() *Memento {
    return &Memento{
        Content:   e.Content,
        Cursor:    e.Cursor,
        Timestamp: time.Now(),
    }
}

func (e *Editor) Restore(m *Memento) {
    e.Content = m.Content
    e.Cursor = m.Cursor
}

手动深拷贝 + copy() 处理切片(高频修改场景)

当结构体含大容量 []byte 或频繁回滚时,json 序列化太重。此时应为关键字段提供显式复制逻辑:

  • []byte:用 copy(dst, src)append([]byte(nil), src...)
  • map[string]int:新建 map 并遍历赋值
  • 对嵌套结构体:递归调用其 Clone() 方法(需自行实现)
type Document struct {
    Lines []string
    Meta  map[string]string
}

func (d *Document) Save() *Document {
    linesCopy := make([]string, len(d.Lines))
    copy(linesCopy, d.Lines)

    metaCopy := make(map[string]string)
    for k, v := range d.Meta {
        metaCopy[k] = v
    }

    return &Document{
        Lines: linesCopy,
        Meta:  metaCopy,
    }
}

unsafe.Pointer 回滚原始内存?别这么做

有人试图用 unsafe 记录结构体地址+大小再 memcpy,这在 Go 中极其危险:GC 可能移动对象,且 unsafe 绕过编译器检查,一旦结构体字段增减或对齐变化,回滚就会静默破坏内存。Go 的内存模型不保证结构体布局跨版本稳定,生产环境必须杜绝此类操作。

真正需要极致性能的场景(如游戏帧状态),应改用预分配缓冲池 + 显式状态转移函数,而不是依赖底层内存快照。