# 并发规范 > Go 后端并发约定。 - 每个 goroutine 必须有明确退出路径:外部用 `context.Context` 或 stop channel 通知,内部 `select` 响应;禁止 fire-and-forget,不允许 goroutine 泄漏。 - 调用方能等待 goroutine 退出:`sync.WaitGroup` 或 `done` channel。 - `init()` 中不启动 goroutine、不做 IO。 - channel 容量只用 0 或 1,更大缓冲须注释说明理由。 - 涉及阻塞或远程调用的函数第一个参数是 `ctx context.Context`,不把 ctx 存进 struct。 ## 模式:服务级后台 goroutine 的落地形态(2026-07 起为既定写法) 面板内「异步旁路」(通知发送 `Notifier.SendAsync`、日志异步落库 `SystemLogService.Record`、定期清理 `StartCleanup`)统一三件套: ```go s.wg.Add(1) go func() { defer s.wg.Done() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // 自带超时,不复用请求 ctx defer cancel() if err := s.doWork(ctx); err != nil { log.Printf("xxx: %v", err) // 旁路失败只记日志,绝不影响主流程 } }() ``` - 服务暴露 `Wait()`(内部 `wg.Wait()`);常驻循环(如清理 ticker)额外接收 ctx,`select` 响应退出。 - cmd/server 装配的 defer 顺序必须是「先停生产者、再等消费者」(LIFO):`defer notifier.Wait()` / `defer systemLogs.Wait()` 写在 `defer tasks.Stop()` / `defer stopCleanup()` **之前**。 - 陷阱:`cron.Stop()` 返回的 context 要等(`<-s.cron.Stop().Done()`),否则仍在执行的任务尚未 `wg.Add` 时 `Wait()` 会提前返回,goroutine 泄漏且违反 WaitGroup 的 Add/Wait happens-before 约束。