在业务开发中,我们经常遇到一类对象,它们的行为会随着内部状态的变化而改变。比如一个订单,在待支付状态下可以取消,在已支付状态下可以发货,在已完成状态下既不能取消也不能发货。如果直接在一个方法里写一堆if-else或者switch来判断当前状态,随着状态增多,代码会迅速膨胀成难以维护的意大利面条。状态模式(State Pattern)正是为了解决这个问题而生的:它把每一种状态抽象成一个独立的类型,让状态自己决定该做什么、下一步转移到哪个状态。Go语言虽然没有类和继承,但凭借接口和组合,实现状态模式反而更加简洁自然。

一、状态模式的核心思想与Go语言适配
状态模式属于行为型设计模式,它的核心是把状态的判断逻辑和状态下的行为从上下文对象中抽离出来,分散到各个具体状态对象中。上下文(Context)只持有一个当前状态的引用,所有状态相关的请求都委托给当前状态对象处理。当状态需要变化时,状态对象会反过来修改上下文的引用,实现状态流转。
在Java、C++这类语言中,状态模式通常依赖抽象类和继承来实现公共逻辑复用。Go没有继承,但有两大武器可以替代:interface定义状态的行为契约,struct组合复用公共字段和方法。这种组合方式更符合Go的设计哲学——"接受接口,返回结构体"。此外Go的方法值和方法表达式特性,也让状态方法的传递变得非常灵活。
状态模式和策略模式结构上很像,都通过组合持有可替换的行为对象,但意图完全不同。策略模式中各个策略彼此独立、互不感知,由客户端选择策略;而状态模式中各个状态互相知道彼此的存在,状态之间的流转是模式内部自发完成的,客户端通常不感知具体状态。理解这一点,才能在合适的场景选择合适的模式。
二、用Go实现一个完整的订单状态机
下面以电商订单为例,演示状态模式的完整实现。订单有四种状态:待支付、已支付、已完成、已取消。每种状态对"支付"、"发货"、"取消"三个操作的响应都不同。
第一步,定义状态接口。这个接口声明了所有状态相关的操作:
// OrderState 定义订单状态的行为契约
type OrderState interface {
Pay(ctx *OrderContext) string // 支付
Ship(ctx *OrderContext) string // 发货
Cancel(ctx *OrderContext) string // 取消
Name() string // 状态名称
}第二步,定义上下文对象OrderContext,它持有当前状态,并将请求委托出去:
type OrderContext struct {
OrderNo string
state OrderState
}
func (o *OrderContext) SetState(s OrderState) {
o.state = s
}
func (o *OrderContext) GetState() OrderState {
return o.state
}
// 请求委托给当前状态处理
func (o *OrderContext) Pay() string { return o.state.Pay(o) }
func (o *OrderContext) Ship() string { return o.state.Ship(o) }
func (o *OrderContext) Cancel() string { return o.state.Cancel(o) }第三步,实现各个具体状态。注意状态对象可以是单例的,因为它们不保存状态数据,所有数据都在上下文中:
// 待支付状态
type PendingState struct{}
func (p *PendingState) Pay(ctx *OrderContext) string {
ctx.SetState(&PaidState{})
return "支付成功,订单变为已支付状态"
}
func (p *PendingState) Ship(ctx *OrderContext) string {
return "待支付订单不能发货"
}
func (p *PendingState) Cancel(ctx *OrderContext) string {
ctx.SetState(&CanceledState{})
return "订单已取消"
}
func (p *PendingState) Name() string { return "待支付" }
// 已支付状态
type PaidState struct{}
func (p *PaidState) Pay(ctx *OrderContext) string {
return "订单已支付,请勿重复支付"
}
func (p *PaidState) Ship(ctx *OrderContext) string {
ctx.SetState(&CompletedState{})
return "发货成功,订单完成"
}
func (p *PaidState) Cancel(ctx *OrderContext) string {
ctx.SetState(&CanceledState{})
return "已支付订单取消成功,等待退款"
}
func (p *PaidState) Name() string { return "已支付" }
// 已完成、已取消状态类似,此处省略部分实现
type CompletedState struct{}
func (c *CompletedState) Pay(ctx *OrderContext) string { return "订单已完成,无法支付" }
func (c *CompletedState) Ship(ctx *OrderContext) string { return "订单已完成,无法发货" }
func (c *CompletedState) Cancel(ctx *OrderContext) string { return "订单已完成,无法取消" }
func (c *CompletedState) Name() string { return "已完成" }最后是使用示例:
func main() {
order := &OrderContext{
OrderNo: "SO-1001",
state: &PendingState{}, // 初始状态:待支付
}
fmt.Println(order.Ship()) // 待支付订单不能发货
fmt.Println(order.Pay()) // 支付成功,订单变为已支付状态
fmt.Println(order.Ship()) // 发货成功,订单完成
fmt.Println(order.Cancel()) // 订单已完成,无法取消
}可以看到,上下文中没有任何if-else判断,每个状态各自处理自己的逻辑。如果以后要新增"退货中"状态,只需实现OrderState接口并在合适的位置触发流转即可,完全符合开闭原则。
三、传统switch写法与状态模式的对比分析
如果不使用状态模式,通常会写出这样的代码:
func (o *Order) Ship() string {
switch o.Status {
case StatusPending:
return "待支付订单不能发货"
case StatusPaid:
o.Status = StatusCompleted
return "发货成功"
case StatusCanceled:
return "已取消订单不能发货"
default:
return "订单已完成,无法发货"
}
}这种写法在状态少时确实简单直接,但问题在于:每新增一个状态,所有涉及状态判断的方法都要修改,牵一发而动全身。状态模式则把修改范围缩小到单个状态实现类,各个状态之间互不干扰。两种方式的对比可以总结如下:
| 对比维度 | switch写法 | 状态模式 |
|---|---|---|
| 状态数量少时 | 简单直观,代码量少 | 略显繁琐,类数量多 |
| 新增状态 | 需修改所有相关方法 | 只需新增状态实现 |
| 违反开闭原则 | 是 | 否 |
| 状态流转逻辑 | 分散在各方法中 | 集中在状态对象内 |
| 单元测试 | 需要覆盖所有分支 | 可对单个状态独立测试 |
实际项目中建议按状态数量和流转复杂度来选择:只有两三个简单状态时用switch即可,超过四个状态、或流转规则复杂、或状态需要携带额外数据时,状态模式的优势会非常明显。
四、进阶技巧:并发安全与状态机库的使用
技巧一:并发安全处理。Go的并发场景非常常见,如果订单对象被多个goroutine访问,状态流转必须加锁保护,否则可能出现两个goroutine同时支付导致状态错乱。最简单的做法是在上下文中嵌入sync.Mutex:
type OrderContext struct {
mu sync.Mutex
OrderNo string
state OrderState
}
func (o *OrderContext) Pay() string {
o.mu.Lock()
defer o.mu.Unlock()
return o.state.Pay(o)
}这样上下文负责加锁,状态对象专注业务逻辑,职责清晰。也可以考虑将状态流转改成"提交新状态"的原子操作,用sync/atomic或乐观锁(比如数据库中带version字段的CAS更新)来保证一致性。
技巧二:使用状态机库减少样板代码。如果状态流转规则复杂但行为简单,可以借助社区的状态机库,例如looplab/fsm。它用声明式的方式定义事件和流转规则:
import "github.com/looplab/fsm"
f := fsm.NewFSM(
"pending",
fsm.Events{
{Name: "pay", Src: []string{"pending"}, Dst: "paid"},
{Name: "ship", Src: []string{"paid"}, Dst: "completed"},
{Name: "cancel", Src: []string{"pending", "paid"}, Dst: "canceled"},
},
fsm.Callbacks{
"enter_state": func(e *fsm.Event) {
fmt.Printf("进入状态: %s\n", e.Dst)
},
},
)
err := f.Event("pay") // pending -> paid
fmt.Println(f.Current()) // 输出: paid这种方式把流转规则集中成一张表,一目了然,特别适合审批流、工单流转这类"规则多、动作少"的场景。而自定义行为复杂时,还是手写状态模式更灵活。
技巧三:状态对象可以携带数据。如果某些状态需要保存额外信息,比如"退款中"状态需要记录退款单号,可以让具体状态持有字段,但这时就不能再用全局单例状态对象,每次流转都要创建新实例。另外,为了避免状态间循环依赖,可以让状态返回一个标记或错误,由上下文决定流转目标,这样状态的耦合度会更低。
总结一下,Golang实现状态模式的关键是三步走:定义状态接口、实现具体状态、上下文持有状态并委托请求。写好之后,再根据并发场景补充锁保护,根据复杂度决定是否引入状态机库。掌握这套思路,无论是订单、工单还是游戏角色,凡是涉及状态流转的模型都能写出清晰优雅的代码。