Go中实现命令模式支持撤销的关键是定义含Execute()和Undo()方法的Command接口,各具体命令结构体封装必要状态并显式实现双向逻辑,调用器用切片维护历史栈实现单步撤销。

在 Go 中实现命令模式并支持撤销操作,关键在于将每个可执行动作封装为独立的 Command 对象,并维护一个命令历史栈(通常用切片实现),同时要求每个命令自身具备 Execute() 和 Undo() 方法。
定义统一命令接口
Go 没有传统意义上的抽象类,但可通过接口定义契约:
type Command interface {
Execute()
Undo()
}
所有具体命令(如“添加用户”、“删除文件”)都必须实现这两个方法。注意:Undo 的逻辑不是自动反向执行,而是由开发者显式编写——比如 DeleteFileCommand 的 Execute() 删除文件,Undo() 就需恢复备份或重新创建该文件。
实现具体命令并保存必要状态
每个命令结构体需内嵌执行所需的数据,确保 Undo() 有足够上下文回退。例如:
立即学习“go语言免费学习笔记(深入)”;
type AddUserCommand struct {
userStore *[]string
userName string
}
func (c *AddUserCommand) Execute() {
*c.userStore = append(*c.userStore, c.userName)
}
func (c *AddUserCommand) Undo() {
users := *c.userStore
if len(users) > 0 && users[len(users)-1] == c.userName {
*c.userStore = users[:len(users)-1]
}
}
- 避免在
Execute()中修改外部状态后丢失还原依据(如不记录被删文件名就无法恢复) - 若操作涉及外部系统(如数据库),
Undo()应是幂等且尽量轻量的补偿动作 - 命令对象应是不可变的(或至少其字段在 Execute 后不再变更),防止多线程下状态错乱
构建支持撤销的命令调用器
用切片模拟栈,按顺序记录已执行命令,提供 Execute()、Undo() 和可选的 Redo():
type CommandInvoker struct {
history []Command
undone []Command // 可选:用于 redo
}
func (i *CommandInvoker) Execute(cmd Command) {
cmd.Execute()
i.history = append(i.history, cmd)
i.undone = nil // 执行新命令后清空 redo 栈
}
func (i *CommandInvoker) Undo() {
if len(i.history) == 0 {
return
}
last := i.history[len(i.history)-1]
last.Undo()
i.history = i.history[:len(i.history)-1]
i.undone = append(i.undone, last)
}
- 每次
Undo()只撤回最近一次Execute(),符合常规交互直觉 - 若需多级撤销,直接循环调用
Undo()即可,无需额外参数 - 历史栈本身不持有命令执行结果,只负责调度;状态管理完全交给命令和接收者(receiver)
注意事项与常见陷阱
命令模式在 Go 中落地时容易忽略几点:
- 不要让命令依赖全局变量或单例——这会让测试困难、Undo 不可靠
- 避免在
Undo()中抛出 panic;建议返回 error 或静默失败(如文件已存在则跳过恢复) - 若命令耗时长(如网络请求),考虑加超时或上下文控制,但
Undo()也应同样支持 cancel - 撤销功能会增加内存占用,长期运行服务中可限制
history最大长度(如只保留最近 100 条)
基本上就这些。核心是把“做什么”和“谁来做”解耦,再让每个“做什么”自己知道怎么撤回——Go 的接口 + 结构体组合,足够干净地表达这个意图。










