我用 WaitGroup 等一批 goroutine 全部干完,却把 wg.Add 写进了 goroutine 内部,结果主协程的 Wait 在 Add 之前就跑完直接返回,程序以为活儿都干完了其实大半还没开始:一次 WaitGroup 时序用错的深度复盘

我要启动一批 goroutine 并发处理任务、再用 WaitGroup 等它们全部干完汇总结果,写成了在循环里 go func(){ wg.Add(1); defer wg.Done(); doWork() }()——把 wg.Add(1) 放在了 goroutine 内部。线上现象诡异:不报错,但汇总结果经常不完整,明明启动 100 个任务却只有零星几个的结果。复盘才搞懂:WaitGroup 是计数器,Add 加、Done 减、Wait 阻塞到归零;而 go 启动 goroutine 是异步的,主协程很可能在那些 goroutine 还没执行到内部的 wg.Add(1) 时就跑到了 wg.Wait(),此时计数器还是 0、Wait 一看是 0 就立刻返回了,主协程以为任务都干完、其实大部分还没开始。根因是把登记(Add)放在了被等待的 goroutine 内部、与主协程的 Wait 形成竞态。WaitGroup 的铁律是 Add 必须在启动 goroutine 的 go 语句之前(主协程里)调用。这篇复盘从故障现场讲到 WaitGroup 计数器机制、为何 Add 必须在 go 之前、其他常见误用,再到 Add 在 go 之前、defer Done 放协程开头、数量匹配、传指针、用 errgroup 的完整正解,以及其他并发时序协调的坑,和异步下代码顺序不等于执行顺序、协调机制依赖严格的先后时序、登记必须先于等待的认知。

我用 WaitGroup 等一批 goroutine 全部干完,却把 wg.Add 写进了 goroutine 内部,结果主协程的 Wait 在 Add 之前就跑完直接返回,程序以为活儿都干完了其实大半还没开始:一次 WaitGroup 时序用错、把登记放在了等待之后的深度复盘

那个"任务明明没做完,程序却说做完了"的诡异 bug,让我对 sync.WaitGroup 的用法彻底较了真。我有个场景:启动一批 goroutine 并发处理任务,然后用 WaitGroup 等它们全部干完,再汇总结果。我写成了这样:在循环里 go func(){ wg.Add(1); defer wg.Done(); doWork() }()——wg.Add(1) 放在了启动的 goroutine 内部。然后主协程 wg.Wait() 等待。线上现象诡异:程序没报错,但汇总出来的结果经常是不完整的——明明启动了 100 个任务,汇总时却只有零星几个的结果,像是大部分 goroutine 还没干完,主协程就往下走了。复盘 WaitGroup 的机制,我才彻底搞懂,后背发凉:问题出在我把 wg.Add(1) 放在了 goroutine 内部,造成了"登记"和"等待"的时序错乱。WaitGroup 的原理是维护一个计数器:Add(n) 把计数加 n(登记"有 n 件事要等"),Done() 减 1(报告"一件事干完了"),Wait() 阻塞直到计数器归零;关键在于:主协程的 Wait() 和 goroutine 内部的 Add()并发执行的,谁先谁后没有保证;而我的循环 go func(){...}() 启动 goroutine 是异步的——主协程很可能在那些 goroutine 还没来得及执行到内部的 wg.Add(1) 时,就已经跑到了 wg.Wait();此时计数器还是 0(没几个 Add 执行),Wait() 一看"计数器是 0",立刻就返回了——主协程以为"所有任务都干完了",其实大部分 goroutine 还没开始干。根本原因是:wg.Add 必须在启动 goroutine 之前(主协程里)调用,以确保 Wait 执行时计数器已正确登记;我把 Add 放进了 goroutine 内部,使得"登记"与"等待"产生竞态,Wait 在 Add 之前执行、看到计数器为 0 就提前返回。问题的根,是 WaitGroup 的 Add 时序用错——把登记(Add)放在了被等待的 goroutine 内部,与主协程的 Wait 形成竞态,导致 Wait 在登记完成前就看到计数 0 而提前返回。这篇就把这次"WaitGroup 时序"的坑,从头到尾复盘一遍。

故障现场:Add 在 goroutine 里,Wait 提前返回

问题在于 Add 与 Wait 的时序竞态:

