Go状态模式核心是用接口定义行为契约、结构体字段存状态、方法委托切换逻辑;状态切换须由当前状态在Handle内更新context.currentState,且避免循环引用和迁移逻辑分散。

Go 语言没有类继承和虚函数,直接套用传统状态模式的 UML 类图会写得很别扭——不是不能实现,而是容易误入“强行模拟 OOP”的坑。关键在于:用接口定义行为契约,用结构体字段保存当前状态,用方法委托而非继承来切换逻辑。
用 interface{} 定义状态行为契约
状态模式的核心是把不同状态下的行为抽成统一接口。Go 里不靠父类,而靠一个干净的接口,比如:
type State interface {
Handle(ctx *Context)
}
所有具体状态(如 IdleState、RunningState)都实现这个接口。注意:接口方法接收 *Context,而不是自身状态指针——因为状态变更要能修改上下文里的 currentState 字段,必须能写回。
常见错误是让 Handle 接收 *IdleState,结果状态切换时无法在上下文中更新引用,导致永远卡在初始状态。
立即学习“go语言免费学习笔记(深入)”;
状态切换必须由状态自己触发,且更新 context.currentState
状态变更不是外部调用 ctx.SetState(Running) 就完事;而是当前状态在 Handle 内部决定下一步该进哪个状态,并显式赋值给上下文的字段:
func (s *IdleState) Handle(ctx *Context) {
fmt.Println("idle → starting...")
ctx.currentState = &RunningState{} // 关键:直接替换
}
这里不能用 new(RunningState) 或匿名结构体,除非确保类型一致且可比较;更稳妥的是定义为导出变量或工厂函数返回指针。
容易忽略的点:ctx.currentState 字段类型必须是 State 接口,否则赋值失败;同时所有具体状态类型需保证线程安全——如果 Context 被并发使用,得加锁或改用原子操作。
避免在状态中持有 *Context 引用(循环依赖风险)
有些实现会让每个状态结构体存一个 *Context 字段,方便回调或读取上下文数据。这看似方便,但极易引发循环引用、内存泄漏,且破坏状态的无状态性。
正确做法是:只在 Handle 方法参数里传入 *Context,按需读取或修改其公开字段/方法。例如:
-
ctx.InputQueue是公开切片,状态可直接append -
ctx.Notify()是公开方法,状态可直接调用 - 绝不让
RunningState包含ctx *Context字段
否则一旦上下文生命周期变长,所有状态实例都会被隐式持有着,GC 无法回收。
用嵌入+接口组合替代“状态基类”,提升复用性
多个状态共享日志、计时器、配置等公共能力?不要建基类,而是定义辅助接口并嵌入:
type Logger interface {
Log(msg string)
}
type Context struct {
currentState State
logger Logger
}
func (c *Context) Log(msg string) { c.logger.Log(msg) }
// 然后在 Handle 中:
func (s *RunningState) Handle(ctx *Context) {
ctx.Log("running tick")
}
这样既避免了继承链,又让各状态通过 ctx 统一访问能力。真正难处理的是状态迁移条件分散、边界不清晰——建议把迁移逻辑收拢到上下文的 Transition 方法里,而不是放任每个状态自由 ctx.currentState = xxx。
状态模式在 Go 里最易错的,不是语法,而是把“谁负责决定切换”搞混:是事件驱动?是定时轮询?还是外部命令?这些决策点必须落在单一位置,否则很快变成一堆 if-else 散落在各个 Handle 里,比不用模式还难维护。










