微服务核心解法是分层契约+显式组合+状态收口;用stepfunc+then实现可测流程链,transitionto统一状态变更,saga处理跨服务事务,dag工作流支撑复杂审批。

微服务不是把单体拆成一堆 HTTP 接口就完事了;复杂业务逻辑在拆分后反而更难协调——状态不一致、事务跨服务、错误恢复无序、调试链路断裂,这些才是真痛点。核心解法不是堆中间件,而是用分层契约 + 显式组合 + 状态收口,在服务边界内守住逻辑完整性。
服务内用 StepFunc + Then 实现可测可复用的流程链
别在 handler 里写 10 层嵌套:查用户 → 校验余额 → 调支付网关 → 发 Kafka → 更新订单 → 记日志 → 发短信……这种写法无法单测、改一处牵全身、错误处理全靠 if err != nil { return } 手动传播。
- 定义统一签名:
type StepFunc[T, R any] func(T) (R, error),所有环节(如validateOrder、fetchInventory、chargePayment)都遵守它 - 用
Then组合:Then(validateOrder, fetchInventory, chargePayment, saveOrder),失败自动短路,成功才透传结果 - I/O 操作必须带
context.Context控制超时,禁止在链中直接阻塞;http.Client.Do必须用ctx,不能裸调 - 每个 step 只做一件事,返回 error 必须用
%w包装,比如return fmt.Errorf("failed to charge: %w", err)
跨服务状态变更必须走 TransitionTo,禁用字符串赋值
订单从 pending → paid → shipped,如果两个服务同时写 order.State = "shipped",就会覆盖对方变更、漏触发钩子、跳过合法性校验——线上最常见的是支付回调重试 + 人工发货同时到达,发两遍货单。
- 状态值用常量:
const StatePaid = "paid",禁用"paid"字面量 - 转移入口唯一:
order.TransitionTo(StatePaid),内部只做三件事:查validTransitions[StatePending][StatePaid]表、原子更新字段(用stateMu.Lock())、触发onEnter/onExit - 耗时动作(如通知物流、发邮件)移出
TransitionTo,改为状态变更后启动 goroutine,通过 channel 或消息队列异步执行 - 读状态用
stateMu.RLock(),避免只读操作被写锁阻塞
事务与审批流需显式终局控制,不能靠 panic 或隐式返回
有人在审批节点写 if amount > 10000 { panic("exceed limit") },结果 recover 不到,或恢复后流程继续往下走,导致误审。更糟的是,事务没回滚,状态却变了。
- 审批函数签名必须是:
func(ctx context.Context, req *ApprovalRequest) (bool, error),返回true表示“已终局(通过/拒绝),不再流转” - 错误要结构化:
ApprovalError{Code: "AMT_EXCEED", Abort: true},Abort: true则整条链立即终止 - GORM 事务必须用
db.Transaction(func(tx *gorm.DB) error { ... }),别在 handler 里手动Begin/Commit;事务块内所有 DB 操作必须用传入的tx,不能混用db - 跨服务事务用 Saga 模式:本地事务 + 补偿动作,补偿函数也必须是
StepFunc链,失败走重试队列
工作流引擎选型:DAG 建模 + 粒度化状态持久化
当业务出现循环审批、条件跳转、多角色协同(比如“先意图识别 → 若是投诉则情感分析 → 再路由到人工”),线性函数链撑不住,就得上 DAG 工作流。但自研容易踩坑:状态丢、超时错配、重启后流程断。
- 节点必须统一签名:
func(ctx context.Context, data map[string]interface{}) (map[string]interface{}, error),不暴露跳转逻辑 - 每个流程实例有唯一
instanceID,高频字段(当前节点名、重试次数)存 Redis 带 TTL,关键字段(原始表单、审批意见)存 PostgreSQL 的workflow_instances表 - 节点级独立配置:
Timeout: 30 * time.Second、MaxRetries: 3,别给整个流程包一层context.WithTimeout - 人工等待节点(如
wait_for_approval)必须支持外部事件唤醒(Webhook → Pub/Sub → 恢复流程),禁止轮询
真正麻烦的从来不是“怎么写完”,而是“怎么让别人看懂、改得动、出问题能快速定位”。函数链的顺序就是执行顺序,状态转移表就是合法路径,事务块就是原子边界——这些契约一旦立住,复杂性就可控。否则,服务越拆越多,问题越藏越深。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