// 我的错误写法: wg.Add(1) 放在 goroutine 内部
func process(tasks []Task) []Result {
    var wg sync.WaitGroup
    results := make([]Result, 0)
    var mu sync.Mutex

    for _, task := range tasks {
        go func(t Task) {
            wg.Add(1)              // ✗ Add 在 goroutine 内部! 与主协程的 Wait 竞态
            defer wg.Done()
            r := doWork(t)
            mu.Lock(); results = append(results, r); mu.Unlock()
        }(task)
    }

    wg.Wait()                      // 主协程在这里等
    return results                 // ✗ 可能在大部分 goroutine 还没 Add 时就返回了!
}

/*
为什么结果不完整(时序竞态):
  - WaitGroup 维护一个计数器: Add(n)加n、Done()减1、Wait()阻塞到计数器归零;
  - 我的 for 循环用 go 启动 goroutine 是"异步"的: go 语句立即返回, goroutine 何时真正
    开始执行(执行到内部的 wg.Add(1))是不确定的、由调度器决定;
  - 主协程 for 循环跑完(只是"发起"了100个go), 立刻执行 wg.Wait();
  - 此时那100个 goroutine 可能一个都还没执行到 wg.Add(1) → 计数器还是 0;
  - wg.Wait() 一看计数器=0 → "没有要等的" → 立即返回!
  - 主协程返回 results(此刻几乎是空的), 而那些 goroutine 还在后台慢慢跑、慢慢 Add/Done;
  → 表现: 没报错, 但结果不完整(主协程没等真正的任务完成就走了)。

  (有时还会更糟: 若Wait归零返回后又有goroutine Add, 可能触发 panic: WaitGroup misuse)

★ WaitGroup 的铁律(官方文档明确):
  Add 必须在 "会导致 Wait 的那个 goroutine" 之前调用, 即:
  Add 要在启动 goroutine 的 go 语句【之前】, 在主协程里调用——
  这样 Wait 执行时, 计数器已经被正确登记好了, 不会出现"还没登记就开始等"的竞态。
*/

看着那个"没报错但结果残缺"的输出,我又困惑又恍然:"我以为 Add 写在 goroutine 里、紧挨着 Done,逻辑更紧凑、更清晰……谁知道主协程的 Wait 根本不等这些 Add 执行,看见计数器是 0 就走了!原来 Add 必须在 go 之前登记好。"这个坑最隐蔽的地方在于:不一定每次都错——有时调度器让几个 goroutine 抢先执行了 Add,Wait 就会等(碰巧对);有时主协程跑得快,Wait 提前返回(错)。这种时序相关的竞态,表现飘忽不定、难以复现,在测试里(任务少、跑得快)可能恰好没暴露,一上线(任务多、负载高)就出问题下面就来拆解,WaitGroup 到底该怎么用。

第一件事:搞懂 WaitGroup 的计数器与 Add 时序

我顺着这次事故,把 WaitGroup 的机制和正确用法彻底理清了。

WaitGroup 怎么工作? 为什么 Add 必须在 go 之前?

【核心: WaitGroup是计数器(Add加/Done减/Wait阻塞到0); Add必须在启动goroutine的go语句之前(主协程里)
   调用, 否则Wait可能在Add之前执行、看到计数0就提前返回; defer wg.Done()放协程开头, Add数量要匹配】

1. WaitGroup 的三个操作:
   - Add(n): 计数器 += n(登记"有n件事要等");
   - Done(): 计数器 -= 1(报告"一件事完成了", 等价于 Add(-1));
   - Wait(): 阻塞当前协程, 直到计数器变为 0。

2. 为什么 Add 必须在 go 之前(时序是关键):
   - go func(){...}() 是异步的: go语句立即返回, 协程体何时执行由调度器定, 不确定;
   - 若 Add 在协程体内 → 主协程可能在协程还没执行Add时就到了Wait → 计数器仍是0 → Wait立即返回;
   - 把 Add(1) 放在 go 之前(主协程里) → Wait执行时计数器已经是正确的总数 → 才会真正等待。
   - 官方文档原话: "the Add call should happen before the statement creating the goroutine"。

3. 正确的固定写法(背下来):
   for _, t := range tasks {
       wg.Add(1)              // ① Add 在 go 之前, 主协程里
       go func(t Task) {
           defer wg.Done()    // ② Done 用 defer 放协程开头, 保证一定执行(即使panic)
           doWork(t)
       }(t)                   // ③ 循环变量用参数传入(同544篇, 避免闭包捕获问题)
   }
   wg.Wait()                  // ④ 等全部完成

   或一次性 Add: wg.Add(len(tasks)) 再循环 go(数量确定时)。

