
本文详解 Go 并发编程中因 for 循环变量复用导致的指针误引用问题:当在循环中取 &category 发送给 goroutine 时,所有 goroutine 实际共享同一内存地址,造成数据覆盖和逻辑错乱。
本文详解 go 并发编程中因 for 循环变量复用导致的指针误引用问题:当在循环中取 `&category` 发送给 goroutine 时,所有 goroutine 实际共享同一内存地址,造成数据覆盖和逻辑错乱。
在 Go 的并发实践中,一个隐蔽却高频踩坑的问题是:在 for range 循环中取循环变量的地址(&v)并传递给 goroutine 或 channel,会导致所有 goroutine 操作同一份内存。您提供的网络爬虫代码正是这一经典陷阱的典型体现。
问题核心在于这段代码:
for _, category := range *categories {
numRequests += 1
downloadChannel <p>虽然 <code>range</code> 每次迭代看似为 <code>category</code> 创建了新副本,但 Go 编译器会<strong>复用同一个栈变量 <code>category</code> 的内存地址</strong>。因此,<code>&category</code> 始终返回相同指针值,后续 goroutine 从 channel 读取时拿到的其实是已被覆盖的“最新”值——这正是您观察到 <code>parent</code> 字段被意外篡改、URL 拼接错乱的根本原因。并发 worker 越多,竞争越激烈,现象越明显。</p><p>✅ 正确解法是确保每个结构体实例拥有独立内存地址。推荐两种安全方式:</p><p><strong>方式一:显式索引遍历(推荐)</strong></p><pre class="brush:php;toolbar:false;">cats := *categories
for i := range cats {
numRequests++
downloadChannel <p><strong>方式二:在循环内创建局部副本</strong></p><pre class="brush:php;toolbar:false;">for _, category := range *categories {
numRequests++
c := category // 显式拷贝值
downloadChannel <blockquote><p>? 提示:使用 <code>go vet</code> 工具可自动检测此类问题。运行 <code>go vet ./...</code> 会提示:<code>loop variable category captured by func literal</code>(若该变量用于闭包)或类似警告——尽管本例中是 channel 传递而非闭包,但原理相通,建议开启 <code>go vet -shadow</code> 等增强检查。</p></blockquote><p>此外,您的代码还存在其他可优化点:</p>
-
allCategories = append(allCategories, *categories...)应注意*categories是切片解包,但categories本身是*[]Category类型,需确认extractCategories返回值是否已正确解引用; -
close(downloadChannel)和close(resultsChannel)的关闭时机有竞态风险:多个 worker 可能同时尝试关闭已关闭的 channel,引发 panic。建议改用sync.WaitGroup或通过主 goroutine 统一控制 channel 生命周期; -
log.Fatal在 worker 中会终止整个程序,应改为错误返回或 channel 通知主 goroutine 处理。
总结:Go 中「值语义」与「地址语义」的边界必须清晰。循环变量是栈上复用的临时容器,其地址不具备唯一性;而切片元素、显式局部变量或堆分配对象(如 new(Category))才具备稳定地址。掌握这一底层机制,是写出健壮并发 Go 代码的关键基础。










