命令模式在Go中核心是封装可撤销的执行单元,应使用结构体承载状态和上下文,明确依赖与错误处理,避免硬套接口,按需实现Undo而非强制统一。

命令模式的核心不是接口,而是可撤销的执行单元
Go 语言没有传统面向对象的抽象类或虚函数,所以硬套 UML 类图里的 Command 接口 + execute()/undo() 方法容易跑偏。真正关键的是:把「一个操作」封装成能延迟调用、能携带上下文、能统一管理生命周期的值。Go 里最自然的载体就是函数类型和结构体组合。
用 struct 封装命令比纯函数更实用
纯函数(如 func())无法自带状态,而真实命令往往需要参数、依赖、甚至回滚所需的数据快照。用结构体承载命令逻辑,既清晰又可控。
-
Execute()和Undo()方法必须接收明确的上下文(比如*App或DB),不能隐式依赖全局变量 - 命令实例应在创建时捕获必要参数(如 ID、原始值),避免执行时再去查——否则 undo 可能失败
- 如果命令涉及 IO 或可能失败,
Execute()应返回error;Undo()同理,且不应 panic
type DeleteUserCommand struct {
UserID int
Name string // 执行前缓存,用于 undo 恢复
DB *sql.DB
}
func (c *DeleteUserCommand) Execute() error {
_, err := c.DB.Exec("DELETE FROM users WHERE id = ?", c.UserID)
return err
}
func (c *DeleteUserCommand) Undo() error {
_, err := c.DB.Exec("INSERT INTO users(id, name) VALUES (?, ?)", c.UserID, c.Name)
return err
}
命令队列要区分「执行」和「撤销」的顺序逻辑
典型误区是把命令堆进 slice 然后逆序调 Undo() —— 这只适用于线性、无分支的操作流。实际中更常见的是:用户执行 A → B → C,然后撤销 C,再执行 D,此时历史不该丢弃 A/B,但也不能让 D 的 undo 插在 C 前面。
- 推荐用栈(
[]Command)只记录已执行且**未被覆盖**的命令 - 每次新命令执行前,先清空栈顶所有已被「跳过」的 undo 项(即实现类似 Photoshop 的“历史画笔”行为)
- 撤销操作本质是 pop +
Undo(),而非遍历整个历史
不要给每个命令都加 undo,优先保证 execute 的幂等与可测
很多业务场景(如发通知、写日志、调第三方 API)根本不可逆。强行设计 Undo() 会引入额外状态管理成本,还可能掩盖真正的问题。
立即学习“go语言免费学习笔记(深入)”;
- 对无法 undo 的命令,
Undo()可返回errors.New("not supported"),调用方需处理该错误 - 更务实的做法是:用事件溯源(event sourcing)替代命令模式——把所有变更记为不可变事件,重放即恢复状态
- 测试时重点验证
Execute()是否按预期修改了状态,而不是纠结 undo 是否“完美”
命令模式在 Go 里不是炫技工具,它是当你需要精确控制操作生命周期、支持重做/撤销、或解耦触发与执行时机时的务实选择。别先定义接口,先想清楚:这个操作要传什么?在哪执行?失败了怎么收场?undo 是刚需,还是自我感动?










