go中命令模式用struct{do,undo func()}而非interface,因编译期强制赋值、闭包捕获执行时快照、避免指针污染与运行时panic;撤销栈存值类型,undo/redo需清空redostack并由外部注入依赖。

Go 里命令模式不是靠接口定义行为,而是靠值捕获 + 闭包封装来保证撤销安全;硬套 Java 风格的 Command 接口+Execute()/Undo() 方法,大概率会在 Undo 时读到被污染的变量、panic 或静默失败。
为什么用 struct{Do, Undo func()} 而不是 interface{Execute(), Undo()}
Go 没有抽象类,也没有方法重写检查机制。一旦你定义了 type Command interface { Execute(); Undo() },编译器完全不拦你漏实现 Undo(),或者在某个具体命令里把 Undo() 写成带参数的 Undo(ctx context.Context) —— 运行时才 panic,堆栈还指向调用处而非命令定义处。
而用结构体字段存两个 func():
- 创建命令时就必须显式赋值,漏写直接编译报错
- 闭包自动捕获执行时的快照(如
oldText := doc.Text),Undo 时用的就是那一刻的值,不是当前值 - 无需类型断言、无指针解引用风险,
history[i].Undo()直接调用,干净利落
撤销栈必须用 []Command,不能用 []*Command 或 container/list
用 []*Command 存指针,看似省内存,实则埋雷:
- 多个命令可能共享同一份业务对象(比如都持有
*Document),Undo 时改的是最新状态,不是命令执行时的状态 - 如果文档被 GC 或重置,指针变 dangling,
Undo()panic -
container/list每次遍历都要Next()判空,调试时打印栈顶命令得写循环,不如history[len(history)-1]一眼可见
正确做法是:命令创建时拷贝关键字段,栈存值类型:
type TextInsertCmd struct {
Do func()
Undo func()
// 不存 *Document,只存 oldLen int, newText string
}
history = append(history, TextInsertCmd{...})
Undo/Redo 必须配合 redoStack 和清空逻辑
用户按 Ctrl+Z 后又输入新内容,旧的 Redo 就该失效——这是常识,但代码里不处理就会出问题:
- 每次调用新命令前,必须
redoStack = redoStack[:0](清空) -
Undo()时:从history弹出最后一个,执行其Undo,再append(redoStack, cmd) -
Redo()时:从redoStack弹出,执行其Do,再append(history, cmd) - 别在
Undo()里做副作用(比如发 HTTP 请求),它只能回滚本地状态
命令里绝不能存 *sql.DB、*http.Client 或全局 logger
这些对象生命周期和命令不一致:
- DB 可能已 Close,命令还在栈里,下次 Undo 一调就 panic
- logger 是全局单例,但命令需要的是“执行那一刻”的上下文(比如 traceID),不是当前 logger 实例
- 真正该存的只有数据:ID、旧值、新值、坐标、文本片段
依赖项应由外部注入,例如:
func NewDeleteCmd(id int64, oldName string) Command {
return Command{
Do: func() { db.Delete(id) }, // db 来自外层闭包,非字段
Undo: func() { db.Insert(id, oldName) },
}
}
最易被忽略的一点:命令对象本身不负责错误处理,也不该返回 error。所有错误必须在 Do 或 Undo 闭包内捕获并记录到日志或状态字段中——因为调用方(如编辑器)无法对每个命令的 error 做差异化处理,统一兜底才是实际做法。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











