真要支撑审批类多步骤流程,必须用有向无环图(dag)建模 + 节点级超时/重试 + 状态分粒度持久化;否则上线后改一个节点,整条流程就不可控。

直接上结论:别写 if 套 switch 再套 select 的“伪工作流”,真要支撑审批类多步骤流程,必须用有向无环图(DAG)建模 + 节点级超时/重试 + 状态分粒度持久化。否则上线后改一个节点,整条流程就不可控。
为什么不能用 if-else 或 channel 拼审批流程
常见错误现象是:加个“驳回后回到上一步”逻辑,就得在 3 个地方改代码;人工审批节点一等三天,整个流程 context 就超时卡死;重启服务后所有待审批任务全丢。
- 分支逻辑靠
if判断,节点间依赖关系隐式埋在代码里,无法做静态校验 - 用
channel串行传递数据,一旦某个节点 panic 或阻塞,后续全部 hang 住 - 状态只存内存
map[string]interface{},并发写入会 panic,重启即丢失 - 所有节点共用一个
context.WithTimeout,导致 HTTP 调用等不及、人工审批又等太急
用邻接表 + toposort 构建 DAG 调度骨架
核心不是算法多炫,而是让流程定义可加载、可校验、可调试。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 用
map[string][]string表示邻接表:"approve"→["notify", "record"],不写死执行顺序 - 加载流程定义后立刻调用拓扑排序检测环路,报错必须带路径,例如:
"cycle detected: 'validate' → 'enrich' → 'validate'" - 节点执行函数统一签名:
func(ctx context.Context, data map[string]interface{}) (map[string]interface{}, error),禁止节点内直接调用下一个节点 - 调度器负责查当前节点的
NextNodeIDs,按拓扑序推入待执行队列,节点只管“算”,不管“派”
审批类流程的状态怎么存才不丢不乱
关键在于区分「瞬态上下文」和「持久化快照」——本地跑得通,不代表线上扛得住。
- 每个流程实例必须有唯一
instanceID,所有存储都以它为 key - 高频字段(如
current_node、retry_count)存在 Redis,设 TTL 为 72h,避免脏数据堆积 - 低频但关键字段(如
approval_comment、form_data)走 PostgreSQL,在workflow_instances表中存结构化字段 - 不要每步都序列化整个
datamap,只存 diff:比如用jsonpatch记录变更,或自定义结构体标记dirty字段 - 状态更新和业务操作(如扣库存、发消息)必须包在同一个 DB 事务里,否则出现“流程走到支付,但订单没锁成功”
人工节点(如 wait_for_approval)怎么唤醒不轮询
靠定时轮询查数据库,既浪费资源,又延迟高。真实业务中,审批动作来自外部系统(如钉钉、飞书、内部 OA),必须支持事件驱动恢复。
- 人工节点执行时,只写状态为
ProcessStatePaused,并记录等待的event_type(如"approved"或"rejected") - 对外暴露 webhook 接口,收到审批回调后,立即调用
pubsub.Publish("instance:123", "approved") - 监听该 topic 的 worker 收到消息后,查出对应
instanceID,触发调度器恢复执行 - 超时配置必须独立:比如
Timeout: 72 * time.Hour,MaxRetries: 0(人工不自动重试)
最易被忽略的一点:DSL(比如 YAML 定义流程)只用于路由决策,不参与执行逻辑。节点行为由 Go 函数实现,DSL 只决定“下一步去哪”,不决定“下一步怎么算”。否则,改个条件表达式就得重新编译部署。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










