命令接口必须定义execute和undo方法,二者缺一不可;undo应是execute的逆操作且仅依赖命令自身字段;命令栈用slice实现更安全高效;命令需持有足够信息还原状态,避免存指针;并发场景下需分离栈操作与资源同步。

命令接口定义必须包含 Execute 和 Undo 方法
Go 没有继承,所以命令模式靠接口驱动。如果只定义 Execute() 而漏掉 Undo(),后续所有撤销逻辑就无法统一调度——不是编译报错,而是运行时才发现某条命令不能撤回。
典型错误是把撤销逻辑塞进 Execute() 里做状态快照,结果导致执行和撤销耦合、内存泄漏(比如缓存了整个对象图)、或并发下状态不一致。
-
type Command interface { Execute() error; Undo() error }是最小可行契约 - 每个具体命令(如
MoveCommand、DeleteCommand)必须同时实现两个方法,且Undo()应是Execute()的逆操作,而非“重试上一步” - 避免在
Undo()中依赖外部可变状态(如全局 map),它应仅基于命令自身字段恢复
命令栈用 slice 实现比用链表更合适
Go 的 slice 在追加、截断、遍历上性能稳定,而手写双向链表不仅增加复杂度,还容易在 Undo() 时因指针误操作导致 panic 或静默错误。
常见陷阱是用 append() 不断扩容命令栈,却不控制最大长度,导致内存持续增长;或者用 stack = stack[:len(stack)-1] 截断后,旧元素未被 GC(尤其含大字段时)。
- 声明为
var history []Command,执行后history = append(history, cmd) - 撤销时用
if len(history) > 0 { last := history[len(history)-1]; last.Undo(); history = history[:len(history)-1] } - 必要时加长度限制:比如
if len(history) >= 100 { history = history[1:] },从头部裁剪而非尾部
撤销函数不能假设命令状态未被外部修改
Go 是值语义 + 指针混用的语言。如果命令结构体里存的是指针(如 *Document),而外部代码在 Execute() 后又改了该文档,那么 Undo() 恢复的可能是已失效的快照。
这不是设计缺陷,而是使用方式问题:命令必须持有足够信息来还原,而不是依赖“当时那个对象还活着”。
- 推荐在
Execute()前深拷贝关键字段(如 ID、旧内容、坐标),存在命令 struct 里;避免存指针 - 若必须存指针(如性能敏感场景),应在
Execute()中立即读取所需值并保存,而不是延迟到Undo()再 dereference - 对 map/slice 类型字段,初始化时用
make()并 copy 数据,不要直接赋值引用
goroutine 安全的命令执行需显式同步
命令模式本身不带并发语义,但实际业务常在 goroutine 中调用 Execute() 或 Undo()。此时命令栈(history)和底层资源(如文件句柄、数据库连接)都可能被多路访问。
最简方案不是加锁整个栈,而是按职责分离:栈操作锁粒度小,资源变更由命令自己保证原子性。
- 命令栈读写用
sync.Mutex或sync.RWMutex,只包住append和slice截断操作 - 每个命令的
Execute()和Undo()应自行处理其依赖资源的并发问题(例如用 channel 控制单次写入,或用sync.Once初始化) - 禁止在命令方法内启动 goroutine 并异步修改共享状态——这会让撤销不可预测
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











