应封装transitions类型并提供add方法,用map[state]map[event]state定义转移规则,避免硬编码嵌套map;状态行为通过state接口解耦,事件必须为具名struct,transitionto需原子执行exit→查表→enter三步并加锁保护。

用 map[State]map[Event]State 定义转移规则,别手写嵌套字面量
硬编码 map[string]map[string]string 字面量极易漏键、缩进错位,一加新状态就崩。必须封装 Transitions 类型并提供 Add() 方法:
-
rules字段初始化为make(map[State]map[Event]State),避免 nil map panic - 每次
Add(from, event, to)前检查t.rules[from]是否为 nil,再make(map[Event]State) - 即使某状态不允许任何转移(如
StateCancelled),也要显式写t.Add(StateCancelled, EventAny, StateCancelled),否则查表返回零值""导致静默错误
状态行为必须通过接口解耦,拒绝 switch-case 散落各处
把 State 定成字符串常量或自定义类型(type State string),但业务逻辑绝不能靠 switch o.State 分支——那会随状态数爆炸式增长。正确做法是定义 State 接口:
- 至少含
CanHandle(event Event) bool和Handle(ctx context.Context, event Event, data interface{}) error - 每个具体状态(
PendingState、PaidState)实现该接口,只专注自身响应逻辑 - 主结构体持有
current State接口值,调用current.CanHandle(e)判断,再current.Handle(...)执行,完全不暴露状态类型
事件必须是具名 struct,禁止用 string 或 int 当事件标识
用 "pay"、1 这类 magic 值传事件,IDE 不补全、编译器不校验、序列化易错、后期加字段要改所有分支。必须用 struct:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type PayEvent struct{ OrderID string; UserID string }type TimeoutEvent struct{ NodeID string; RetryCount int }- 状态的
Handle()方法按具体 struct 类型接收,Go 编译器强制校验字段存在性与类型 - 事件 struct 不实现接口、不导出方法,纯数据载体,序列化/反序列化天然安全
TransitionTo 必须原子执行:Exit → 查表 → Enter,且带锁
状态迁移不是简单赋值,三步顺序不可颠倒,且并发下必须保护:
- 先调当前状态
OnExit()(如发取消通知、释放资源) - 再查
transitions.rules[current][event]得目标状态,!ok就直接返回ErrInvalidTransition - 最后调目标状态
OnEnter()(如发支付成功消息、更新 DB 字段) - 整个过程用
sync.Mutex锁住状态字段,但不要锁整个结构体——只锁mu sync.RWMutex保护state字段即可
最易被忽略的是 OnExit 和 OnEnter 的幂等性:它们可能被重试触发多次,不能依赖单次执行语义。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










