引言:今天面试的时候,面试官问了一道学 Go 语言的同学都会的简单代码,是关于 Go 语言 for 循环问题的,他询问了一个点,循环中共享变量的地址会发生改变吗?
相信听到这个问题的你,第一反应肯定是不会变啊,怎么会发生改变呢?听到这里面试官选择摇了摇头,那到底是怎么回事呢?接下来我们来研究一下。
题目
Go 语言切片遍历地址会发生改变吗?
推荐解析
正文
首先是下面这段代码,这段代码相信很多在学习 Go 语言的同学都有接触过,那么我们现在来问一个问题,就是在
for 循环中,v 的地址会发生改变吗?
package main
import (
"fmt"
"runtime"
)
func main() {
slice := make([]int, 3, 5)
fmt.Println(runtime.Version())
fmt.Println("---------------------")
for i, v := range slice {
fmt.Printf("%p\n", &v)
fmt.Println(i)
}
}
在此之前,问过一些学习 Go 的同学,如下图所示:
这位学习 Go 的同学第一直觉就是不会变的,那我们现在来运行一下看看结果:
我们会发现,这个关于 v 地址打印的内容,其是会发生改变的,相信很多人第一反应都是不敢相信的的,我们再来测试一组数据:
这次我们会发现,v 的地址又没有改变了,这是怎么回事呢?我们回到原文,将两次运行结果复制下来对比一下:
第一次运行结果:
go1.22.1
---------------------
0xc00000a100
0
0xc00000a110
1
0xc00000a120
2
==========================
第二次运行结果:
go1.21.2
---------------------
0xc00000a100
0
0xc00000a100
1
0xc00000a100
2
我们会发现一个很关键的点,就是第一次运行结果和第二次运行结果关于 Go 语言的版本号有所不同,到这里我们就可以得出一个初步结论,go 语言 1.22 之后与 1.22 之前的版本是存在差异的,即在 1.22 之前,for 循环遍历循环变量是不会发生改变的,1.22 以及之后的版本都是会进行改变的,那改变的原因是什么呢,我们继续来探究一下。
探究
谷歌每次发布 Go 语言的新版本都会发布博客,首先我们来看一下谷歌发布的博客
谷歌 Go 1.22 博客地址:https://golang.google.cn/blog/go1.22
我们会发现其已经发生了改变了,然后我们将官方 Demo 截取下来:
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
期望输出
这段代码在 go 1.21 版本运行的时候,可能会报一个错误,即 loop variable v captured by func literal
但是其还是会输出的,运行结果如下:
c
c
c
但如果使用 1.22 版本输出的话,顺序随机,但是每个值都会输出
a
c
b
共享变量
到这里我们大概就知道了,在 Go 1.22 之前,循环闭包共享的是同一个变量,而 1.22 之后,其不再共享同一个变量,这里我们再打印地址测试一下,代码如下:
package main
import (
"fmt"
"runtime"
)
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
fmt.Println("Go Version:", runtime.Version())
for _, v := range values {
go func() {
fmt.Println(&v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
运行结果如下:
Go Version: go1.22.1
0xc0000260a0
0xc000026080
0xc000026090
=======================
Go Version: go1.21.2
0xc000026080
0xc000026080
0xc000026080
总结
到这里,我们就探究完毕了,Go 1.22 之后,for 循环的循环变量不再共享了,因此循环变量的地址也会发生改变。
最后
最后,我们回到一开始的问题,for 循环遍历切片,循环变量的地址会发生改变吗?这个时候我们需要回答到关于版本变更的问题,即在 1.22 之前,循环变量的地址是共享的,不会发生改变,在 1.22 之后,循环变量的地址不再是共享的,是会发生改变的。
其他补充
鱼聪明 AI 的回答:
鱼聪明 AI 地址:https://www.yucongming.com/
在 Go 语言中,切片(slice)是一个引用类型(reference type)。当你遍历切片时,遍历过程中获取的元素地址不会改变,因为切片本身并不会重新分配内存或改变其底层数组的地址。
具体来说:
- 切片包含了一个指向数组的指针(或称为引用),以及切片的长度和容量。
- 当你通过索引访问切片的元素时,实际上是访问底层数组中对应索引位置的元素。
- 切片的底层数组在不需要扩展容量时,其地址是固定的,因此切片的元素地址在遍历过程中是稳定的。
例如,考虑以下示例代码:
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
for i := range nums {
fmt.Printf("Element at index %d has address: %p\n", i, &nums[i])
}
}
在这个例子中,切片 nums
的每个元素地址在整个循环过程中保持不变,因为 nums
持有的是底层数组的引用,而不是元素本身的复制。
总结来说,Go 语言中遍历切片获取元素的地址是安全的,因为切片的底层数组地址不会随着遍历过程发生改变。
欢迎交流
本文主要介绍 Go 语言切片遍历地址是否会发生改变的问题,在文末还有剩余三个思考题,欢迎小伙伴在评论区进行留言!近期面试鸭小程序已全面上线,想要刷题的小伙伴可以积极参与!
1)for range 循环遍历 slice 有什么问题?
2)如果对切片进行 for range 遍历的时候,遍历过程中追加的元素会不会遍历到?
3)对于需要跨版本兼容的 Go 代码,如何处理循环变量在不同版本中的行为差异?