Golang - 状态模式(State Pattern)

发布时间:2026/8/5 11:55:17
Golang - 状态模式(State Pattern) 状态模式State Pattern一、模式定义状态模式允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像修改了自身的类实际上是把不同状态下的行为委托给不同的状态对象来处理。一句话理解同一个方法调用在不同状态下做完全不同的事情。就像你的手机——来电时按音量键调铃声音量听音乐时按音量键调媒体音量拍照时按音量键当快门。按键没变行为随状态变了。二、解决什么问题状态爆炸一个对象有多个状态每个状态下的行为不同。如果用 if-else/switch 管理代码会变成一个巨大的条件分支迷宫状态转换规则复杂状态之间的跳转有严格约束A只能到BB能到C或DC不能直接到A分散在各方法中的 if-else 很难维护状态机的一致性开闭原则新增一个状态时不应该修改已有状态代码三、核心角色角色职责State状态接口声明所有状态共有的行为方法ConcreteState实现特定状态下的行为负责状态转换逻辑Context上下文持有当前状态引用将行为调用委托给状态对象四、结构图┌─────────────────────┐ ┌──────────────────────┐ │ Context │ │ State │ ├─────────────────────┤ ├──────────────────────┤ │ - state State │──────│ HandleRequest() │ │ Request() │ │ NextState(ctx) │ │ SetState(s) │ └──────────┬───────────┘ │ GetState() State │ │ implements └─────────────────────┘ ┌────────────┼────────────┐ │ │ │ ┌──────┴──┐ ┌──────┴──┐ ┌──────┴──┐ │StateA │ │StateB │ │StateC │ │空闲态 │ │处理态 │ │完成态 │ │Handle() │ │Handle() │ │Handle() │ └─────────┘ └─────────┘ └─────────┘五、与策略模式的本质区别虽然结构完全一样但状态转换的控制权不同策略模式由外部调用者Client决定用哪个策略Context 不知道策略之间的存在状态模式由状态对象自己决定下一个状态Context 不知道状态转换的细节这意味着状态类需要持有 Context 的引用或通过参数传入才能在行为执行完毕后触发状态切换。六、完整练习代码以自动售货机为场景有投币态、选择商品态、出货态、退货态四个状态。packagemainimportfmt// // 前向声明VendingMachine 结构体// // VendingMachine 售货机上下文typeVendingMachinestruct{state VendingState balanceint// 当前余额分productstring// 选择的商品priceint// 商品价格分stockint// 库存}// // 状态接口// // VendingState 售货机状态接口typeVendingStateinterface{// InsertCoin 投币InsertCoin(vm*VendingMachine,amountint)// SelectProduct 选择商品SelectProduct(vm*VendingMachine,productstring)// Dispense 出货Dispense(vm*VendingMachine)// Refund 退款Refund(vm*VendingMachine)// Name 状态名称Name()string}// // 具体状态实现// // --- 1. 空闲态等待投币---typeIdleStatestruct{}func(s*IdleState)Name()string{return空闲态}func(s*IdleState)InsertCoin(vm*VendingMachine,amountint){vm.balanceamount fmt.Printf( [空闲态] 投入 %d 分当前余额 %d 分\n,amount,vm.balance)// 余额大于0切换到投币态vm.SetState(HasCoinState{})}func(s*IdleState)SelectProduct(vm*VendingMachine,productstring){fmt.Println( [空闲态] 请先投币再选择商品)}func(s*IdleState)Dispense(vm*VendingMachine){fmt.Println( [空闲态] 请先投币)}func(s*IdleState)Refund(vm*VendingMachine){fmt.Println( [空闲态] 没有余额可退)}// --- 2. 已投币态等待选择商品---typeHasCoinStatestruct{}func(s*HasCoinState)Name()string{return投币态}func(s*HasCoinState)InsertCoin(vm*VendingMachine,amountint){vm.balanceamount fmt.Printf( [投币态] 再投 %d 分当前余额 %d 分\n,amount,vm.balance)}func(s*HasCoinState)SelectProduct(vm*VendingMachine,productstring){ifvm.stock0{fmt.Printf( [投币态] %s 已售罄请退款\n,product)return}vm.productproduct fmt.Printf( [投币态] 选择商品: %s价格 %d 分\n,product,vm.price)// 检查余额是否足够ifvm.balancevm.price{// 余额足够切换到出货态vm.SetState(DispensingState{})}else{// 余额不足提示并保持投币态fmt.Printf( [投币态] 余额不足还差 %d 分\n,vm.price-vm.balance)}}func(s*HasCoinState)Dispense(vm*VendingMachine){fmt.Println( [投币态] 请先选择商品)}func(s*HasCoinState)Refund(vm*VendingMachine){fmt.Printf( [投币态] 退款 %d 分\n,vm.balance)vm.balance0vm.SetState(IdleState{})}// --- 3. 出货态正在出货---typeDispensingStatestruct{}func(s*DispensingState)Name()string{return出货态}func(s*DispensingState)InsertCoin(vm*VendingMachine,amountint){fmt.Println( [出货态] 正在出货请勿投币)}func(s*DispensingState)SelectProduct(vm*VendingMachine,productstring){fmt.Println( [出货态] 正在出货无法更改商品)}func(s*DispensingState)Dispense(vm*VendingMachine){vm.stock--change:vm.balance-vm.price fmt.Printf( [出货态] 商品 %s 已出货\n,vm.product)ifchange0{fmt.Printf( [出货态] 找零 %d 分\n,change)}// 重置状态vm.balance0vm.product// 如果还有库存回到空闲态否则进入售罄态ifvm.stock0{vm.SetState(IdleState{})}else{vm.SetState(SoldOutState{})}}func(s*DispensingState)Refund(vm*VendingMachine){fmt.Println( [出货态] 正在出货无法退款)}// --- 4. 售罄态 ---typeSoldOutStatestruct{}func(s*SoldOutState)Name()string{return售罄态}func(s*SoldOutState)InsertCoin(vm*VendingMachine,amountint){fmt.Println( [售罄态] 商品已售罄拒绝投币)}func(s*SoldOutState)SelectProduct(vm*VendingMachine,productstring){fmt.Println( [售罄态] 商品已售罄)}func(s*SoldOutState)Dispense(vm*VendingMachine){fmt.Println( [售罄态] 无货可出)}func(s*SoldOutState)Refund(vm*VendingMachine){ifvm.balance0{fmt.Printf( [售罄态] 退款 %d 分\n,vm.balance)vm.balance0}vm.SetState(IdleState{})}// // Context 方法实现// funcNewVendingMachine(price,stockint)*VendingMachine{returnVendingMachine{state:IdleState{},price:price,stock:stock,balance:0,}}func(vm*VendingMachine)SetState(s VendingState){fmt.Printf( 状态切换: %s - %s\n,vm.state.Name(),s.Name())vm.states}func(vm*VendingMachine)GetState()VendingState{returnvm.state}func(vm*VendingMachine)InsertCoin(amountint){vm.state.InsertCoin(vm,amount)}func(vm*VendingMachine)SelectProduct(productstring){vm.state.SelectProduct(vm,product)}func(vm*VendingMachine)Dispense(){vm.state.Dispense(vm)}func(vm*VendingMachine)Refund(){vm.state.Refund(vm)}// // 主函数演示售货机状态流转// funcmain(){// 创建售货机商品价格200分(2元)库存2件vm:NewVendingMachine(200,2)fmt.Println( 场景1: 正常购买 )fmt.Println(当前状态:,vm.GetState().Name())vm.InsertCoin(100)// 投入1元vm.InsertCoin(100)// 再投1元vm.SelectProduct(可乐)// 选择商品vm.Dispense()// 出货fmt.Println(\n 场景2: 余额不足再补投 )fmt.Println(当前状态:,vm.GetState().Name())vm.InsertCoin(100)// 投入1元余额100不够vm.SelectProduct(可乐)// 余额不足提示vm.InsertCoin(100)// 再投1元余额200够了vm.SelectProduct(可乐)// 选择商品vm.Dispense()// 出货库存→0进入售罄态fmt.Println(\n 场景3: 售罄后尝试购买 )fmt.Println(当前状态:,vm.GetState().Name())vm.InsertCoin(200)// 售罄态拒绝投币vm.SelectProduct(可乐)// 售罄态拒绝选择vm.Refund()// 无余额可退回到空闲态fmt.Println(最终状态:,vm.GetState().Name())}七、运行结果 场景1: 正常购买 当前状态: 空闲态 [空闲态] 投入 100 分当前余额 100 分 状态切换: 空闲态 - 投币态 [投币态] 再投 100 分当前余额 200 分 [投币态] 选择商品: 可乐价格 200 分 状态切换: 投币态 - 出货态 [出货态] 商品 可乐 已出货 状态切换: 出货态 - 空闲态 场景2: 余额不足再补投 当前状态: 空闲态 [空闲态] 投入 100 分当前余额 100 分 状态切换: 空闲态 - 投币态 [投币态] 选择商品: 可乐价格 200 分 [投币态] 余额不足还差 100 分 [投币态] 再投 100 分当前余额 200 分 [投币态] 选择商品: 可乐价格 200 分 状态切换: 投币态 - 出货态 [出货态] 商品 可乐 已出货 状态切换: 出货态 - 售罄态 场景3: 售罄后尝试购买 当前状态: 售罄态 [售罄态] 商品已售罄拒绝投币 [售罄态] 商品已售罄 [售罄态] 没有余额可退 状态切换: 售罄态 - 空闲态 最终状态: 空闲态三个场景串联了完整的状态流转链空闲态 → 投币态 → 出货态 → 空闲态/售罄态 → 空闲态。八、关键设计决策解析1. 状态对象是否需要持有 Context 引用在本实现中状态对象通过方法参数接收*VendingMachine而不是在构造时保存引用。好处是状态对象可以是无状态的IdleState、HasCoinState等都是零字段结构体可以复用单例实例。如果状态对象需要维护自己的数据比如记录进入该状态的时间则可以在构造时传入 Context 引用。2. 状态转换放在哪里两种风格状态类内部转换本实现状态类在 Handle 方法中调用vm.SetState()切换状态。优点是状态转换规则集中在一处易于理解状态机Context 中集中转换Context 在调用state.Handle()后根据返回值决定下一个状态。优点是状态类更纯粹缺点是 Context 变胖实际项目中简单状态机用第一种更直观复杂状态机建议用状态转换表状态事件→下一状态的映射表。3. 用 map 实现状态转换表当状态和事件多到一定程度时可以用矩阵式转换表替代每个状态类中的 if-else// transitionTable[当前状态][事件] 下一个状态vartransitionTablemap[string]map[string]string{空闲态:{投币:投币态,选择:空闲态,出货:空闲态,退款:空闲态},投币态:{投币:投币态,选择足够:出货态,选择不足:投币态,出货:投币态,退款:空闲态},// ...}这种方式把转换规则从代码中抽离到数据结构中新增状态只需改表不改代码真正实现开闭原则。九、实际项目身影TCP 连接状态机LISTEN → SYN_SENT → ESTABLISHED → FIN_WAIT → CLOSED每个状态下对报文的处理完全不同HTTP 请求生命周期Idle → Reading → Processing → Writing → Closed游戏角色状态站立/跑/跳/攻击/受伤同一输入在不同状态下触发不同动作Go 的database/sql连接池连接有 free/in-use/expired 等状态不同状态下获取/归还行为不同十、小结状态模式的核心价值是把状态驱动的条件分支转化为多态分发。当你发现一个对象有大量if state X { ... } else if state Y { ... }的代码而且每个分支都很长时就是状态模式上场的信号。Go 中通过接口结构体的组合可以非常优雅地实现状态机且状态对象可以设计为零字段的轻量单例。