Go中状态模式用接口+结构体组合实现,State接口定义行为契约,具体状态结构体实现接口并返回新状态,Context持有状态接口并统一管理切换与校验。

Go 语言没有类和继承,状态模式不能照搬传统 OOP 的实现方式;但可以用接口 + 结构体组合 + 方法集来等效实现,核心是把“状态”抽象为接口,每个具体状态实现该接口,上下文(Context)持有一个状态接口变量并委托行为。
用 State 接口定义行为契约
状态模式的关键是把变化的状态行为抽离出去。在 Go 中,定义一个 State 接口,包含所有可能被状态驱动的方法(比如 Handle、OnEnter)。不要试图让状态结构体嵌入上下文,也不要让上下文实现状态接口——职责必须分离。
常见错误是把状态逻辑写死在 Context 里,靠 switch 判断当前状态枚举值分支调用,这违背了开闭原则,新增状态就得改 Context。
-
State接口方法应只接收必要参数,避免依赖上下文内部字段 - 如果状态需要修改上下文数据,通过传入指针或回调函数(如
func() *Context)解耦 - 接口方法名保持动词开头(如
Process、Cancel),不加State前缀
用结构体实现具体状态并控制流转
每个具体状态(如 IdleState、RunningState)是普通结构体,实现 State 接口。状态切换不发生在接口方法内部,而由该方法返回下一个状态实例(或通过回调通知 Context 更新),这样能清晰看到流转路径,也便于单元测试。
立即学习“go语言免费学习笔记(深入)”;
示例中容易踩的坑:在 RunningState.Process() 里直接给 Context 的 state 字段赋新状态,导致无法做前置校验或日志;正确做法是返回 State,由 Context 统一处理赋值与生命周期管理。
- 状态结构体通常不含字段,或只含只读配置(如超时时间),避免状态间共享可变数据
- 若需访问上下文,通过构造函数注入(
NewRunningState(ctx *Context)),而非在方法中传参 - 避免在状态实现中调用
time.Sleep或阻塞 I/O,否则会卡住整个 Context
Context 持有状态接口并封装切换逻辑
Context 是状态的容器和调度中心,它持有 state State 字段,并提供公开方法(如 Start()、Pause())作为用户入口。这些方法不直接实现业务逻辑,而是调用当前 state 的对应方法,并按返回值更新自身状态。
注意:不要让 Context 实现 State 接口,否则会模糊“谁负责状态判断、谁负责行为执行”的边界;也不要让状态反向持有 Context 指针(除非必要),这会增加循环依赖风险。
- Context 的状态字段应设为私有(小写
state),强制通过方法变更 - 每次状态变更建议记录日志或触发 hook(如
onStateChange(old, new)) - 如果状态流转有条件限制(如只能从
Idle→Running,禁止Running→Idle),校验逻辑放在 Context 的公开方法里,不在状态实现中
测试状态切换时重点覆盖边界与并发
Go 中状态模式最难测的不是单个状态行为,而是切换过程中的竞态和中间态。例如两个 goroutine 同时调用 ctx.Pause() 和 ctx.Resume(),可能导致状态丢失或重复执行。
真实项目中,状态机常配合 channel 或 sync/atomic 使用;但状态模式本身不解决并发问题——它只解决行为组织。所以测试必须包含并发调用场景,且验证最终状态是否符合预期。
- 用
sync.WaitGroup启多个 goroutine 触发状态变更,再用atomic.LoadPointer或互斥检查最终状态 - 对每个状态实现编写独立单元测试,输入固定上下文快照,断言输出动作(如是否发送消息、是否修改某字段)
- 避免在测试中 sleep 等待状态变更,改用 channel 通知或轮询加超时
状态模式在 Go 里真正难的不是写法,而是决定哪些逻辑该放进状态、哪些该留在 Context;一旦划分模糊,就容易变成“接口套接口”的过度设计。多数时候,先用 switch 快速验证流程,再根据扩展需求逐步提取状态接口更稳妥。










