我在 for 循环里起了一堆 goroutine 处理每个元素,结果它们全在处理同一个、还是最后一个,我对着循环变量被闭包捕获排查了大半天的复盘

用 Go 写批处理,想为切片每个元素并发起一个 goroutine,经典的 for + go 写法,行云流水。结果目瞪口呆:10 个元素起了 10 个 goroutine,处理的却全是同一个、还是最后一个,前 9 个一个没处理。盯着代码反复看,循环明明遍历了每个元素,凭什么 goroutine 全拿到最后一个?排查大半天才理解 Go 1.22 之前坑了无数人的经典陷阱:for 循环变量被闭包捕获。根因是三重机制叠加:循环变量 item 整个循环复用同一个、每轮只是重新赋值;闭包捕获的是变量本身(引用)不是当时的值;而 goroutine 是异步的不立刻执行,等它真正跑时主循环早结束、item 已停在最后值 e,于是所有 goroutine 都读到 e。这篇从循环变量复用+闭包捕获引用+异步执行的三重成因、影子变量 item:=item / go func(x){}(item) 传参 / 升级 Go1.22 的正解、循环变量设计为何这样又为何 1.22 改了、闭包捕获的各种案发现场(go/defer/存指针/存闭包/回调)、值拷贝 vs 引用的辨析、决策图与铁律,到附上一段并排对比踩坑与修复输出的可运行代码。核心领悟:并发世界里代码写下的顺序和实际执行的时刻是分离的,稍后才执行的闭包引用此刻还在变的状态就是灾难;要为每个异步任务用传值冻结住它执行时所需的状态,而不是引用一个活的会变的共享变量。

我在 for 循环里起了一堆 goroutine 处理每个元素,结果它们全在处理同一个、还是最后一个,我对着循环变量被闭包捕获排查了大半天的复盘

那是我用 Go 写的一个批处理任务。我有一个切片,想为每个元素并发起一个 goroutine 去处理,经典的"for 循环 + go"写法,我写得行云流水。可跑出来的结果让我目瞪口呆:我有 10 个元素,起了 10 个 goroutine,但它们处理的居然全是同一个元素——而且是切片里的最后一个。前面 9 个元素,一个都没被处理,最后一个却被处理了好几遍。我盯着代码反复看:循环明明老老实实遍历了每个元素,凭什么 goroutine 拿到的全是最后一个?我甚至怀疑是切片本身的问题。排查了大半天,我才真正理解了 Go(1.22 之前)那个坑了无数人的经典陷阱:for 循环变量被闭包捕获。这篇就把这场"goroutine 全在处理最后一个"的事故,从头复盘一遍。

故障现场:10 个 goroutine,处理的全是最后一个

先看现场。问题就藏在那段"每个 goroutine 引用了循环变量"的代码里:

package main

import (
    "fmt"
    "sync"
)

func main() {
    items := []string{"a", "b", "c", "d", "e"}
    var wg sync.WaitGroup

    // ✗ 经典错误写法(Go 1.22 之前)
    for _, item := range items {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println("处理:", item)   // ← 这里引用了循环变量 item
        }()
    }
    wg.Wait()

    // 期望输出(顺序不定): 处理: a / 处理: b / 处理: c / 处理: d / 处理: e
    // 实际输出(Go 1.22前)可能是:
    //   处理: e
    //   处理: e
    //   处理: e
    //   处理: e
    //   处理: e
    //   ↑ 5个goroutine, 处理的全是最后一个 "e" !

    // 现象拼图(Go 1.22 之前):
    //   - for range 循环里, item 是【同一个变量】, 每次迭代只是给它【重新赋值】,
    //     而不是每次都创建一个新的 item。
    //   - goroutine 里的闭包, 捕获的是【item 这个变量本身】(引用), 不是
    //     "当时那一刻 item 的值"。
    //   - 关键: go func(){...}() 是【异步】的, 它不会立刻执行!
    //   - 等这些 goroutine 真正开始跑时, for 循环大概率早已结束,
    //     此时 item 的值停在了【最后一次赋值】, 即 "e"。
    //   - 于是所有 goroutine 读到的 item 都是 "e"。
    //   - ★ 我以为每个 goroutine 捕获了"它那次循环的 item 值",
    //     实际它们捕获的是"同一个 item 变量", 而这变量最后变成了 "e"。
}

