go中命令模式应以结构体组合和函数值实现,而非接口继承;命令结构体封装接收者引用与参数,执行逻辑由接收者具体方法承担,避免过度抽象和职责污染。

命令模式在 Go 里不是靠接口继承,而是靠函数值和结构体组合
Go 没有抽象类、没有方法重写,硬套 Java 那套 Command 接口 + execute() 方法会写得很别扭。真正轻量又符合 Go 习惯的做法,是把「命令」看作可携带上下文的数据结构 + 一个闭包或方法值。
常见错误是定义一堆空接口或泛型 Command[T],结果调用时还得反复断言、类型检查,反而增加耦合。接收者(Receiver)应该直接暴露方法,发送者(Invoker)只管传参、触发,中间用结构体字段存依赖。
- 接收者定义为普通 struct,比如
type FileSaver struct{ fs *os.File } - 命令封装成 struct,字段包含接收者引用 + 必要参数:
type SaveCommand struct{ receiver *FileSaver; data []byte } - 执行逻辑放在方法里:
func (c *SaveCommand) Execute() error { return c.receiver.Save(c.data) } - 避免把
Execute做成 interface,除非你真需要运行时替换行为(比如测试 mock)
用函数值替代 Command 接口更简洁,但要注意闭包捕获变量的生命周期
如果接收者逻辑简单、无状态,直接用 func() 类型最省事。比如定时任务调度器里塞个命令,time.AfterFunc(delay, cmd) 里的 cmd 就是 func()。
容易踩的坑是闭包意外捕获了循环变量或临时对象。例如在 for 循环里创建多个命令:
立即学习“go语言免费学习笔记(深入)”;
for _, id := range ids {
cmds = append(cmds, func() { process(id) }) // ❌ 全部执行最后一个 id
}
正确写法是显式传参或复制变量:
for _, id := range ids {
id := id // ✅ 创建新绑定
cmds = append(cmds, func() { process(id) })
}
- 函数值适合一次性、无上下文复用的场景;结构体命令适合需撤销、重试、日志追踪的场景
- 函数值无法自带元数据(如超时时间、重试次数),得靠外部 map 或额外参数传
- 如果命令要序列化(比如发到 MQ),函数值没法 encode,必须用结构体 + 显式字段
撤销操作不能靠“反向命令”,而要由接收者自己提供 Undo 方法
命令模式常被误解为每个 Execute() 都得配一个 Undo()。但在 Go 里,更自然的方式是让接收者暴露 UndoSave()、RollbackTx() 这类具体方法,命令结构体只负责调用它。
强行在 SaveCommand 里塞一个 undoFn func() 字段,会导致 undo 逻辑和执行逻辑脱节,出错时难以保证一致性。
- 撤销是否可行,取决于接收者能力,不是命令本身能决定的。比如
HTTPClient发请求后没法“撤销”,只能设计补偿操作 - 如果需要通用 undo 管理,用栈存
*Command指针即可,但Execute()和Undo()都应调用接收者的具体方法 - 注意资源泄漏:Undo 前要确认 Execute 是否真的成功,避免对未初始化的接收者调用
Undo()
命令队列加超时和重试时,别在命令结构体里埋 context 或重试逻辑
把 context.Context 或重试次数塞进命令 struct,看似封装完整,实则污染职责。命令只该描述“做什么”,不该决定“做几次”或“等多久”。这些是调度器(Invoker)的事。
典型错误是这样写:type HTTPCommand struct{ ctx context.Context; url string; maxRetries int } —— 导致同一个命令在不同场景下无法复用,也难以单元测试。
- 命令结构体保持纯净:只含业务参数和接收者引用
- 超时、重试、限流由上层 Invoker 控制,比如用
retry.Do(cmd.Execute, retry.WithMax(3)) - 如果接收者本身需要 ctx(如 http 调用),让它自己从入参取,不要让命令替它保管
- 并发执行多个命令时,每个命令应有独立的
context.WithTimeout,避免一个超时拖垮全部










