go状态模式用接口+结构体组合+委托实现,state接口只含状态相关行为,状态体不存业务数据,context统一管控切换并加锁,测试聚焦非法操作拦截。

Go 语言里状态模式不是靠继承模拟“类切换”,而是用接口 + 结构体组合 + 委托调用,把行为逻辑从上下文里彻底剥离。只要接口定义得当、状态切换由 Context 统一控制、避免循环引用和阻塞操作,就能写出清晰、可测、易扩展的状态驱动系统。
State 接口设计要精简,别暴露所有方法
接口里只放真正随状态变化的行为,比如 Pay、Ship、Cancel,而不是把生命周期钩子(OnEnter、OnExit)或通用工具方法也塞进去。否则每加一个事件,所有具体状态都得补实现,违背开闭原则。
- 推荐写法:
type OrderState interface { Pay(*Order) error; Ship(*Order) error } - 错误做法:在接口里加
Log() string或Validate() bool——这些该由 base 结构体或独立函数承担 - 如果事件类型多(如工作流有 10+ 操作),改用
CanHandle(event string) bool+Handle(ctx context.Context, event string, data interface{}) error两方法模型,避免接口爆炸
具体状态结构体不存业务数据,只做决策与流转
状态结构体是轻量的“行为容器”,不是数据载体。订单 ID、创建时间、用户信息这些必须放在 Order(Context)里,状态只负责“当前能做什么”和“下一步切到哪”。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 正确示例:
PaidState的Ship方法里调用order.SetShippedAt(time.Now()),而不是自己存shippedAt time.Time - 常见坑:在
ShippedState{orderID: "xxx"}里缓存 ID,导致状态对象无法复用、GC 压力大、并发下容易错乱 - 若需共享逻辑(如统一打日志),用匿名嵌入
baseState结构体,而非复制代码
Context 必须封装状态切换,禁止状态自行赋值
状态切换入口只能是 Context 提供的 SetState 或带校验的 Transition 方法。不允许具体状态内部直接写 order.State = &CompletedState{},否则绕过日志、校验、hook 等关键控制点。
-
SetState应为私有字段 + 公开方法:func (o *Order) SetState(s OrderState) { log.Printf("state change: %s → %s", o.State.Name(), s.Name()); o.state = s } - 更安全的做法是提供
Transition(event string) error,内部查迁移表(如allowedTransitions["Paid"]["Ship"] = true),再调s.CanHandle(event) - 并发场景下,
SetState必须加锁(sync.Mutex或atomic.StorePointer),尤其当Handle可能异步触发时
测试重点不是“状态有没有切”,而是“非法操作是否被拦截”
单元测试里最该覆盖的是边界情况:比如对已发货订单重复调 Pay() 是否返回明确错误;人工跳转(如从 Pending 直接设为 Completed)是否被 Transition 拦住;并发调用 Ship() 是否不会导致状态错乱。
- 每个具体状态写一个测试文件,验证其方法在当前状态下响应是否符合预期(如
PaidState.Pay()返回“已支付”提示) - 集成测试走完整路径:
NewOrder() → Pay() → check state == Paid → Ship() → check state == Shipped → Pay() → expect error - 用
mock替换具体状态,验证Order是否真的把请求委托给了当前状态,而不是自己写了 if-else
最容易被忽略的是状态间通信的安全性:传给状态的 Context 指针一旦被长期持有(比如启动 goroutine),就可能引发 use-after-free 或竞态;还有迁移表硬编码在代码里,上线后改规则得发版——这些细节不处理,状态机越用越脆。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










