命令模式将请求封装为对象,实现发送者与接收者解耦,支持撤销、重做、异步任务管理。通过Command接口和具体实现(如TurnOnLightCommand),结合调用者(Invoker)与历史记录栈,可统一调度操作,提升系统灵活性与可维护性。

Golang中的命令模式,说白了,就是把一个请求封装成一个对象。这样一来,请求的发送者和接收者就彻底解耦了。我个人觉得,它最核心的价值在于,它能让你把“做什么”和“怎么做”分离开来,这在很多复杂系统里简直是福音。你可以把不同的操作当成一个个独立的命令对象,然后统一管理、调度,甚至还能玩出撤销、重做、日志记录这些花样。
解决方案
在Golang里实践命令模式,我们通常会定义一个接口,比如
Command,它只包含一个
Execute()方法。所有具体的请求,比如“打开灯”、“关闭门”,都会被封装成实现这个
Command接口的结构体。这些结构体内部会持有真正执行操作的对象(我们称之为接收者,Receiver)的引用,并包含执行操作所需的参数。
想象一下,你有一个智能家居系统。你不想让遥控器(Invoker)直接知道怎么操作灯泡(Receiver),它只需要知道“我有一个打开灯的命令”就行了。
-
定义命令接口:
立即学习“go语言免费学习笔记(深入)”;
package main import "fmt" // Command 是命令接口,所有具体命令都应该实现它 type Command interface { Execute() error } -
定义接收者(Receiver): 这是真正执行操作的对象。比如,一个灯泡。
// Light 是一个接收者,知道如何打开和关闭 type Light struct { Name string isOn bool } func (l *Light) TurnOn() { if !l.isOn { fmt.Printf("%s 灯亮了\n", l.Name) l.isOn = true } else { fmt.Printf("%s 灯已经亮着呢\n", l.Name) } } func (l *Light) TurnOff() { if l.isOn { fmt.Printf("%s 灯灭了\n", l.Name) l.isOn = false } else { fmt.Printf("%s 灯已经灭着呢\n", l.Name) } } -
定义具体命令(Concrete Command): 这些命令会持有接收者的引用,并调用接收者的特定方法。
// TurnOnLightCommand 是打开灯的命令 type TurnOnLightCommand struct { light *Light } func (c *TurnOnLightCommand) Execute() error { c.light.TurnOn() return nil } // TurnOffLightCommand 是关闭灯的命令 type TurnOffLightCommand struct { light *Light } func (c *TurnOffLightCommand) Execute() error { c.light.TurnOff() return nil } -
定义调用者(Invoker): 调用者不关心具体命令的实现细节,只知道如何执行一个命令。
// RemoteControl 是调用者,它持有并执行命令 type RemoteControl struct { command Command } func (rc *RemoteControl) SetCommand(cmd Command) { rc.command = cmd } func (rc *RemoteControl) PressButton() error { if rc.command == nil { return fmt.Errorf("没有设置命令") } fmt.Println("遥控器按钮被按下...") return rc.command.Execute() } -
实际使用:
// main 函数,模拟客户端代码 func main() { livingRoomLight := &Light{Name: "客厅"} bedroomLight := &Light{Name: "卧室"} turnOnLivingRoom := &TurnOnLightCommand{light: livingRoomLight} turnOffBedroom := &TurnOffLightCommand{light: bedroomLight} turnOnBedroom := &TurnOnLightCommand{light: bedroomLight} remote := &RemoteControl{} // 打开客厅灯 remote.SetCommand(turnOnLivingRoom) remote.PressButton() // 关闭卧室灯 remote.SetCommand(turnOffBedroom) remote.PressButton() // 再次打开卧室灯 remote.SetCommand(turnOnBedroom) remote.PressButton() // 尝试关闭客厅灯 remote.SetCommand(&TurnOffLightCommand{light: livingRoomLight}) remote.PressButton() }通过这种方式,
RemoteControl
根本不知道它在操作的是灯泡,也不知道具体是“打开”还是“关闭”,它只知道有一个Command
需要Execute
。这就是解耦的力量。
Golang中,命令模式与函数式编程的结合点在哪里?
这个问题挺有意思的,说实话,Golang本身对函数式编程的支持不像Haskell或Scala那么纯粹,但它的一等公民函数特性,确实让命令模式有了新的玩法。在我看来,结合点主要体现在,你可以用一个函数类型来充当简单的命令接口,或者在命令结构体内部封装一个函数。
比如,我们最初的
Command接口是
interface { Execute() error }。这其实可以被一个函数类型 type Action func() error所替代,在某些场景下。
什么时候用函数,什么时候用结构体呢? 如果你的命令非常简单,不需要维护任何状态,或者所有状态都能通过闭包捕获,那么直接使用
func() error这样的函数类型作为命令,会非常简洁。比如,你只是想打印一条日志,或者触发一个不带参数的事件,一个匿名函数就能搞定。
// 简单的函数式命令
type SimpleCommand func() error
func (s SimpleCommand) Execute() error {
return s()
}
func main() {
// ... (上面的Light和RemoteControl定义)
// 使用函数式命令来打印一条消息
logCommand := SimpleCommand(func() error {
fmt.Println("这是一个日志命令,由函数实现。")
return nil
})
remote := &RemoteControl{}
remote.SetCommand(logCommand)
remote.PressButton()
// 甚至可以直接把一个匿名函数赋值给一个变量,然后作为命令执行
// 但如果想塞到RemoteControl里,还是需要一个统一的接口。
// 这时候,SimpleCommand这个适配器就派上用场了。
}但如果你的命令需要:
- 维护状态: 比如一个“调整亮度”的命令,它可能需要记住当前亮度值,或者调整的步长。
- 复杂的生命周期或资源管理: 命令执行前后可能需要初始化或清理资源。
-
实现多个操作(如撤销): 这就要求命令对象有多个方法,而不仅仅是
Execute
。 - 需要通过反射或类型断言进行识别: 命令对象本身携带类型信息。
这种情况下,一个结构体实现的命令模式会更合适。结构体能更好地封装状态和行为。
当然,你也可以玩一个“混合体”:一个命令结构体内部包含一个函数字段。这样,命令结构体负责状态和上下文,而具体的执行逻辑则委托给内部的函数。这在一些需要高度灵活性的场景下,比如动态生成命令逻辑时,会非常有用。
如何在Golang中实现命令的撤销(Undo)与重做(Redo)功能?
撤销和重做是命令模式最经典的用例之一。它能实现的关键,就在于命令被封装成了对象,并且这些对象知道如何“反向操作”。
要实现撤销,我们的
Command接口需要稍微扩展一下,增加一个
Undo()方法:
// UndoableCommand 是支持撤销的命令接口
type UndoableCommand interface {
Execute() error
Undo() error
}接着,我们修改具体的命令,让它们也实现
Undo()方法。例如,
TurnOnLightCommand的撤销操作就是
TurnOff(),而
TurnOffLightCommand的撤销操作就是
TurnOn()。
// TurnOnLightCommand 变为可撤销的
type TurnOnLightCommand struct {
light *Light
}
func (c *TurnOnLightCommand) Execute() error {
c.light.TurnOn()
return nil
}
func (c *TurnOnLightCommand) Undo() error {
c.light.TurnOff() // 打开的命令,撤销就是关闭
return nil
}
// TurnOffLightCommand 变为可撤销的
type TurnOffLightCommand struct {
light *Light
}
func (c *TurnOffLightCommand) Execute() error {
c.light.TurnOff()
return nil
}
func (c *TurnOffLightCommand) Undo() error {
c.light.TurnOn() // 关闭的命令,撤销就是打开
return nil
}然后,我们需要一个“历史记录”机制来存储执行过的命令,以便将来撤销。通常,我们会用两个栈(或者切片模拟栈)来实现:
-
undoStack
: 存储已经执行的命令。 -
redoStack
: 存储被撤销的命令。
当一个命令被
Execute()时,它会被推入
undoStack,同时
redoStack会被清空(因为任何新操作都会使之前的重做路径失效)。 当执行
Undo()时,
undoStack顶部的命令被弹出,调用其
Undo()方法,然后被推入
redoStack。 当执行
Redo()时,
redoStack顶部的命令被弹出,调用其
Execute()方法,然后被推入
undoStack。
// CommandHistory 管理命令历史,支持撤销和重做
type CommandHistory struct {
undoStack []UndoableCommand
redoStack []UndoableCommand
}
func NewCommandHistory() *CommandHistory {
return &CommandHistory{
undoStack: make([]UndoableCommand, 0),
redoStack: make([]UndoableCommand, 0),
}
}
func (ch *CommandHistory) ExecuteAndRecord(cmd UndoableCommand) error {
err := cmd.Execute()
if err != nil {
return err
}
ch.undoStack = append(ch.undoStack, cmd)
ch.redoStack = make([]UndoableCommand, 0) // 新操作会清空重做历史
fmt.Println("命令已执行并记录。")
return nil
}
func (ch *CommandHistory) Undo() error {
if len(ch.undoStack) == 0 {
return fmt.Errorf("没有可撤销的命令")
}
cmd := ch.undoStack[len(ch.undoStack)-1]
ch.undoStack = ch.undoStack[:len(ch.undoStack)-1]
err := cmd.Undo()
if err != nil {
return err
}
ch.redoStack = append(ch.redoStack, cmd)
fmt.Println("命令已撤销。")
return nil
}
func (ch *CommandHistory) Redo() error {
if len(ch.redoStack) == 0 {
return fmt.Errorf("没有可重做的命令")
}
cmd := ch.redoStack[len(ch.redoStack)-1]
ch.redoStack = ch.redoStack[:len(ch.redoStack)-1]
err := cmd.Execute()
if err != nil {
return err
}
ch.undoStack = append(ch.undoStack, cmd)
fmt.Println("命令已重做。")
return nil
}
func main() {
// ... (Light定义)
livingRoomLight := &Light{Name: "客厅"}
bedroomLight := &Light{Name: "卧室"}
history := NewCommandHistory()
// 执行并记录命令
history.ExecuteAndRecord(&TurnOnLightCommand{light: livingRoomLight})
history.ExecuteAndRecord(&TurnOnLightCommand{light: bedroomLight})
history.ExecuteAndRecord(&TurnOffLightCommand{light: livingRoomLight})
fmt.Println("\n--- 尝试撤销 ---")
history.Undo() // 撤销关闭客厅灯
history.Undo() // 撤销打开卧室灯
fmt.Println("\n--- 尝试重做 ---")
history.Redo() // 重做打开卧室灯
history.Redo() // 重做关闭客厅灯
fmt.Println("\n--- 再次执行新命令,重做历史被清空 ---")
history.ExecuteAndRecord(&TurnOnLightCommand{light: livingRoomLight})
history.Redo() // 此时会报错,因为重做栈已空
}实现撤销/重做时,最大的挑战是确保
Undo()操作能真正且安全地逆转
Execute()的效果,尤其是涉及到外部状态、并发操作或者资源释放时。有时候,
Undo()可能需要比
Execute()更多的上下文信息,或者需要处理一些副作用。这需要我们在设计具体命令时就考虑周全。
封装异步请求时,Golang命令模式的优势体现在哪些方面?
在Golang中处理异步请求,
goroutine和
channel几乎是标配。但当异步请求变得复杂,需要统一管理、排队、限流、错误重试或者状态追踪时,命令模式就能派上大用场了。
任务抽象与解耦: 一个异步请求,比如发送一个HTTP请求、执行一个数据库查询,本身就是一个操作。用命令模式,你可以把这个操作封装成一个
AsyncCommand
对象。这个对象包含了请求的所有细节(URL、参数、回调函数等),而发起请求的组件(调用者)只知道它要执行一个AsyncCommand
,而无需关心这个请求是如何被发送、如何处理响应的。这极大地提高了代码的模块化和可维护性。-
统一的异步任务队列: 你可以创建一个
WorkerPool
或者一个简单的TaskQueue
,它接收AsyncCommand
接口类型的任务。所有的异步请求都被转化为命令对象,然后提交到这个队列。WorkerPool
里的goroutine
会从队列中取出命令并执行。这对于实现并发控制和限流非常方便。// AsyncCommand 异步命令接口,可能需要返回一个结果或错误通道 type AsyncCommand interface { ExecuteAsync() chan error // 或者 chan interface{} 来返回结果 } // HTTPRequestCommand 封装一个异步HTTP请求 type HTTPRequestCommand struct { URL string Method string Body []byte Response chan []byte // 用于返回响应 Error chan error // 用于返回错误 } func (c *HTTPRequestCommand) ExecuteAsync() chan error { errChan := make(chan error, 1) go func() { // 模拟一个耗时的HTTP请求 fmt.Printf("异步执行 HTTP %s 请求到 %s...\n", c.Method, c.URL) time.Sleep(time.Second * 2) // 模拟网络延迟 if c.URL == "http://bad.example.com" { errChan <- fmt.Errorf("请求 %s 失败:网络错误", c.URL) return } // 模拟成功响应 c.Response <- []byte(fmt.Sprintf("成功响应来自 %s", c.URL)) errChan <- nil }() return errChan } // WorkerPool 异步命令执行池 type WorkerPool struct { commandQueue chan AsyncCommand workerCount int } func NewWorkerPool(workers int) *WorkerPool { return &WorkerPool{ commandQueue: make(chan AsyncCommand, workers*2), // 缓冲区 workerCount: workers, } } func (wp *WorkerPool) Start() { for i := 0; i < wp.workerCount; i++ { go wp.worker(i) } } func (wp *WorkerPool) worker(id int) { fmt.Printf("工作者 %d 启动...\n", id) for cmd := range wp.commandQueue { errChan := cmd.ExecuteAsync() err := <-errChan if err != nil { fmt.Printf("工作者 %d 执行命令失败: %v\n", id, err) } else { // 通常这里会从 cmd.Response 接收结果 fmt.Printf("工作者 %d 执行命令成功。\n", id) } } } func (wp *WorkerPool) Submit(cmd AsyncCommand) { wp.commandQueue <- cmd } func (wp *WorkerPool) Stop() { close(wp.commandQueue) } func main() { pool := NewWorkerPool(3) pool.Start() resp1 := make(chan []byte, 1) err1 := make(chan error, 1) req1 := &HTTPRequestCommand{URL: "http://example.com/api/data", Method: "GET", Response: resp1, Error: err1} pool.Submit(req1) resp2 := make(chan []byte, 1) err2 := make(chan error, 1) req2 := &HTTPRequestCommand{URL: "http://bad.example.com", Method: "POST", Response: resp2, Error: err2} pool.Submit(req2) // 等待结果 select { case res := <-resp1: fmt.Printf("收到响应: %s\n", string(res)) case err := <-err1: fmt.Printf("请求出错: %v\n", err) case <-time.After(5 * time.Second): fmt.Println("等待请求1超时") } select { case res := <-resp2: fmt.Printf("收到响应: %s\n", string(res)) case err := <-err2: fmt.Printf("请求出错: %v\n", err) case <-time.After(5 * time.Second): fmt.Println("等待请求2超时") } time.Sleep(time.Second * 3) // 等待所有worker完成 pool.Stop() }