看清真相后,我恍然大悟。问题的根源,是 Go 1.22 之前一个反直觉的规则:for ... range 循环里,item同一个变量,每次迭代只是给它重新赋值,而不是每次都创建一个新的 item而 goroutine 里的闭包,捕获的是 item 这个变量本身(引用),不是"当时那一刻 item 的值"更关键的是,go func(){}() 是异步的,它不会立刻执行;等这些 goroutine 真正开始跑时,for 循环大概率早已结束,此时 item 的值停在了最后一次赋值,即 "e"。于是所有 goroutine 读到的 item 都是 "e"。我以为每个 goroutine 捕获了"它那次循环的值",实际它们捕获的是"同一个变量",而这个变量最后变成了 "e"

第一件事:搞懂循环变量、闭包捕获与异步执行的三重作用

要解决它,得先搞懂这个 bug 背后三个机制是如何叠加成灾的。

"goroutine 全处理最后一个" 的三重成因

# 成因1: 循环变量是"复用"的(Go 1.22 之前)
#   for _, item := range items { ... }
#   - item 只在循环开始时声明【一次】, 之后每轮迭代是给它【重新赋值】。
#   - 整个循环, item 自始至终是【同一个变量】(同一个内存地址)。
#   - 循环结束后, item 的值 = 最后一次迭代的值。

# 成因2: 闭包捕获的是"变量", 不是"值"
#   go func() { use(item) }()
#   - Go 的闭包捕获的是【变量本身(引用)】, 不是"捕获那一刻的值"。
#   - 所以闭包里的 item, 永远指向那个【唯一的、会变的】item 变量。
#   - 当闭包真正执行时, 它读到的是 item 【当时】的值。

# 成因3: goroutine 是"异步"的(延迟执行)
#   - go func(){}() 只是"启动"一个goroutine, 不阻塞、不立刻执行。
#   - 主循环会飞快地把5个goroutine都启动完, 循环就结束了。
#   - 等goroutine的调度真正轮到、开始执行时, item 早已是最后的值 "e"。

# 三者叠加 = 灾难:
#   复用的变量(只有一个item) + 闭包捕获引用(都指向这个item)
#   + 异步执行(执行时循环已结束、item已是最后值)
#   = 所有goroutine都读到最后一个值。

# ★ 同样的坑也出现在(只要"闭包捕获循环变量 + 延迟执行"):
#   - defer func(){ use(item) }()  在循环里
#   - 把 &item 存进 slice/map (存的都是同一个地址!)

# Go 1.22 的修复:
#   从 Go 1.22 起, for 循环【每次迭代都创建新的循环变量】!
#   → 上面的代码在 Go 1.22+ 直接就对了(每个goroutine捕获各自的item)。
#   但: 很多项目还在用旧版本; 且理解这个机制对写闭包至关重要。

# 核心: 旧版Go循环变量复用(同一个) + 闭包捕获变量引用 + goroutine异步执行,
#   三者叠加致所有goroutine读到最后值; Go1.22起每轮新建变量已修复, 但需懂原理。

原来,这个 bug 是三个机制叠加的结果。成因一:循环变量是"复用"的(Go 1.22 前)——item 只声明一次,每轮迭代是给它重新赋值,整个循环它都是同一个变量,循环结束后值停在最后一次。成因二:闭包捕获的是"变量"不是"值"——Go 闭包捕获的是变量本身(引用),所以闭包里的 item 永远指向那个唯一的、会变的变量,执行时读到它当时的值。成因三:goroutine 是"异步"的——go func(){}() 只是启动不立刻执行,主循环飞快启动完所有 goroutine 就结束了,等 goroutine 真正执行时 item 早已是最后的 "e"三者叠加:复用的变量 + 闭包捕获引用 + 异步执行 = 所有 goroutine 都读到最后一个值同样的坑也出现在循环里的 defer func(){ use(item) }()、把 &item 存进 slice/map(存的都是同一个地址)。好消息是:Go 1.22 起,for 循环每次迭代都创建新的循环变量,这个坑被官方修复了;但很多项目还在用旧版,且理解这个机制对写闭包至关重要。

第二件事:正解——给每次迭代一个独立的变量副本

搞懂了原理,正解就清晰了:让每个 goroutine 捕获"它那次迭代的独立副本",而不是共享同一个循环变量

// ====== 正解一(经典): 循环内 "影子变量", 创建本次迭代的副本 ======
for _, item := range items {
    item := item   // ← 关键! 在循环体内重新声明一个同名局部变量(本次迭代独有)
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("处理:", item)   // ✓ 捕获的是本次迭代的副本, 各不相同
    }()
}
// → item := item 看着怪, 但它在每次迭代里都创建了一个【新的局部 item】,
//   闭包捕获的是这个新变量, 所以每个goroutine拿到的是各自那次的值。