4. 其他常见 WaitGroup 误用:
   - 忘了 Done / Done次数不够 → 计数器永不归零 → Wait 永久阻塞(死锁, fatal error);
   - Done 多调了 → 计数器变负 → panic: negative WaitGroup counter;
   - Add 的数量和实际 goroutine 数不匹配 → 少等或多等;
   - 复制 WaitGroup(值传递)→ 副本和原来的不是同一个计数器(WaitGroup不能复制, 要传指针);
   - Wait 返回后又 Add(复用同一个wg做下一轮)→ 可能 panic, 要确保上一轮Wait后再Add。

5. 本质: WaitGroup 是"登记-完成-等待"的协调机制
   - "登记(Add)"必须先于"等待(Wait)", 否则等待方不知道要等多少;
   - 这是个时序约束: 先把"要等几件事"登记清楚, 再开始等。

一句话: WaitGroup是计数器, Add必须在启动goroutine的go语句之前(主协程)调用, 否则Wait与Add竞态、
   会提前返回; defer wg.Done()放协程开头、Add数量匹配、别复制wg(传指针)、Wait后再复用要谨慎。

这套认知,是整个坑的根。WaitGroup 的三个操作:Add(n) 登记、Done() 完成减一、Wait() 阻塞到计数器归零为什么 Add 必须在 go 之前:go 是异步的、协程体何时执行不确定;Add 在协程体内则主协程可能在 Add 前就到 Wait、计数器仍是 0、Wait 立即返回;Add 放 go 之前则 Wait 时计数器已是正确总数正确写法:循环里 wg.Add(1) 在 go 之前、defer wg.Done() 放协程开头、循环变量用参数传入(或数量确定时一次性 Add(len))。其他误用:忘 Done 永久阻塞死锁、Done 多调 panic 负计数、数量不匹配、复制 wg、Wait 后复用本质:"登记(Add)"必须先于"等待(Wait)",否则等待方不知道要等多少一句话:WaitGroup 是计数器,Add 必须在启动 goroutine 的 go 语句之前(主协程)调用,否则 Wait 与 Add 竞态、会提前返回;defer wg.Done() 放协程开头、Add 数量匹配、别复制 wg(传指针)、Wait 后再复用要谨慎。

第二件事:正解——Add 在 go 之前、defer Done、数量匹配

知道了时序的关键,正解就清楚了:把 Add 提到 go 语句之前,Done 用 defer 兜住。

// 正解1: Add 在 go 之前(主协程里), defer Done 在协程开头(本次该做的)
func process(tasks []Task) []Result {
    var wg sync.WaitGroup
    results := make([]Result, len(tasks))   // 预分配, 按索引写, 避免append加锁

    for i, t := range tasks {
        wg.Add(1)                  // ✓ Add 在 go 之前! 主协程里, Wait时计数已正确
        go func(idx int, task Task) {
            defer wg.Done()        // ✓ defer 放协程开头, 保证一定执行(哪怕doWork panic)
            results[idx] = doWork(task)   // 各写各的索引, 无需锁
        }(i, t)                    // ✓ 循环变量用参数传入(同544篇)
    }

    wg.Wait()                      // 此时计数器=len(tasks), 真正等所有完成
    return results                 // ✓ 结果完整
}

// 正解2: 数量确定时, 一次性 Add(更清晰)
wg.Add(len(tasks))                 // 一次登记总数
for i, t := range tasks {
    go func(idx int, task Task) {
        defer wg.Done()
        results[idx] = doWork(task)
    }(i, t)
}
wg.Wait()

// 正解3: 传指针, 别复制 WaitGroup
func worker(wg *sync.WaitGroup) {  // ✓ 传指针; 传值会复制出另一个计数器, 失效
    defer wg.Done()
    ...
}

// 正解4: 配合 errgroup 处理"并发 + 错误收集"(更现代)
import "golang.org/x/sync/errgroup"
func processV2(tasks []Task) error {
    var g errgroup.Group
    for _, t := range tasks {
        t := t                     // 或Go1.22+循环变量已每轮独立
        g.Go(func() error {        // 不用手动Add/Done, 内部管理; 还能收集第一个错误
            return doWork(t)
        })
    }
    return g.Wait()                // 等全部完成, 返回第一个非nil错误
}

