go中命令模式核心是func()类型而非接口,推荐用struct{do,undo func()}封装命令,以闭包捕获状态快照,配合[]command管理撤销重做栈,避免硬套java接口继承。

Go 里命令模式不是靠接口继承撑起来的,而是用 func() 和结构体字段组合把“动作”变成值——硬套 Java 那套 Command 接口 + 多个 struct 实现,写出来既啰嗦又难维护。
为什么别定义 Command 接口
常见错误是先写:type Command interface { Execute() },再为每个操作建一个 LightOnCommand、DeleteRowCommand……结果发现:
- 每个 struct 都要重复写
Execute()方法,只是调用 receiver 的不同方法 - 加
Undo()就得改接口、所有实现、测试,一动全动 - 接口掩盖了真正差异——其实是参数和状态不同,不是行为契约不同
更自然的做法是直接用结构体字段存两个闭包:Do func() 和 Undo func()。执行逻辑、撤销逻辑、所需数据全在初始化时捕获,不靠方法重写,也不靠运行时类型判断。
怎么用 struct{Do, Undo func()} 封装命令
命令对象本质是“快照 + 行为”,不是“类模板”。比如编辑器插入文本:
type InsertCommand struct {
content string
pos int
doc *Document // 注意:这里存的是值或只读快照,不是 *Document 指针
oldLen int
}
func NewInsertCommand(doc *Document, content string, pos int) InsertCommand {
// 捕获当前状态,不是引用
return InsertCommand{
content: content,
pos: pos,
doc: doc.Copy(), // 或只存 doc.Content[pos:]
oldLen: len(doc.Content),
}
}
func (c InsertCommand) Do() {
c.doc.Insert(c.content, c.pos)
}
func (c InsertCommand) Undo() {
// 回到 oldLen 长度,不是读 c.doc.Content 当前值
c.doc.Truncate(c.oldLen)
}
关键点:
-
Undo()依赖Do()执行后保存的原始状态(如oldLen),而不是反向计算 - 结构体字段只存必要数据,不存
*sql.DB、*http.Client这类外部依赖 - 如果需要传参或返回 error,就用
func(context.Context) error类型,别塞进接口方法签名
撤销栈必须用 []Command,别碰 container/list
历史管理是线性操作,不是随机访问。用 container/list 看似“标准”,实际带来三重麻烦:
- 每次
Undo()都要list.Back().Value.(Command),类型断言失败就 panic - 遍历调试时得写循环 +
Next()判空,不如fmt.Printf("%v", history)直观 - slice 的
history = history[:len(history)-1]是 O(1) 截断,list.Remove()是指针操作,GC 更吃力
正确姿势:
type CommandManager struct {
history []Command
redoStack []Command
}
func (m *CommandManager) Execute(cmd Command) {
cmd.Do()
m.history = append(m.history, cmd)
m.redoStack = nil // 新操作清空重做栈
}
func (m *CommandManager) Undo() {
if len(m.history) == 0 {
return
}
last := m.history[len(m.history)-1]
last.Undo()
m.history = m.history[:len(m.history)-1]
m.redoStack = append(m.redoStack, last)
}
闭包 vs 结构体:什么时候该用哪种
轻量一次性命令,直接用 func() 类型:
type Command func()
var saveCmd Command = func() {
os.WriteFile("config.json", data, 0644)
}
saveCmd()
但只要涉及撤销、参数复用、测试隔离,就必须升格为结构体:
- 闭包里引用了局部变量?执行时变量可能已变,Undo 就不可靠
- 想对同一批命令统一加日志或超时?结构体字段可统一注入
logger或ctx - 单元测试要 mock 执行逻辑?结构体字段可替换为测试用闭包,
func()类型本身没法打桩
最常被忽略的一点:命令对象生命周期必须短。它不该持有长期资源(如打开的文件句柄、未关闭的事务),而应把清理逻辑显式写进 Undo() 或单独的 Cleanup() 方法。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