// ====== 正解二(推荐): 用参数传值, 把当前值"拷贝"进 goroutine ======
for _, item := range items {
    wg.Add(1)
    go func(it string) {        // ← 通过参数接收
        defer wg.Done()
        fmt.Println("处理:", it) // ✓ it 是调用时传入的值的副本, 不受循环变量变化影响
    }(item)                      // ← 启动时就把当前 item 的【值】传进去
}
// → go func(it string){...}(item): 启动goroutine的【那一刻】, 就把item当前的值
//   作为参数拷贝进去了, 之后item怎么变都和这个it无关。最清晰、最推荐。

// ====== 正解三: 索引也一样要注意 ======
for i := range items {
    i := i                       // 或 go func(idx int){...}(i)
    go func() { use(i) }()       // ✓ 同样需要副本, 否则i也是共享的
}

// ====== 正解四: 升级到 Go 1.22+ , 语言层面已修复 ======
// go.mod 里 go 1.22 起, 循环变量每轮迭代自动是新的, 上面的"裸写法"直接就对:
for _, item := range items {
    go func() { fmt.Println(item) }()   // Go 1.22+ ✓ (每轮item是新变量)
}
// 但: 跨版本协作/老项目, 别依赖这个; 显式传值(正解二)在任何版本都对、最稳。

// ====== 正解五: 同样的修法适用于 defer 和 存指针 ======
for _, item := range items {
    item := item
    defer fmt.Println(item)          // ✓ 副本, defer时各是各的
    ptrs = append(ptrs, &item)       // ✓ 每个&item是不同的地址(各副本)
}

// 核心: 给每次迭代独立副本 —— 循环内 item:=item 影子变量, 或 go func(x){}(item)
//   传值(最推荐, 任何版本都对); Go1.22+已语言级修复但老项目仍需显式; defer/存指针同理。

修复的核心,是"让每个 goroutine 捕获它那次迭代的独立副本,而不是共享同一个循环变量"正解一(经典):循环内"影子变量"——在循环体里写 item := item,看着怪,但它每次迭代都创建一个新的局部 item,闭包捕获这个新变量,于是各 goroutine 拿到各自的值。正解二(推荐):用参数传值——go func(it string){...}(item),启动 goroutine 的那一刻就把 item 当前的值作为参数拷贝进去,之后 item 怎么变都和 it 无关,最清晰最推荐。正解三:索引同理(i := i 或传参)。正解四:升级到 Go 1.22+——语言层面已修复(每轮循环变量自动是新的),但跨版本协作/老项目别依赖它,显式传值在任何版本都对、最稳正解五:defer 和存指针同样适用这个修法归根结底:给每次迭代独立副本——循环内 item := item 影子变量,或 go func(x){}(item) 传值(最推荐、任何版本都对);Go 1.22+ 已语言级修复但老项目仍需显式。

第三件事:为什么 Go 要这么设计、又为什么改了

排查后我去深究了这个设计的来龙去脉,理解它"为什么曾经这样、后来又改了",收获很大。

循环变量设计的演变

# Go 1.22 之前: 循环变量"每个循环只声明一次, 反复复用"
#   for i, v := range s { ... }  →  i 和 v 整个循环就一份。
#
#   当初为什么这么设计?
#     - 性能/简洁: 复用一个变量, 不用每轮分配新变量, 看似高效。
#     - 符合C系语言的传统直觉(C/Java的for也是复用变量)。
#
#   但它造成的问题:
#     - 闭包捕获循环变量 = 几乎总是 bug(尤其配合goroutine/defer)。
#     - 这是Go官方统计中【最常见的Go编程错误之一】, 坑了无数人,
#       连Go核心团队都承认这是个"设计失误"。

# Go 1.22 (2024): 改为"每次迭代都创建新的循环变量"
#   - 每轮迭代, i 和 v 都是【全新的变量】。
#   - 闭包捕获它, 自然就是捕获"本轮的那个", 符合直觉了。
#   - 代价: 极小的性能开销(通常可忽略, 且编译器会优化没被捕获的情况)。
#   - 这是Go少有的"破坏了一点点向后兼容"的改动, 但因为旧行为
#     "几乎总是错的", 所以改了利大于弊。