// 核心: Add 在 go 语句之前(主协程)、defer wg.Done() 放协程开头、循环变量传参、传指针不复制;
//   并发+错误处理优先用 errgroup, 省去手动 Add/Done 的时序陷阱。

这套正解的关键,是让"登记(Add)"在"等待(Wait)"之前确定完成,不留竞态Add 在 go 之前:在主协程里、启动 goroutine 之前 wg.Add(1),保证 Wait 时计数器已是正确总数——这正是本次我该做的。defer Done 放协程开头:保证每个 goroutine 无论正常结束还是 panic 都会 Done,计数器能归零。数量确定时一次性 Add(len):更清晰。传指针不复制:WaitGroup 不能按值复制。用 errgroup:并发 + 错误收集场景优先用 errgroup,它内部管理计数、还能收集错误,省去手动 Add/Done 的时序陷阱。

第三件事:其他几个"并发时序/协调"的坑

顺着这次 WaitGroup,我把 Go 并发里"时序与协调"相关的几个坑也一并理了:

几个Go并发时序/协调的坑:

坑1: WaitGroup 忘了 Done → Wait 永久阻塞(死锁):
   fatal error: all goroutines are asleep - deadlock!
   正解: 用 defer wg.Done() 放协程开头, 保证一定执行。

坑2: 主协程不等 goroutine 直接退出 → goroutine 还没跑完, main 结束了:
   main里 go func(){...}() 然后main返回 → 程序退出, goroutine被强杀;
   正解: 用 WaitGroup/channel 等它们; main别裸跑goroutine就退。

坑3: 用 channel 同步但没人收 → 发送方阻塞:
   无缓冲channel发送, 没有接收方 → 发送方永久阻塞(同580篇相关);
   正解: 确保收发配对, 或用带缓冲channel, 或select+default。

坑4: 启动了goroutine却没等它初始化完就用它的结果(同上WaitGroup本质):
   "发起"不等于"完成"; 异步发起后, 必须有同步点(Wait/channel)确认完成才能用结果。

坑5: select 没有 default 且所有 case 都阻塞 → 卡死; 或有 default 但空转忙等;
   正解: 想清要不要阻塞; 配合 ctx.Done() 做超时/取消。

坑6: 多个goroutine写同一变量没同步(数据竞争)→ 结果错乱(用 -race 检测);
   正解: 加锁、用channel传递、或各写各的(如按索引写不同位置)。

共同的根: 并发里, "发起一个异步动作" 和 "这个动作真正完成/生效" 之间有时间差;
   任何"依赖它已完成/已登记"的后续动作, 都必须有一个明确的"同步点"来保证时序——
   不能假设"我发起了/我写了这行, 它就已经按我期望的顺序生效了"。

这些坑看似不同,根却是同一个:并发里,"发起一个异步动作"和"这个动作真正完成/生效"之间有时间差、有不确定的时序;任何依赖"它已完成/已登记"的后续动作,都必须有一个明确的"同步点"来保证时序,不能想当然假设"我写在前面它就先执行"认清这个根("发起≠生效,跨协程的时序要靠同步点保证"),才能避开各种并发竞态。

第四件事:WaitGroup 正误对照 / 误用后果——两张表

我把 WaitGroup 的正确与错误写法、以及各种误用的后果,整理成对照表,贴在了团队的 Go 规范里:

写法 对/错 结果
wg.Add(1) 在 go 之前(主协程) ✓ 对 Wait 正确等待全部
wg.Add(1) 在 goroutine 内部 ✗ 错 Wait 竞态,可能提前返回
defer wg.Done() 在协程开头 ✓ 对 保证一定 Done
wg.Done() 写在末尾(可能被 panic 跳过) △ 欠妥 panic 时漏 Done,死锁
wg.Add(len(tasks)) 一次性 ✓ 对 数量确定时清晰
传 wg(值) 给函数 ✗ 错 复制了计数器,失效
传 *wg(指针) ✓ 对 同一个计数器
误用 后果
Add 在 goroutine 内 Wait 提前返回,结果不完整
忘了 Done Wait 永久阻塞,deadlock
Done 多调(超过 Add) panic: negative WaitGroup counter
Add 数 < goroutine 数 少等,部分没等到就返回
Add 数 > goroutine 数 多等,Wait 永久阻塞
复制 WaitGroup 副本独立计数,协调失效
Wait 后立即 Add 复用 可能 panic(WaitGroup reused)

