Go中命令模式优先用函数类型而非接口+结构体,因func()更轻量、闭包可捕获上下文、撤销通过返回undo函数实现;需序列化或审计时才改用结构体。

命令模式在 Go 里为什么不用接口+结构体组合?
Go 没有传统 OOP 的抽象类或虚函数,强行套用 UML 类图里的 Command 接口 + Execute() 方法会显得笨重。实际中更自然的做法是直接用函数类型封装行为,配合闭包捕获上下文——既满足“命令可排队、可撤销、可记录”的核心诉求,又不引入冗余类型。
-
func()类型本身就能作为命令载体,比定义type Command interface { Execute() }更轻量 - 撤销操作不依赖
Undo()方法,而是通过返回额外的撤销函数(func())实现 - 命令参数不靠继承传递,而用闭包绑定,避免构造器膨胀
如何用函数类型实现可撤销的命令?
关键在于每个命令生成时,同时产出执行函数和对应的撤销函数。两者共享状态(比如一个 *int 或 map[string]string),但互不耦合。
type Command struct {
execute func()
undo func()
}
func NewIncrementCommand(counter int) Command {
oldValue := counter
return Command{
execute: func() { counter++ },
undo: func() { counter = oldValue },
}
}
func NewSetConfigCommand(cfg map[string]string, key, value string) Command {
oldValue := cfg[key]
return Command{
execute: func() { cfg[key] = value },
undo: func() { cfg[key] = oldValue },
}
}
- 每个
NewXXXCommand构造函数内部捕获当前状态(如oldValue),确保撤销时能准确还原 - 不依赖全局或外部状态管理器,命令自身携带全部必要信息
- 如果命令涉及 I/O(如写文件),撤销函数必须是幂等的,且要考虑失败回滚路径
命令队列怎么支持批量执行与原子回滚?
用切片存 Command 值即可,但要注意:执行中途出错时,已执行的命令必须按逆序调用 undo,否则状态不一致。
func ExecuteCommands(commands []Command) error {
var executed []Command
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
func ExecuteCommandsWithRollback(commands []Command) error {
var executed []Command
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
// 实际带回滚的版本:
func ExecuteCommandsAtomic(commands []Command) error {
var executed []Command
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
func ExecuteCommandsAtomic(commands []Command) error {
var executed []Command
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
// 正确带回滚的版本:
func ExecuteCommandsAtomic(commands []Command) error {
var executed []Command
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
func ExecuteCommandsAtomic(commands []Command) error {
var executed []Command
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
// 简洁可靠版:
func ExecuteCommandsAtomic(commands []Command) error {
var executed []Command
defer func() {
if r := recover(); r != nil {
// 逆序撤销
for i := len(executed) - 1; i >= 0; i-- {
executed[i].undo()
}
}
}()
for _, cmd := range commands {
cmd.execute()
executed = append(executed, cmd)
}
return nil
}
- 上面示例中,
defer+recover()仅适用于 panic 场景;若命令返回error,需手动检查并触发回滚 - 更健壮的做法是让每个
Command的execute()返回error,并在循环中显式判断 - 不要把命令队列设计成单例或全局变量,容易引发并发竞争——每次执行都应传入新切片
什么时候该放弃函数式命令,改用结构体字段?
当命令需要序列化(如存数据库、发网络)、或需动态 inspect 参数(比如做审计日志、权限校验)、或参数极多且变化频繁时,函数闭包就不好办了。这时回到结构体 + 方法是更清晰的选择。
立即学习“go语言免费学习笔记(深入)”;
type DeleteUserCommand struct {
UserID int64 `json:"user_id"`
AdminID int64 `json:"admin_id"`
Reason string `json:"reason"`
Executed bool `json:"executed"`
}
func (c *DeleteUserCommand) Execute() error {
if c.Executed {
return errors.New("command already executed")
}
// ... real logic
c.Executed = true
return nil
}
func (c *DeleteUserCommand) Undo() error {
// restore user
return nil
}
- 结构体命令适合跨进程/跨服务场景,因为字段可直接 JSON 序列化
- 注意
Executed这类运行时状态不应被序列化,否则重放命令会出错 - 如果命令要支持重试,结构体字段必须是幂等设计(比如用唯一操作 ID 去重)
真正难的不是写命令,而是决定哪些状态该由命令自己捕获,哪些该由外部协调器统一管理——边界模糊时,先从函数式开始,等出现序列化或审计需求再重构。