# 启示:
#   - 即便是顶级语言团队, 早期的设计决策也可能是错的。
#   - 一个"看起来高效/简洁"的设计, 如果违反使用者的直觉、
#     频繁导致bug, 那它就是个坏设计 —— 直觉的代价, 是真实的bug。
#   - Go 1.22 用"打破一点兼容性"换"消除一类高频bug", 是务实的取舍。

# 核心: 旧Go复用循环变量(图省事/随C传统)导致闭包捕获几乎必bug, 是公认设计失误;
#   Go1.22改为每轮新建变量修复之, 体现"违反直觉、频繁致错的设计就是坏设计"。

深究这个设计的演变,让我收获的不只是知识,还有对"设计决策"的思考。Go 1.22 之前,循环变量"每个循环只声明一次、反复复用"——当初这么设计,是出于性能/简洁的考虑(复用变量不用每轮分配)和对 C 系语言传统的沿袭但它造成了严重问题:闭包捕获循环变量几乎总是 bug(尤其配合 goroutine/defer),这是 Go 官方统计中最常见的编程错误之一,连核心团队都承认是"设计失误"于是Go 1.22(2024)改为"每次迭代都创建新的循环变量"——闭包捕获它自然就是本轮的那个,符合直觉了,代价是极小的性能开销。这是 Go 少有的、打破一点向后兼容的改动,但因为旧行为"几乎总是错的",所以利大于弊这件事给我的启示很深:即便是顶级语言团队,早期的设计决策也可能是错的;一个"看起来高效简洁"的设计,如果违反使用者直觉、频繁导致 bug,那它就是个坏设计——直觉的代价,是真实的 bug下面这张图,是这次 goroutine 全处理最后一个的成因与解法:

第四件事:闭包捕获循环变量的各种"案发现场"速查

这次踩坑后,我把"闭包捕获循环变量"会出问题的各种场景整理成一张表,以后写循环里的闭包就警觉。

场景 Go 1.22前的问题 修法
循环里 go func(){use(v)} 所有goroutine读到最后值 传参 go func(x){}(v) 或 v:=v
循环里 defer func(){use(v)} 所有defer用最后值 v:=v 或 defer f(v) 传参
append(slice, &v) 所有指针指向同一地址(最后值) v:=v 再取地址
把闭包存进slice/map 存的闭包都引用同一个v v:=v 或工厂函数传参
循环里注册回调/handler 回调触发时都用最后值 v:=v 或传参

这张表,把"闭包捕获循环变量"的各种翻车现场都列了出来。它们的共同特征是:一个在"循环里创建"、却在"循环结束后才执行"(或被多次执行)的闭包,引用了循环变量无论是 goroutine、defer、存指针、存闭包、注册回调,本质都一样:闭包"延迟"到循环结束后才读那个变量,而那时变量早已是最后的值它给我的启发是:判断"会不会踩这个坑",关键看两点:一是"闭包有没有捕获循环变量",二是"闭包是不是延迟/异步/多次执行的"——两者同时满足,就危险反过来,如果闭包在循环内就立刻同步执行完了(比如 for v := range s { f(func(){ use(v) }) }f 立刻调用了闭包),那其实没问题(因为执行时 v 还是当前值)。所以真正的风险点,是"捕获"和"延迟执行"的组合——记住这个组合,就能在写任何"循环里的闭包"时,精准地判断要不要做副本。

第五件事:值拷贝 vs 引用,这对概念贯穿始终

这次的坑,本质是"引用了一个会变的东西"。我把"值拷贝 vs 引用"这对贯穿编程的概念,借此梳理了一遍。

方式 捕获/传递的是 后续原变量变化 本文场景
闭包捕获变量 变量本身(引用) 会跟着变(危险) goroutine读到最后值
函数参数传值 当时值的副本 不受影响(安全) go func(x){}(v) 修法
v := v 影子变量 新建副本变量 各自独立(安全) 每轮一个独立v
取地址 &v 变量的地址 指向同一个会变的(危险) 所有指针指向最后值

这张表,把这次 bug 的本质——"捕获引用 vs 拷贝值"——讲透了。核心区别是:闭包捕获变量、取地址 &v,拿到的是"会变的那个东西"(引用/地址),原变量一变它就跟着变(危险);而函数传参、v := v 影子变量,拿到的是"当时那一刻的副本",原变量怎么变都和它无关(安全)它给我的最大启发,超出了这个具体的 bug:"值拷贝"和"引用"的区别,是贯穿几乎所有编程语言的核心概念之一;无数 bug 的根源,都是"我以为我拿到的是一份独立的副本,实际我拿到的是一个共享的、会变的引用"(或反之)这次的循环变量、之前文章里的 Java 自动拆箱、Python 可变默认参数、切片共享底层数组……它们看似在不同语言、不同场景,根子上却往往都连着同一个问题:"这个东西,是值还是引用?是独立的还是共享的?它会不会在我背后被改变?"所以,每当我处理一个变量、尤其是要把它"传递、存储、延迟使用"时,我都会下意识地问:"我拿的是它的副本,还是它本身?如果是本身,它会变吗?"——想清楚这个,能避开一大类最隐蔽、最难查的 bug。

