命令模式是一种行为设计模式,它将请求封装为对象,从而使你可以用不同的请求、队列或日志来参数化其他对象,并支持可撤销的操作。在Golang中,由于没有类的继承体系,我们更依赖接口与结构体组合来实现这一模式。核心思路是定义一个统一的命令接口,包含执行与撤销两个方法,然后由具体的命令结构体保存执行所需的接收者和上下文数据。

一、命令模式的核心接口设计
在Golang中实现命令与撤销操作,第一步是抽象出命令接口。该接口通常只需要两个方法:Execute用于触发操作,Undo用于回滚操作。这种抽象让调用者不需要知道具体业务逻辑,只管调用接口即可。接收者(Receiver)则是真正执行业务逻辑的对象,例如一个文档编辑器或者一个任务管理器。
下面的代码展示了最基础的接口与接收者定义。我们注意到,具体命令结构体持有接收者指针以及执行时需要的参数,这样在撤销时才能还原现场。使用接口而非具体类型,也方便后续扩展新的命令而无需修改调用者代码。
package main
import "fmt"
// Command 定义命令接口,包含执行与撤销
type Command interface {
Execute() error
Undo() error
}
// Document 作为接收者,模拟一个文本文档
type Document struct {
content string
}
func (d *Document) Insert(text string) {
d.content += text
}
func (d *Document) Delete(lastLen int) {
if len(d.content) >= lastLen {
d.content = d.content[:len(d.content)-lastLen]
}
}
func (d *Document) Show() {
fmt.Println("当前内容:", d.content)
}
二、具体命令的实现与上下文保存
具体命令需要把“做什么”和“怎么撤销”绑定在一起。以向文档插入文本为例,InsertCommand在Execute时调用接收者的Insert方法,同时记录插入的长度;在Undo时调用接收者的Delete方法并传入该长度,从而精确删除刚加入的内容。这种保存上下文的方式是命令模式支持回滚的关键。
如果操作涉及多个字段变更,结构体里就应保存所有旧值。例如修改配置时,要把原配置备份下来。下面的InsertCommand演示了最小可用的撤销逻辑,实际项目中你可能在Undo里加入错误判断,确保只有成功执行过的命令才能撤销。
// InsertCommand 具体插入命令
type InsertCommand struct {
doc *Document
text string
}
func (c *InsertCommand) Execute() error {
c.doc.Insert(c.text)
return nil
}
func (c *InsertCommand) Undo() error {
c.doc.Delete(len(c.text))
return nil
}
// 使用示例
func main() {
doc := &Document{}
cmd := &InsertCommand{doc: doc, text: "Hello"}
cmd.Execute()
doc.Show()
cmd.Undo()
doc.Show()
}
三、调用者与历史栈管理
为了让用户连续撤销多步操作,我们需要一个调用者(Invoker)来维护已执行命令的历史切片。每次执行命令后,将其追加到历史中;撤销时从切片尾部取出并调用Undo,同时弹出该记录。这样便实现了后进先出的回滚栈。Golang的slice操作非常契合这种需求。
以下代码实现了一个简单的Invoker,它提供Do和Rollback方法。Do负责执行并压栈,Rollback负责弹栈并撤销。注意我们在Rollback里做了长度判断,避免空栈 panic。该结构可轻松扩展为重做(Redo)功能,只需再维护一个重做栈即可。
type Invoker struct {
history []Command
}
func (i *Invoker) Do(c Command) error {
if err := c.Execute(); err != nil {
return err
}
i.history = append(i.history, c)
return nil
}
func (i *Invoker) Rollback() error {
if len(i.history) == 0 {
return fmt.Errorf("无命令可撤销")
}
last := i.history[len(i.history)-1]
i.history = i.history[:len(i.history)-1]
return last.Undo()
}
四、方案优缺点与适用场景
使用命令模式实现撤销操作的最大优势是解耦:调用方不依赖具体业务,新增命令不影响旧代码,且天然支持日志化和事务化。对于编辑器、绘图软件、运维操作平台等需要回溯行为的系统,该模式能显著降低复杂度。同时,由于每个命令都是独立对象,也方便做序列化持久化。
不过它也有代价。每个操作都要生成一个命令对象并保存上下文,在高频或大数据量场景下会带来内存压力。若撤销逻辑写错,可能导致状态不一致。因此在Golang项目中,建议仅为关键操作实现命令回滚,并结合单元测试验证Execute与Undo的对称性,避免线上出现无法还原的脏状态。
五、完整可运行示例整合
将前面几节组合起来,我们就能得到一个具备插入与撤销能力的迷你文档程序。下面示例在main函数中模拟用户输入,依次执行插入、撤销、再插入,并打印文档状态,帮助直观理解整个流程。
func main() {
doc := &Document{}
inv := &Invoker{}
cmd1 := &InsertCommand{doc: doc, text: "Golang "}
cmd2 := &InsertCommand{doc: doc, text: "Command "}
inv.Do(cmd1)
inv.Do(cmd2)
doc.Show()
inv.Rollback()
doc.Show()
inv.Do(&InsertCommand{doc: doc, text: "Pattern"})
doc.Show()
}
通过上述代码可以看到,命令模式在Golang里的落地非常轻量。只要遵循接口约束并妥善保存上下文,就能用少量代码换来清晰的撤销体系。当业务逐渐复杂时,你还可以让Command接口增加Redo方法,或引入宏命令批量执行,进一步释放该模式的扩展潜力。