Go中不用继承而用接口组合实现状态模式,因Go无类继承机制,需通过State接口及组合实现行为切换,符合“组合优于继承”原则,避免耦合与类型断言。

状态模式在 Go 中为什么不用继承而用接口组合
Go 没有类继承,所以不能像 Java 那样让 State 成为抽象基类、让具体状态去继承。真正可行的路径是定义一个统一的状态接口(比如 State 接口),再让每个状态类型实现它;然后在上下文结构体(如 Context)中持有一个 State 接口变量,通过赋值切换行为。
这种设计避免了运行时类型断言或反射,也符合 Go 的组合优于继承原则。如果误用嵌入 struct 来“模拟继承”,反而会让状态间耦合变高、切换逻辑分散——这是初学者最容易踩的坑。
- 所有状态类型必须实现同一组方法(如
Handle()、TransitionTo()) -
Context不应知道具体状态类型,只依赖接口 - 状态切换必须由当前状态或
Context主动调用,不能靠外部强制类型赋值
如何安全地在 Context 中切换状态并防止竞态
当多个 goroutine 可能并发调用 Context.Handle() 时,直接修改 ctx.currentState 字段会引发数据竞争。正确做法是加锁,但更推荐把状态切换封装进方法内部,并确保整个「读当前状态 → 执行逻辑 → 写新状态」是原子的。
另外要注意:状态对象本身是否可复用?如果不同 Context 实例共享同一个状态实例(比如 IdleState{}),那它就不能含任何属于某个上下文的字段(如计数器、缓存等),否则会相互污染。
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.RWMutex保护currentState字段读写 - 状态切换逻辑统一收口到
Context.SetState(s State)方法中 - 避免在状态方法里直接修改
Context的非状态字段,除非明确约定该状态拥有该字段所有权
一个最小可运行的 Go 状态模式示例(带日志和切换校验)
下面是一个带状态流转约束的典型例子:订单从 Created → Paid → Shipped,不允许跳步或回退。每个状态的 Process() 方法决定能否切换、切到哪。
type State interface {
Process(ctx *OrderContext) string
}
type CreatedState struct{}
func (s *CreatedState) Process(ctx *OrderContext) string {
ctx.SetState(&PaidState{})
return "order paid"
}
type PaidState struct{}
func (s *PaidState) Process(ctx *OrderContext) string {
ctx.SetState(&ShippedState{})
return "order shipped"
}
type ShippedState struct{}
func (s *ShippedState) Process(ctx *OrderContext) string {
return "already shipped"
}
type OrderContext struct {
mu sync.RWMutex
currentState State
}
func (c *OrderContext) SetState(s State) {
c.mu.Lock()
defer c.mu.Unlock()
c.currentState = s
}
func (c *OrderContext) Handle() string {
c.mu.RLock()
s := c.currentState
c.mu.RUnlock()
if s == nil {
return "no state set"
}
return s.Process(c)
}
// 使用:
// ctx := &OrderContext{currentState: &CreatedState{}}
// fmt.Println(ctx.Handle()) // "order paid"
// fmt.Println(ctx.Handle()) // "order shipped"
// fmt.Println(ctx.Handle()) // "already shipped"
什么时候不该用状态模式而该用 switch 或 map 查表
如果状态数量少(≤3)、行为差异小(比如只是返回不同字符串)、且不涉及复杂前置校验或副作用,硬套状态模式反而增加认知负担和间接层。此时用 switch ctx.state 更直白;若状态名是字符串且需动态注册,用 map[string]func(){...} 更灵活。
另一个信号是:当你发现 80% 的状态方法都只是转发调用、或大量重复代码(如每个状态都要检查用户权限),说明职责没划清——可能该把权限检查提到 Context.Handle() 层,而不是塞进每个状态里。
- 状态之间行为差异大、生命周期长、需各自维护内部数据 → 适合状态模式
- 状态只是枚举值,行为由外部统一调度 → 用
switch更轻量 - 需要热插拔状态(运行时加载/卸载)→ 用
map[string]State+ 工厂函数
状态模式真正的价值不在“切换”本身,而在把分散的条件分支逻辑,沉淀为可测试、可复用、可独立演进的类型。别为了模式而模式,尤其当 if/else 还没超过二十行的时候。










