Go中State接口设计核心是用接口定义行为契约、结构体字段存状态、指针接收者动态替换状态;必须避免值传递、空指针、竞态,统一通过SetState切换并校验nil。

什么是 Go 里的 State 接口设计核心
Go 没有类和继承,所以状态模式不能照搬 Java/C# 的抽象类 + 子类继承写法。关键在于:用接口定义行为契约,用结构体字段保存当前状态,并通过指针接收者方法动态替换自身状态。
典型错误是把状态当作值类型传参或复制,导致状态切换不生效;正确做法是让上下文(如 *VendingMachine)持有指向状态接口的指针,并在状态变更时更新该指针。
-
State必须是接口,至少包含当前业务需要响应的行为方法(如InsertCoin()、PressButton()) - 每个具体状态实现为独立结构体,方法接收者必须是指针(
func (s *HasCoinState) InsertCoin(m *VendingMachine)),否则无法修改m.state - 上下文结构体里状态字段类型是
State接口,不是具体类型
如何安全地在状态间切换而不引发 panic
常见 crash 场景是状态方法里调用了尚未初始化的上下文字段,或在切换过程中出现竞态(尤其并发调用时)。Go 中最稳妥的做法是:所有状态变更只通过上下文提供的统一入口(如 SetState(s State)),并在其中做 nil 检查和原子赋值。
例如,避免直接写 m.state = &SoldOutState{},而应封装为:
立即学习“go语言免费学习笔记(深入)”;
func (m *VendingMachine) SetState(s State) {
if s == nil {
panic("state cannot be nil")
}
m.state = s
}- 每次状态变更前检查
s != nil,防止空指针解引用 - 如果涉及并发访问,
m.state字段需用sync/atomic.Value或互斥锁保护 - 不要在状态方法内部直接 new 新状态并赋值给
m.state,应由上下文统一调度,便于测试和拦截
switch 和状态接口哪个更适合行为分支
用 switch 枚举状态类型(如 type StateType int; const HasCoin StateType = iota)看似简单,但会破坏状态模式的核心价值:开闭原则。一旦新增状态,所有 switch 处都要改,且无法封装各自逻辑。
接口方式虽然多写几个文件,但换来的是可插拔性——比如测试时可注入 mock 状态,运维时可动态加载新状态实现。
- 用
switch仅适用于状态极少(≤3)、逻辑极简、且确定永不扩展的场景 - 接口方式下,每个状态的副作用(如日志、通知、DB 更新)完全隔离,不会污染其他状态分支
- 注意:Go 的接口是隐式实现,无需声明
implements,但结构体字段命名要一致(如都含machine *VendingMachine才能复用公共逻辑)
为什么常在状态方法里传入 *VendingMachine 而非只传数据
因为状态行为往往需要触发上下文的副作用:扣减库存、发消息、重置计时器、切换到下一个状态。如果只传原始数据(如 coinCount int),状态实现就变成纯函数,无法驱动系统流转。
典型例子:SoldState.PressButton() 需要调用 m.ReleaseItem() 并立即设为 SoldOutState,这两步必须发生在同一上下文实例上。
- 传
*VendingMachine是为了获得「执行权」,不是为了读取字段——尽量减少状态对上下文内部字段的直接访问 - 若担心循环引用,可将上下文需暴露的能力抽成小接口(如
Releaser、Resetter),状态只依赖接口而非具体结构体 - 切勿在状态方法中启动 goroutine 并异步修改
m.state,这极易导致状态错乱,应改为同步调用m.SetState()
状态模式在 Go 里真正难的不是语法,而是克制——忍住不用 switch,忍住不在状态里直接操作上下文私有字段,忍住不把状态逻辑塞进一个大结构体里。每多一层间接,就多一分可维护性。










