本文详解 Go 语言中 for range 循环内对循环变量取地址导致指针意外共享同一内存地址的问题,阐明根本原因,并提供安全、高效获取切片元素指针的正确实践。
本文详解 go 语言中 `for range` 循环内对循环变量取地址导致指针意外共享同一内存地址的问题,阐明根本原因,并提供安全、高效获取切片元素指针的正确实践。
在 Go 中,for range 循环看似简洁,但若不理解其底层机制,极易引发隐蔽的指针错误。典型问题如以下代码所示:
package main
import "fmt"
type t struct {
val int
}
func main() {
l := []t{{1}, {2}}
var p *t
for _, i := range l {
fmt.Println("begin", p)
p = &i // ⚠️ 危险:取的是循环变量 i 的地址,而非切片元素地址
fmt.Println("end", p)
}
}
运行输出为:
begin <nil>
end &{1}
begin &{2}
end &{2}</nil>
而非预期的 &{1} → &{2} 的逐步切换。根本原因在于:i 是每次迭代中被重新赋值的(复用)局部变量,其内存地址始终不变;&i 始终指向该固定栈地址,而该地址在循环中不断被新值覆盖。因此,所有指针最终都指向最后一次迭代写入的值(即 {2}),造成“指针漂移”。
✅ 正确做法是:直接取切片元素的地址,而非循环变量的地址。推荐两种安全方案:
方案一:使用索引遍历(最推荐)
for i := range l {
if i > 0 {
prev := &l[i-1] // 安全:指向切片第 i-1 个元素的真实地址
curr := &l[i] // 安全:指向切片第 i 个元素的真实地址
fmt.Printf("prev=%v, curr=%v\n", *prev, *curr)
}
}
方案二:显式解构索引+值(语义清晰)
for i, v := range l {
if i > 0 {
prev := &l[i-1] // 注意:仍需通过索引取地址
curr := &l[i]
// 使用 prev 和 curr...
}
}
❗ 关键提醒:即使写成 for i, v := range l,&v 仍是错误的——v 仍是复用的副本变量。唯一可靠的地址来源是 &l[i]。
进阶场景:构建指针切片
若需将整个切片转为 []*t,应避免 &i 写法:
ptrs := make([]*t, len(l))
for i := range l {
ptrs[i] = &l[i] // ✅ 正确
}
// 或使用辅助函数(更安全,避免闭包陷阱)
func toPtrSlice[T any](s []T) []*T {
r := make([]*T, len(s))
for i := range s {
r[i] = &s[i]
}
return r
}
总结
- for range 中的循环变量(如 i 或 v)是单个复用变量,其地址固定,&i 会导致所有指针指向同一内存;
- 需要稳定指针时,必须通过索引访问原始切片:&slice[i];
- 在涉及“当前项与前一项”逻辑时,优先使用带索引的 for i := range slice,语义明确且零开销;
- 切勿因追求 for range 的语法糖而牺牲内存安全性——Go 的指针语义要求开发者对数据布局保持清醒认知。