第六件事:在循环里写闭包时,我现在的判断习惯

现在每当我在循环里写闭包(尤其要 go/defer/存起来),我都会先过一遍这张图:

这张图的精髓,是"在循环里写闭包,先判断'捕获循环变量 + 延迟执行'这个危险组合"两个判断:一问"闭包捕获了循环变量吗"(没有就安全),二问"闭包是延迟/异步/多次执行的吗"(go/defer/存起来/回调就是);两者同时满足才危险。危险时的修法,最推荐传参(go func(x){}(v))、也可用影子变量(v := v);Go 1.22+ 已语言级修复,但老项目仍显式更稳最后用go vet / 静态检查兜底(它能检测出这类循环变量捕获问题)。这套习惯,让我写循环里的闭包时,从"行云流水然后被诡异结果教训"变成了"先判断危险组合"——核心始终是:循环里的闭包,只要"捕获了循环变量"且"延迟执行",就必须给它独立副本。

我立下的几条规矩

这场"goroutine 全处理最后一个"的事故,换来了我写 Go(以及一切语言的循环闭包)时,刻进骨子里的几条铁律:

  1. 循环里起 goroutine/defer,别直接捕获循环变量。Go 1.22 前会全读到最后值。
  2. 给每次迭代独立副本。最推荐 go func(x){}(v) 传参,或循环内 v := v 影子变量。
  3. 存指针/存闭包同理。append(s, &v) 前先 v := v,否则所有指针指向同一个。
  4. 警惕"捕获 + 延迟执行"这个危险组合。两者同时满足才会踩坑,记住这个判据。
  5. 分清捕获引用与拷贝值。闭包捕获/取地址是引用(会变),传参/影子变量是副本(独立)。
  6. Go 1.22+ 已修复但别盲目依赖。跨版本/老项目显式传值在任何版本都对。
  7. 用 go vet 兜底。它能静态检测出循环变量被闭包捕获的隐患。

附:一段能亲眼对比"踩坑 vs 修复"的可运行代码

口说无凭。下面这段完整代码,把错误写法和三种正确写法放在一起跑,让你亲眼看见输出的天壤之别:

package main

import (
    "fmt"
    "sort"
    "sync"
)

func collect(run func(out *[]string, wg *sync.WaitGroup)) []string {
    var wg sync.WaitGroup
    var out []string
    var mu sync.Mutex
    _ = mu
    run(&out, &wg)
    wg.Wait()
    sort.Strings(out)
    return out
}

func main() {
    items := []string{"a", "b", "c", "d", "e"}

    // ====== ✗ 错误写法: 闭包直接捕获循环变量(Go 1.22前) ======
    var wg1 sync.WaitGroup
    var bad []string
    var mu1 sync.Mutex
    for _, item := range items {
        wg1.Add(1)
        go func() {
            defer wg1.Done()
            mu1.Lock(); bad = append(bad, item); mu1.Unlock()  // 捕获item引用
        }()
    }
    wg1.Wait()
    sort.Strings(bad)
    fmt.Println("错误写法结果:", bad)
    //   Go1.22前输出: [e e e e e]  ← 全是最后一个!

    // ====== ✓ 正确写法一: 传参冻结值 ======
    var wg2 sync.WaitGroup
    var good1 []string
    var mu2 sync.Mutex
    for _, item := range items {
        wg2.Add(1)
        go func(it string) {        // 传参
            defer wg2.Done()
            mu2.Lock(); good1 = append(good1, it); mu2.Unlock()
        }(item)                     // 启动时拷贝当前值
    }
    wg2.Wait()
    sort.Strings(good1)
    fmt.Println("传参写法结果:", good1)
    //   输出: [a b c d e]  ✓ 每个都对!

    // ====== ✓ 正确写法二: 影子变量 ======
    var wg3 sync.WaitGroup
    var good2 []string
    var mu3 sync.Mutex
    for _, item := range items {
        item := item                // 影子变量, 本轮独立副本
        wg3.Add(1)
        go func() {
            defer wg3.Done()
            mu3.Lock(); good2 = append(good2, item); mu3.Unlock()
        }()
    }
    wg3.Wait()
    sort.Strings(good2)
    fmt.Println("影子变量结果:", good2)
    //   输出: [a b c d e]  ✓

    _ = collect
}

