深挖 go 之 for-range 排坑指南
文章目录
-
遍历取不到所有元素指针?
-
遍历会停止么?
-
对大数组这样遍历有啥问题?
-
对大数组这样重置效率高么?
-
对 map 遍历时删除元素能遍历到么?
-
对 map 遍历时新增元素能遍历到么?
-
这样遍历中起 goroutine 可以么?
遍历取不到所有元素指针?
如下代码想从数组遍历获取一个指针元素切片集合
1 2 3 4 5 6 7 8 | arr := [2]int{1, 2} res := []*int{} for _, v := range arr { res = append(res, &v) } //expect: 1 2 fmt.Println(*res[0],*res[1]) //but output: 2 2 |
答案是【取不到】 同样代码对切片[]int{1, 2}
或map[int]int{1:1, 2:2}
遍历也不符合预期。问题出在哪里?
通过查看go 编译源码[1]可以了解到, for-range 其实是语法糖,内部调用还是 for 循环,初始化会拷贝带遍历的列表(如 array,slice,map),然后每次遍历的v
都是对同一个元素的遍历赋值。也就是说如果直接对v
取地址,最终只会拿到一个地址,而对应的值就是最后遍历的那个元素所附给v
的值。对应伪代码如下:
1 2 3 4 5 6 7 8 | // len_temp := len(range) // range_temp := range // for index_temp = 0; index_temp < len_temp; index_temp++ { // value_temp = range_temp[index_temp] // index = index_temp // value = value_temp // original body // } |
那么怎么改?有两种
- 使用局部变量拷贝
v
1 2 3 4 5 | for _, v := range arr { //局部变量v替换了v,也可用别的局部变量名 v := v res = append(res, &v) } |
- 直接索引获取原来的元素
1 2 3 4 | //这种其实退化为for循环的简写 for k := range arr { res = append(res, &arr[k]) } |
理顺了这个问题后边的坑基本都好发现了,来迅速过一遍
遍历会停止么?
1 2 3 4 | v := []int{1, 2, 3} for i := range v { v = append(v, i) } |
答案是【会】,因为遍历前对v
做了拷贝,所以期间对原来v
的修改不会反映到遍历中
对大数组这样遍历有啥问题?
1 2 3 4 5 6 7 | //假设值都为1,这里只赋值3个 var arr = [102400]int{1, 1, 1} for i, n := range arr { //just ignore i and n for simplify the example _ = i _ = n } |
答案是【有问题】!遍历前的拷贝对内存是极大浪费啊 怎么优化?有两种
- 对数组取地址遍历
for i, n := range &arr
- 对数组做切片引用
for i, n := range arr[:]
反思题:对大量元素的 slice 和 map 遍历为啥不会有内存浪费问题?(提示,底层数据结构是否被拷贝)
对大数组这样重置效率高么?
1 2 3 4 5 | //假设值都为1,这里只赋值3个 var arr = [102400]int{1, 1, 1} for i, _ := range &arr { arr[i] = 0 } |
答案是【高】,这个要理解得知道 go 对这种重置元素值为默认值的遍历是有优化的, 详见go 源码:memclrrange[2]
1 2 3 4 5 6 7 8 9 | // Lower n into runtime·memclr if possible, for // fast zeroing of slices and arrays (issue 5373). // Look for instances of // // for i := range a { // a[i] = zero // } // // in which the evaluation of a is side-effect-free. |
对 map 遍历时删除元素能遍历到么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var m = map [int]int{1: 1, 2: 2, 3: 3} //only del key once, and not del the current iteration key var o sync.Once for i := range m { o.Do( func () { for _, key := range []int{1, 2, 3} { if key != i { fmt.Printf( "when iteration key %d, del key %d\n" , i, key) delete(m, key) break } } }) fmt.Printf( "%d%d " , i, m[i]) } |
答案是【不会】 map 内部实现是一个链式 hash 表,为保证每次无序,初始化时会随机一个遍历开始的位置[3], 这样,如果删除的元素开始没被遍历到(上边once.Do
函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。
对 map 遍历时新增元素能遍历到么?
1 2 3 4 5 | var m = map [int]int{1:1, 2:2, 3:3} for i, _ := range m { m[4] = 4 fmt.Printf( "%d%d " , i, m[i]) } |
答案是【可能会】,输出中可能会有44
。原因同上一个, 可以用以下代码验证
1 2 3 4 5 6 7 8 9 10 11 12 | var createElemDuringIterMap = func () { var m = map [int]int{1: 1, 2: 2, 3: 3} for i := range m { m[4] = 4 fmt.Printf( "%d%d " , i, m[i]) } } for i := 0; i < 50; i++ { //some line will not show 44, some line will createElemDuringIterMap() fmt.Println() } |
这样遍历中起 goroutine 可以么?
1 2 3 4 5 6 7 8 | var m = []int{1, 2, 3} for i := range m { go func () { fmt.Print(i) }() } //block main 1ms to wait goroutine finished time.Sleep(time.Millisecond) |
答案是【不可以】。预期输出 0,1,2 的某个组合,如 012,210.. 结果是 222. 同样是拷贝的问题 怎么解决
- 以参数方式传入
1 2 3 4 5 | for i := range m { go func (i int) { fmt.Print(i) }(i) } |
- 使用局部变量拷贝
1 2 3 4 5 6 | for i := range m { i := i go func () { fmt.Print(i) }() } |
发现没,一个简单的 for-range,仔细剖析下来也是有不少有趣的地方。希望剖析后能让你更进一步的了解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
2019-02-21 数据库学习之事务
2019-02-21 pymysql的使用
2019-02-21 pymysql:Mysql拒绝从远程访问的解决办法