这两张表的核心,第一张是记住黄金写法:Add 在 go 之前、defer Done 在协程开头、传指针;第二张是各种误用都通向"少等(结果残缺)、多等(死锁)、或 panic"——而我栽的"Add 在 goroutine 内"导致的"提前返回结果残缺",恰恰是最隐蔽的(不报错)。记住一条:WaitGroup 用错往往不报错,只是悄悄"没等够"——所以更要严守"Add 先于 Wait"的时序。

第五件事:关于 WaitGroup 的几组容易想当然的认知

这次事故也让我厘清了几组关于 WaitGroup 的、容易想当然的概念:

直觉以为 实际上
Add 写哪都行,反正都会执行 必须在 go 之前,否则 Wait 竞态提前返回
Add 紧挨 Done 写在协程里更清晰 清晰但错,Wait 不等这些 Add
Wait 返回就说明任务都干完了 若 Add 时序错,可能压根没等
没报错就说明 WaitGroup 用对了 Add 在协程内常不报错,只是悄悄少等
WaitGroup 可以随便传值 不能复制,值传递会失效,要传指针
测试通过就没并发时序问题 竞态飘忽,测试(任务少)可能恰好没暴露
go 写在前面就会先执行 go 是异步发起,执行时机由调度器定

这张表里,我栽的是第一行和第二行:觉得"Add 和 Done 写在一起更清晰",把 Add 放进了 goroutine,没意识到主协程的 Wait 根本不等这些还没执行的 Add厘清这些,核心是一个意识:WaitGroup 是个有严格时序要求的协调工具——"登记要等几件事(Add)"必须在"开始等待(Wait)"之前完成;而 goroutine 是异步的,写在前面不代表先执行,所以 Add 必须放在同步的主协程里、go 语句之前。

第六件事:用 WaitGroup / 并发协调时,我现在的自检习惯

现在每当我用 WaitGroup 协调一批 goroutine,我都会先按这张图问自己:

这张图的精髓,是"Add 在 go 之前、defer Done 在开头、传指针、有错误用 errgroup"先看要不要收集错误(要就 errgroup)、再保证Add 在 go 之前defer Done 在协程开头传指针不复制这套习惯,让我从"Add 随手写协程里"变成了"Add 必须先于 Wait 登记好"——核心始终是:WaitGroup 是计数器,Add 必须在启动 goroutine 的 go 语句之前调用,否则 Wait 与 Add 竞态提前返回;defer Done 放协程开头、数量匹配、传指针;并发+错误优先用 errgroup。

我立下的几条规矩

这场"Wait 提前返回、结果残缺"的事故,换来了我写 Go 并发时,刻进骨子里的几条铁律:

  1. WaitGroup 是计数器:Add 加、Done 减、Wait 阻塞到归零。
  2. wg.Add 必须在启动 goroutine 的 go 语句之前(主协程里)调用,否则 Wait 与 Add 竞态、会提前返回。
  3. defer wg.Done() 放在 goroutine 开头,保证哪怕 panic 也会 Done,不留死锁。
  4. Add 的数量要和 goroutine 数严格匹配:少则漏等、多则死锁。
  5. WaitGroup 不能复制(值传递失效),要传指针 *sync.WaitGroup。
  6. WaitGroup 用错常不报错、只是悄悄少等,更要严守 Add 先于 Wait 的时序。
  7. 并发 + 收集错误,优先用 errgroup,省去手动 Add/Done 的时序陷阱。

附:用 -race 和封装防住 WaitGroup 时序坑

借这次的坑,我总结了两条落地手段:一是用 race 检测器跑测试,二是把"并发执行一批任务并等待"封装成一个不会用错的工具函数。

// 1. 用 -race 跑测试, 能发现很多并发时序/数据竞争问题
//    go test -race ./...
//    go run -race main.go
//    (它能检出数据竞争; 配合压力测试能放大WaitGroup这类时序问题的暴露概率)

// 2. 封装一个"并发执行并等待全部完成"的工具, 把正确时序固化进去
func RunAll[T any, R any](items []T, fn func(T) R) []R {
    var wg sync.WaitGroup
    results := make([]R, len(items))
    for i, item := range items {
        wg.Add(1)                      // ✓ Add 固定在 go 之前, 调用方无从写错
        go func(idx int, it T) {
            defer wg.Done()            // ✓ defer Done 固定在开头
            results[idx] = fn(it)      // 各写各的索引, 无需锁
        }(i, item)
    }
    wg.Wait()
    return results
}

