状态模式适合用来处理对象在不同状态下对同一操作有不同响应的场景。它的关键是把状态相关的行为封装到独立的状态对象中,上下文只负责维护当前状态并委托调用。在Go里没有类继承机制,但接口和结构体组合完全可以实现这一模式。下面先看状态模式的基本结构和Go语言下的实现思路。

用接口和上下文解耦状态行为
在状态模式中,上下文对象通常不直接判断自己处于哪种状态,而是持有一个状态接口引用。每一次业务操作都通过这个接口转发给具体状态对象。这样上下文代码不会出现大量if或switch分支,新增状态时也只需要添加一个实现接口的结构体,而不必修改上下文的核心逻辑。
Go语言实现的第一步是定义一个状态接口。接口方法签名需要和上下文能提供的操作对齐。比如上下文有一个Request方法,那么状态接口就可以定义Handle(ctx *Context)。上下文内部维护current字段,并暴露SetState方法来切换状态对象。下面是基础骨架代码:
type State interface {
Handle(ctx *Context)
}
type Context struct {
current State
}
func (c *Context) SetState(s State) {
c.current = s
}
func (c *Context) Request() {
if c.current != nil {
c.current.Handle(c)
}
}
这段代码把状态切换的职责交给了具体状态对象,上下文只提供修改入口。这样做的好处很明显:状态逻辑分散到各个状态结构体中,单个状态变化不会影响其他状态。但要注意,状态对象在调用ctx.SetState时需要知道下一个状态是什么,这通常通过在状态方法内部创建新的状态实例并赋给上下文来完成。
具体状态如何触发行为切换
每个具体状态都实现接口方法,并在方法内部完成两件事:执行当前状态应有的业务逻辑,以及在满足条件时把上下文切换到下一个状态。切换的关键是拿到上下文指针,然后调用其SetState方法。以下示例展示两个状态互相切换的过程:
type StateA struct{}
func (s *StateA) Handle(ctx *Context) {
fmt.Println("当前状态A:执行A逻辑,准备切换到B")
ctx.SetState(&StateB{})
}
type StateB struct{}
func (s *StateB) Handle(ctx *Context) {
fmt.Println("当前状态B:执行B逻辑,准备切换到A")
ctx.SetState(&StateA{})
}
测试时可以这样调用:先创建一个Context,设置初始状态为&StateA{},然后连续调用Request方法。每次调用都会打印当前状态信息,并自动切换为另一个状态。这种写法比在上下文里维护一个字符串状态字段再配合switch分支要清晰得多。
不过真实项目中,状态切换往往不是简单的来回跳转,而是根据业务条件触发。例如支付状态只有在接收到支付成功消息时才切换到已支付状态。此时可以在状态方法中接收额外参数,或者在上下文中保存必要数据,让状态对象根据数据决定跳转目标。更常见的做法是把业务动作本身定义成接口方法,例如Pay、Ship、Cancel,每个状态只关心自己能合法处理的那些动作。
订单状态机完整实现示例
订单系统是状态模式的典型应用场景。一个订单通常会经过待支付、已支付、已发货、已完成、已取消等状态。不同状态下,支付、发货、取消操作的结果完全不同。在待支付状态下可以支付或取消,但不能发货;在已支付状态下可以发货,但不能重复支付。下面给出一个简化但完整的订单状态机实现。
type OrderState interface {
Pay(order *Order) error
Ship(order *Order) error
Cancel(order *Order) error
}
type Order struct {
State OrderState
Paid bool
}
func (o *Order) SetState(s OrderState) {
o.State = s
}
type PendingState struct{}
func (s *PendingState) Pay(order *Order) error {
fmt.Println("支付成功,订单进入已支付状态")
order.Paid = true
order.SetState(&PaidState{})
return nil
}
func (s *PendingState) Ship(order *Order) error {
return fmt.Errorf("待支付订单不能发货")
}
func (s *PendingState) Cancel(order *Order) error {
fmt.Println("订单已取消")
order.SetState(&CancelledState{})
return nil
}
type PaidState struct{}
func (s *PaidState) Pay(order *Order) error {
return fmt.Errorf("订单已支付,请勿重复支付")
}
func (s *PaidState) Ship(order *Order) error {
fmt.Println("开始发货,订单进入已发货状态")
order.SetState(&ShippedState{})
return nil
}
func (s *PaidState) Cancel(order *Order) error {
fmt.Println("已支付订单取消中,将进入退款流程")
order.SetState(&CancelledState{})
return nil
}
type ShippedState struct{}
func (s *ShippedState) Pay(order *Order) error {
return fmt.Errorf("已发货订单不能支付")
}
func (s *ShippedState) Ship(order *Order) error {
return fmt.Errorf("订单已发货,请勿重复操作")
}
func (s *ShippedState) Cancel(order *Order) error {
return fmt.Errorf("已发货订单不能直接取消,请走售后流程")
}
type CancelledState struct{}
func (s *CancelledState) Pay(order *Order) error {
return fmt.Errorf("已取消订单不能支付")
}
func (s *CancelledState) Ship(order *Order) error {
return fmt.Errorf("已取消订单不能发货")
}
func (s *CancelledState) Cancel(order *Order) error {
return fmt.Errorf("订单已处于取消状态")
}
这个实现把所有非法操作都以错误形式返回给调用方,状态转移被限制在合法路径上。调用方只需创建订单并设置初始状态为&PendingState{},然后根据用户动作调用Pay、Ship或Cancel方法即可。每个方法内部会自动更新订单状态,无需外部维护状态字段。
需要特别注意的是,状态对象应该尽量保持无状态。本例中的具体状态都是空结构体,这有助于状态实例被复用,也能避免多个订单共享同一个状态对象时出现数据竞争。如果某些状态确实需要保存临时数据,建议把数据放在上下文对象Order中,而不是放在状态结构体里。
并发安全与可测试性考量
在Go项目中,状态模式经常与并发场景结合,尤其是Web服务或消息消费者。上下文对象可能被多个goroutine同时访问,因此状态切换操作必须保证原子性。最简单的做法是在SetState方法里加互斥锁,或者使用sync/atomic配合接口指针。例如可以给Order增加一个sync.Mutex字段,所有修改状态的方法先加锁再切换。
type Order struct {
mu sync.Mutex
State OrderState
Paid bool
}
func (o *Order) SetState(s OrderState) {
o.mu.Lock()
defer o.mu.Unlock()
o.State = s
}
func (o *Order) Pay() error {
o.mu.Lock()
defer o.mu.Unlock()
return o.State.Pay(o)
}
func (o *Order) Ship() error {
o.mu.Lock()
defer o.mu.Unlock()
return o.State.Ship(o)
}
func (o *Order) Cancel() error {
o.mu.Lock()
defer o.mu.Unlock()
return o.State.Cancel(o)
}
加锁以后,状态查询和切换变成串行操作,避免了并发修改导致的状态错乱。但要警惕死锁风险:状态方法内部如果再次调用上下文的加锁方法,而外层已经持有锁,就会造成死锁。解决方式是在状态方法内部只操作传入的上下文数据,不重复调用上下文的公开加锁方法,或者把锁的粒度缩小到具体字段级别。
可测试性方面,状态模式的优势是每个状态可以独立测试。你可以编写单元测试直接调用具体状态的方法,传入一个设定好初始数据的Order实例,验证状态是否按预期切换。由于状态对象之间解耦,测试某个状态时不依赖其他状态的实现细节。同时上下文对象的公开方法保持统一,集成测试时只需要模拟用户动作序列,检查最终状态是否符合业务规则。
如果在项目中发现某个结构体的方法里出现了超长的switch或if分支来判断当前状态,那么引入状态模式通常能显著改善可维护性。它虽然会增加一些类型数量,但换来的是清晰的状态转移图和可扩展性。对于状态数量较多、转移规则复杂的业务模块,这种代价非常值得。