状态模式在Go中应采用组合+接口委托而非继承,通过State接口和上下文字段实现解耦;并发切换需加锁或atomic.Value;数据共享应通过只读方法或显式传参;接口应最小化,仅定义当前状态必需行为。

状态模式的核心不是继承,而是组合 + 接口委托
Go 没有类继承,所以不能照搬 Java/C++ 里“抽象状态类 + 子类实现”的写法。强行用嵌入(embedding)模拟继承,反而会让状态切换逻辑散落在各处、难以追踪。真正符合 Go 风格的做法是:定义 State 接口,让每个具体状态(如 IdleState、RunningState)独立实现该接口;再由上下文结构体(如 Machine)持有一个 State 类型字段,并把行为委托给当前状态对象。
这样做的好处是:状态逻辑彻底解耦,新增状态只需实现接口,不改上下文;切换时只需替换 state 字段值,无副作用。
如何安全地在运行时切换状态并避免竞态
状态切换常发生在并发场景(比如 goroutine 触发事件),直接赋值 m.state = newState 会导致读写冲突。必须加锁或使用原子操作。推荐用 sync.Mutex 包裹状态字段和所有涉及状态的方法调用:
- 所有公开方法(如
Start()、Pause())内部先mu.Lock(),调用完再mu.Unlock() -
State接口方法本身不加锁——它们只负责业务逻辑,不负责同步 - 避免在状态方法中调用上下文的其他可变方法,否则容易形成锁嵌套或死锁
如果性能敏感且状态切换极少,也可用 atomic.Value 存储 State,但需确保每次赋值都是新实例(因为 atomic.Value 不支持原地修改)。
状态间传递数据的三种可行方式
不同状态可能需要共享上下文数据(如计数器、配置、缓存)。不要让每个 State 实现都持有完整上下文指针——这会破坏封装,也容易引发循环引用。更合理的方式有:
- 通过上下文结构体提供只读访问方法(如
GetCount()、Config()),状态内调用这些方法获取所需信息 - 在状态切换时,由上下文显式传入必要参数(如
s.Enter(ctx, lastEvent)),避免隐式依赖 - 对少量高频访问字段(如
id、timeout),可在State接口方法签名中作为参数传入,而非从上下文中拉取
切忌在 State 实现里直接访问上下文的未导出字段——那等于把封装撕开了一个口子,后续重构成本极高。
为什么 State 接口不宜定义太多方法
常见错误是把所有可能行为(HandleA、HandleB、OnTimeout、OnError……)全塞进 State 接口。结果是每个具体状态都要实现一堆空方法(或 panic),违反接口最小化原则,也掩盖了真实的行为差异。
更好的做法是:
- 只定义当前状态“必须响应”的行为(如
Run()在RunningState中有意义,在StoppedState中应返回错误或忽略) - 对某些状态不支持的操作,统一返回
ErrInvalidState错误,而不是留空实现 - 把事件分发逻辑留在上下文中(如
machine.handleEvent(e)),再根据当前状态调用对应方法——这样能清晰看到“什么事件触发了什么状态行为”
接口越小,实现越轻量,测试越容易覆盖边界。状态模式的价值不在“看起来像设计模式”,而在让状态变更路径可读、可测、可推演。










