# 并发规范

> 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 约束。
