
go 中对切片进行 range 遍历时,循环变量复用会导致所有指针指向同一内存地址,引发递归中存储的结构体指针内容异常变更。
go 中对切片进行 range 遍历时,循环变量复用会导致所有指针指向同一内存地址,引发递归中存储的结构体指针内容异常变更。
在使用 Go 实现 HTML 树结构递归遍历时,一个常见但隐蔽的陷阱是:向切片中追加结构体指针后,其值在后续递归调用中“意外改变”。问题根源并非指针本身被修改,而是range 循环变量 tag 被重复赋值,导致所有 &tag 实际指向同一个栈地址。
考虑如下典型错误代码:
for _, tag := range htmlTag.ChildTags {
getAllCertainDivs(className, idName, &tag, matchingDivs) // ❌ 危险!所有 &tag 指向同一变量
}
尽管 htmlTag.ChildTags 包含多个独立的 HtmlTag 值,但 range 语句中的 tag 是单个变量,每次迭代仅复制对应元素值到该变量中。因此 &tag 始终取的是这个复用变量的地址,而非各元素在原切片中的真实地址。当递归深入、函数栈展开时,该变量内容不断被覆盖,导致之前存入 *matchingDivs 的指针所指向的内存内容发生不可预期变更——表现为“追加后值就变了”。
✅ 正确解法有两种(推荐第一种):
方案一:显式声明新变量(清晰、安全、符合直觉)
在循环体内重新声明同名变量,触发新变量分配:
for _, tag := range htmlTag.ChildTags {
tag := tag // ✅ 创建新变量,获得独立地址
getAllCertainDivs(className, idName, &tag, matchingDivs)
}
此写法利用了 Go 的短变量声明特性,在每次迭代中为 tag 分配新的栈空间,确保 &tag 指向唯一且稳定的地址。
方案二:通过索引直接取址(零额外开销)
绕过循环变量,直接对原切片索引取地址:
for i := range htmlTag.ChildTags {
getAllCertainDivs(className, idName, &htmlTag.ChildTags[i], matchingDivs) // ✅ 安全
}
该方式无需额外变量,直接获取切片底层数组中第 i 个元素的真实地址,语义明确且性能最优。
⚠️ 注意事项:
- 此问题与闭包捕获循环变量、goroutine 异步执行等场景本质相同,均源于 range 变量复用机制;
- 若 HtmlTag 较大,按值传递 tag 再取地址虽安全,但有拷贝开销;若需极致性能且确定切片生命周期足够长,优先采用索引方案;
- 切勿依赖 &htmlTag.ChildTags[i] 在切片扩容后仍有效——本例中因递归只读不扩容,故安全;若涉及动态增删子节点,应改用索引+深度克隆或重构为基于索引的遍历逻辑。
总结:Go 的 range 是语法糖,不是“为每个元素创建独立变量”,理解其底层行为是写出健壮递归/并发代码的关键一步。











