Go range 指针切片复用变量时为什么地址相同
来源:17golang原创
时间:2026-09-15 05:01:18 476浏览 收藏
在 Go 代码里,下面这种写法很容易让人误以为拿到了切片元素的地址:
for _, item := range items {
pointers = append(pointers, &item)
}
真正发生的事情取决于 Go 语言版本和变量声明方式:item 是 range 产生的迭代变量,默认承接的是元素值;在 Go 1.21 及更早语义中,它会在多轮循环中复用,所以 &item 可能每次都是同一个地址。需要指向原切片元素时,应改用 &items[i];只想保存每轮的独立副本时,则在循环体中重新声明一个变量。Go 1.22 起,使用 := 声明的循环变量按模块语言版本采用每轮新变量语义,但它仍然不是切片元素本身。
range的 value 是元素副本,&item不等于&items[i]。- 旧语言语义复用同一个循环变量,因此指针切片可能全部指向同一地址。
- 要原元素用索引取地址,要独立副本就显式复制后再取地址。
先分清 range 变量和切片元素
假设切片元素是结构体,for i, item := range items 每轮会把 items[i] 的值交给 item。这一步是值拷贝,不会让 item 自动变成切片存储区里的那个对象。因此有两个完全不同的地址表达式:
| 写法 | 实际指向 | 适用意图 |
|---|---|---|
&item | range 迭代变量 | 需要独立副本时谨慎使用 |
&items[i] | 切片底层数组中的元素 | 后续要修改原切片对象 |
item.ID | 当前副本的字段 | 只读计算或生成结果 |
所以“地址相同”的根因不是指针语法失效,而是取地址的对象选错了。特别是在旧项目中,循环结束后 item 还保留最后一轮的值,多个指针就可能一起读到这个最终值。

为什么旧项目里会出现相同地址
在 Go 1.21 及更早的循环变量语义下,:= 声明的 range 变量属于整个循环作用域,运行时会反复给同一个变量赋值。第一次取到 items[0],第二次再把 items[1] 写进同一块变量存储;如果每轮把它的地址追加到 []*Item,这些指针自然相同。
type Item struct {
// ID 用来观察每轮循环到底保存了哪个对象。
ID int
}
func collect(items []Item) []*Item {
pointers := make([]*Item, 0, len(items))
for _, item := range items {
// 旧语言语义下,item 可能是被多轮复用的同一个变量。
pointers = append(pointers, &item)
}
return pointers
}
这段代码的危险信号是“range value 的地址被跨轮保存”。不要只看编译是否通过,还要问两个问题:模块的 go.mod 语言版本是什么?业务需要的是原切片元素,还是每轮数据的独立快照?
Go 1.22 起,循环变量的规则按模块声明的语言版本切换到每轮新变量语义,闭包捕获问题因此减少;但 &item 仍然指向迭代变量的副本,不会因为地址不同就变成 &items[i]。这两个问题要分开判断。
两种修复写法:原元素与独立副本
如果指针要代表切片中的真实对象,直接使用索引:
func pointersToElements(items []Item) []*Item {
pointers := make([]*Item, 0, len(items))
for i := range items {
// 通过索引取地址,指针稳定地指向切片元素本身。
pointers = append(pointers, &items[i])
}
return pointers
}
如果业务只想保存每轮值的副本,不希望后续修改影响原切片,可以在循环体中显式创建新变量:
func copiesAsPointers(items []Item) []*Item {
pointers := make([]*Item, 0, len(items))
for _, item := range items {
itemCopy := item
// 每轮显式复制,地址表达的是当前快照而不是原切片元素。
pointers = append(pointers, &itemCopy)
}
return pointers
}
第二种写法在代码审查中更直白,也不依赖读者记住项目是否启用了 Go 1.22 语义。若结构体很大,要评估复制成本;如果只是读取字段,通常不需要构造指针切片,直接处理 item 就够了。

用四项检查避免再次踩坑
- 先看对象:需要修改原切片就检查是否使用了
&items[i],不要用&item猜测。 - 再看版本:确认
go.mod的go行。Go 1.22 的每轮变量规则不能替代对指针目标的判断。 - 检查 map:map 的 range value 同样是副本,不能通过
&value得到 map 元素地址;需要修改时应通过 key 回写。 - 检查闭包和 append:把地址或闭包保存到循环外,就属于跨轮使用,必须明确想捕获的是原元素、快照还是索引。
最后可以用一个简单的思路复查:把指针解引用后与原切片逐项对照,再检查是否允许后续修改原切片。只要“指针代表谁”说不清,代码就不应直接合并。
常见问题
Go 1.22 以后可以放心写 &item 吗?
不能一概而论。它在符合新语言版本语义时通常不会再因为循环变量复用而地址相同,但仍指向 value 副本;需要原切片元素时依旧使用 &items[i]。
为什么用索引取地址后,扩容还要小心?
指针指向切片当前底层数组中的元素。对切片追加导致底层数组更换后,旧指针仍指向旧数组中的对象,因此要明确指针的生命周期和数据所有权。
只读场景还需要指针切片吗?
多数只读处理不需要。直接在循环体内消费 item 或保存值切片,通常更简单,也避免了地址、生命周期和版本语义的额外判断。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
300 收藏
-
318 收藏
-
245 收藏
-
210 收藏
-
438 收藏
-
208 收藏
-
125 收藏
-
485 收藏
-
496 收藏
-
241 收藏
-
132 收藏
-
212 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习