Command 接口应定义为 type Command interface { Execute(ctx context.Context) error; Undo(ctx context.Context) error; Name() string },所有方法必须接收 context.Context 以支持取消和超时,且 Undo 需幂等并检查执行状态。

Command 接口定义要支持泛型错误和上下文取消
Go 没有抽象类,但 Command 本质是行为契约,必须能执行、可撤销、带状态反馈。直接用函数类型太弱(无法撤销),用结构体嵌入接口更可控。关键点是:所有命令必须接收 context.Context,否则队列无法统一响应超时或取消。
常见错误是定义成 type Command func(),结果发现没法加撤销逻辑、也没法透传 ctx。正确做法是:
type Command interface {
Execute(ctx context.Context) error
Undo(ctx context.Context) error
Name() string
}
注意:Execute 和 Undo 都接受 ctx —— 这不是可选的。比如一个写文件命令,若在 Execute 中阻塞于磁盘 I/O,没 ctx 就无法中断。
执行队列需区分串行/并发且内置错误处理策略
“队列”不等于简单切片 + for 循环。真实场景要考虑:命令失败是否继续?是否允许并发执行?是否需要回滚已成功命令?
立即学习“go语言免费学习笔记(深入)”;
- 串行队列用
for遍历,每个Execute后检查err != nil,再决定调用Undo回滚前面的命令 - 并发队列用
sync.WaitGroup+chan error收集结果,但注意:并发下Undo不能盲目并行 —— 可能依赖执行顺序,建议只在全部失败后按逆序串行Undo - 务必设置每条命令的默认超时,例如封装一层
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second),避免单个卡死拖垮整个队列
别漏掉:队列本身应实现 error 返回,比如 Run(ctx context.Context) error,而不是让调用方自己遍历判断。
Undo 回滚必须幂等且能识别“未执行”状态
很多实现把 Undo 写成无条件操作,结果在命令根本没 Execute 成功时也去删文件、改数据库,造成数据丢失。
安全做法是在命令结构体中加状态字段:
type FileWriteCmd struct {
path string
data []byte
executed bool // 初始 false
}
然后 Execute 成功后才置 executed = true,Undo 开头就检查:if !c.executed { return nil }。这比靠外部记录状态更可靠,因为状态和行为绑定在同一实例内。
另外,Undo 自身也必须可重入:比如删除文件前先 os.Stat,不存在就直接返回 nil,不要报错。
实际使用时最容易忽略命令生命周期与资源泄漏
命令对象可能持有文件句柄、DB 连接、HTTP 客户端等资源。如果队列执行完就丢弃命令实例,而 Undo 又没被调用(比如程序 panic 或提前退出),资源就泄漏了。
解决方案有两个方向:
- 强制要求所有命令实现
Close()方法,并在队列Run结束后统一调用(无论成功失败) - 更推荐:命令内部用
defer在Execute函数末尾注册清理,但注意 defer 在 panic 时仍会执行 —— 所以清理逻辑里要加recover()判断是否真需要释放
真正麻烦的是跨 goroutine 的资源管理。比如一个命令启动了后台 goroutine 轮询状态,Undo 必须能通知它退出并 wait 结束,否则 goroutine 泄漏比内存泄漏更难排查。