/* 在 Go 1.21 及更早版本运行, 输出对比鲜明:
   错误写法结果: [e e e e e]      ← 灾难现场!
   传参写法结果: [a b c d e]      ← 修复 ✓
   影子变量结果: [a b c d e]      ← 修复 ✓
   (Go 1.22+ 三个都是 [a b c d e], 因为语言已修复)
*/

// 核心: 同样的并发逻辑, 仅"是否给每轮独立副本"的差别, 错误写法全是最后值、
//   正确写法各是各的; 在Go1.21跑一遍, 那行[e e e e e]会让你永远记住这个坑。

这段可运行的对比代码,把"踩坑"和"修复"放在了同一个屏幕上,让差异一目了然。同样是并发处理 5 个元素、同样的逻辑骨架,仅仅是"是否给每轮一个独立副本"的差别:错误写法(直接捕获循环变量)在 Go 1.21 跑出来是刺眼的 [e e e e e](全是最后一个),而传参和影子变量两种正确写法,跑出来都是正确的 [a b c d e]这,正是我想用这段代码,留给每个 Go 开发者的最后一课:对于"踩坑写法和正确写法,在代码上只差一点点,结果却天差地别"的陷阱,最好的学习方式,就是把它们并排放在一起、亲手跑一遍、亲眼对比输出当你亲眼看见那行 [e e e e e],再对比旁边正确的 [a b c d e],这个坑就会以一种近乎肌肉记忆的方式,刻进你的脑子里。知道一个坑"存在",和亲眼见过它"发生"、并亲手对比过"错与对",是两种深度完全不同的掌握而对于那些"差之毫厘、谬以千里"的细节陷阱,后一种掌握,才是真正能在未来某个深夜、某个紧急关头,帮你一眼认出它、并绕开它的、靠得住的本能。把每一个值得记住的坑,都亲手"跑成对比实验"——这是我从这次事故里,带走的最实用的学习方法。

写在最后

回头看,这场由"循环变量被闭包捕获"引发的、goroutine 集体处理最后一个的事故,真正教给我的,远不止"记得 v := v"这一个技巧。它让我对"并发"和"闭包"这两个本就烧脑的概念叠加在一起时的复杂性,有了切肤的敬畏。我栽跟头,是因为我脑子里那个"同步、顺序执行"的直觉,在面对"异步的 goroutine"时彻底失灵了。我下意识地以为 go func(){ use(item) }() 会"在这一轮循环里,带着这一轮的 item"去执行;可现实是,它只是"登记"了一个稍后才跑的任务,而等它真正跑起来时,那个被它引用的 item,早已不是它"出生时"的样子了这让我领悟到一个关于并发编程的深刻道理:在并发的世界里,"代码写下的顺序"和"代码实际执行的时刻"是分离;而一旦一段"稍后才执行的代码"(闭包/goroutine)引用了一个"此刻还在变化的状态"(循环变量),你就必须极其小心地问:"等它真正执行时,它引用的那个状态,会是什么样?"同步编程里,我们习惯了"读到代码的这一行,状态就是此刻的样子";但并发打破了这个假设——你登记的任务,是在一个"未来的、你无法精确预知的时刻"执行的,那时的世界(共享状态)可能早已天翻地覆所以,写并发代码的核心心法之一,就是为每个异步任务,"冻结"住它执行时所需要的状态(通过传值拷贝),而不是让它去引用一个"活的、会变的"共享变量这,是我用一次"goroutine 全处理最后一个"的事故,换来的、关于 Go、也关于"并发与状态"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次在循环里写下 go func 时,条件反射地把循环变量"传值冻结"一下,那我对着那一堆"处理: e"熬的这大半天,就值了。

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

我把对象的方法直接传给 setTimeout 当回调,运行到一半就报 this 是 undefined,我对着 JavaScript 的 this 指向丢失排查了大半天的复盘

2026-6-2 6:16:13

技术教程

我在 for-each 遍历 List 的时候顺手删了几个元素,线上偶发抛 ConcurrentModificationException,明明是单线程、根本没有并发,我对着这个异常排查了大半天的复盘

2026-6-2 6:26:26

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