// 调用方只管业务逻辑, 不碰 Add/Done/Wait, 也就不会把时序写错:
results := RunAll(tasks, func(t Task) Result {
    return doWork(t)
})

// 原则: 把"容易写错时序"的并发原语(WaitGroup的Add/Done/Wait)封装进一个正确的工具,
//   让调用方只写业务、碰不到时序细节——比依赖每个人都记得"Add在go之前"可靠得多;
//   再用 -race 做自动化检测兜底。

这两条手段一个""一个"":封装把"Add 在 go 之前、defer Done"的正确时序固化进工具,让调用方根本碰不到、也就写不错;-race 检测器则在测试时主动放大并发问题、把潜伏的时序竞态揪出来并发的坑往往飘忽难复现,所以"把正确做法封装成默认"加"用工具自动检测",远比"靠人记住规则"可靠。

写在最后

回头看,这场由"把 wg.Add 放在了 goroutine 内部"引发的、Wait 提前返回的事故,真正教给我的,远不止"Add 要写在 go 之前"这一个技巧。它让我对"并发、异步的世界里, '代码写在前面' 完全不等于 '它就先执行'; 而很多协调机制能正确工作, 恰恰依赖一个严格的'先后时序'——'登记我要做什么' 必须先于 '检查是不是都做完了', 一旦这个时序被打乱, 协调就失效了",有了一次刻骨的体会。我栽跟头,是因为我把同步世界的直觉(写在前面的先执行)带进了异步并发的世界——我以为"Add 写在 Done 旁边, 反正都在任务里, 顺理成章";可我忽略了: go 语句一发出, goroutine 的执行就脱离了我代码的书写顺序, 交给了调度器;于是"主协程的 Wait" 和 "子协程里的 Add" 成了一场赛跑, 而 Wait 常常跑赢——它在"该等的事还没登记"时, 就看着空计数器宣布"都做完了";问题的本质, 是我没保证"登记(Add)"切实发生在"等待(Wait)"之前这让我领悟到一个关于"时序与协调"的深刻认知:任何"协调多方"的机制(等待、汇合、计数、确认),其正确性都建立在一个隐含的时序前提上:"建立协调关系的动作(登记、注册、占位、声明)" 必须切实发生在 "依赖这个关系的动作(等待、检查、汇总)" 之前;一旦你把"登记"放到了"等待"之后(或与之竞态), 等待方就会基于一个"还不完整的登记"做出判断, 得出"虚假的完成";这在并发编程里叫"happens-before"(先行发生)——而它远不止于代码: 开会前要先确认到齐几人、统计前要先登记完名单、验收前要先明确验收项; 凡是"等所有相关方就位再行动"的事, 都要先确保"就位的登记"完成于"行动的判断"之前这给了我一种处理"协调与等待"时的清醒:每当我要"等一组事情都完成再继续"时,要确保"我要等哪些事(登记)"是在"开始等待之前"就确定、登记完毕的,而不是边等边登记——尤其在异步并发下, 别假设"写在前面的登记就先于等待执行", 要用明确的同步点(把 Add 放在 go 之前)来保证这个时序;"让'建立协调关系'切实先行于'依赖该关系的判断'",是一切等待/汇合/计数类协调能正确工作的根基认清异步下代码顺序不等于执行顺序、协调机制依赖严格的先后时序、登记必须先于等待——这,是我用一次 WaitGroup 时序用错的事故,换来的、关于 Go 并发、也关于如何保证协调之时序的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写 WaitGroup 时,把那行 wg.Add(1) 稳稳地放在 go 语句的前面,那我对着那个"没报错却结果残缺"的输出排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我图省事调了个异步函数发通知,既没 await 也没 catch,想着失败了无所谓,结果它一旦 reject 整个 Node 进程就因为未处理的 Promise 拒绝直接崩溃退出:一次 unhandled rejection 拖垮服务的深度复盘

2026-6-3 0:36:59

技术教程

我在 Java 里用 Arrays.asList 把数组转成 List,以为得到了一个普通的可增删的列表,结果往里 add 一个元素就抛 UnsupportedOperationException,因为它根本不是 ArrayList 而是个固定大小的数组视图的深度复盘

2026-6-3 0:48:16

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索