likes
comments
collection
share

Go语言循环的坑,你踩过多少?`for range`对于数组,`map`的遍历非常方便,但是`for range`也有不

作者站长头像
站长
· 阅读数 17

go语言的循环不像其他语言一样有多种,比如c++for,while,do-while。在go语言中循环就只有for一种,所以用起来也是十分的方便

1. for循环的形式

Go 语言的 for 循环有 3 种形式,只有其中的一种使用分号。 第一种:类似于c语言的for循环

for init; condition; post {

}

第二种:类似于c语言的while

for condition { 

}

第三种:类似于c语言的 for(;;)

for { 

}
  • init: 一般为赋值表达式,给控制变量赋初值;
  • condition: 关系表达式或逻辑表达式,循环控制条件;
  • post: 一般为赋值表达式,给控制变量增量或减量
for i := 0i < 10i++ {
    fmt.Println(i)
}

2. for range

for 循环的 range 格式可以对 slicemap、数组、字符串等进行迭代循环。格式如下:

for key, value := range oldMap {
    newMap[key] = value
}

以上代码中的 keyvalue 是可以省略。 如果只想读取 key,格式如下:

for key := range oldMap

如果只想读取 value,格式如下:

for _, value := range oldMap

代码展示:

package main

import "fmt"

func main() {
   for i := 0; i < 5; i++ {
      fmt.Printf("current i %d\n", i)
   }
   j := 0
   for {
      if j == 5 {
         break
      }
      fmt.Printf("current j %d\n", j)
      j++
   }
   var strAry = []string{"aa""bb""cc""dd""ee"//是的,不指定初始个数也ok
   //切片初始化
   var sliceAry = make([]string0)
   sliceAry = strAry[1:3]
   for i, str := range sliceAry {
      fmt.Printf("slice i %d, str %s\n", i, str)
   }
   //字典初始化
   var dic = map[string]int{
      "apple":      1,
      "watermelon"2,
   }
   for k, v := range dic {
      fmt.Printf("key %s, value %d\n", k, v)
   }
}

输出如下:

current i 0
current i 1
current i 2
current i 3
current i 4
current j 0
current j 1
current j 2
current j 3
current j 4
slice i 0, str bb
slice i 1, str cc
key apple, value 1
key watermelon, value 2

3. for range的坑

通过上面例子不难发现for range对于数组,map的遍历非常方便,但是for range也有不少的坑,其中不乏一些熟悉golang的开发人员,往往也会掉到坑里,下面就给大家来捋一捋Go语言 for range中那些容易遇到的坑,这也是面试Go语言基础时通常会被面试官问到的

3.1 for range取不到所有元素的地址

PS:如果你用的是Go 1.22版本以及之后的话,那是能取到地址的,而且要注意,这个地址是临时变量的地址,不是原元素的地址

package main

import "fmt"

func main() {
    arr := [2]int{12}
    res := []*int{}
    for _, v := range arr {
       res = append(res, &v)
    }
    // expect: 1 2 预期的结果
    // but 
    // result: 2 2 实际的结果
    fmt.Println(*res[0], *res[1])
}

上述代码通过定义一个数组arr,数组元素为1,2。然后试图通过取到数组的这两个元素的地址放到切片res中,最后通过取地址操作符*res[0]*res[1]打印出切片中的元素,希望结果输出1和2,但结果恰恰不是我们所预期的那样。 实际代码输出:

2,2

实际输出的是两个2,那么问题在哪里呢? 我们可以在每次for range 循环打印出v的地址,会发现v是不变的,那么我们每次将v的地址加入 到res中,res中最终所有的元素都是一个地址,这一个地址最终指向 的是 v 最后遍历得到的值——也就是2

package main

import "fmt"

func main() {
    arr := [2]int{12}
    res := []*int{}
    for _, v := range arr {
       // v 每次都是同一个变量
       fmt.Println(&v)
       res = append(res, &v)
    }
    // expect: 1 2
    // but
    // result: 2 2
    fmt.Println(*res[0], *res[1])
}

那如果我们想要得到预期的结果1和2,应该怎么改呢?两种方式

  1. 第一种:使用局部变量v1拷贝v
for _, v := range arr {
    //局部变量v替换了v,也可用别的局部变量名
    v1 := v 
    res = append(res, &v1)
}
  1. 第二种:直接使用索引获取原来的元素
for k := range arr {
    res = append(res, &arr[k])
}

3.2 循环是否会停止

v := []int{123}
for i := range v {
    v = append(v, i)
}

在循环遍历的同时往遍历的切片追加元素,循环会停止吗? 答案是:。 在 Go 语言中,for i := range v 语句会在循环开始前对切片 v 的长度进行一次评估,并将这个长度用于控制循环的迭代次数。之后,如果在循环体内修改了切片 v 的长度(比如通过 append 函数),这个修改并不会影响已经确定的循环迭代次数。 上述例子可以看作是下面这个代码:

v := []int{123}
length := len(v)
for i := 0i < length; i++ {
    v = append(v, i)
}

3.3 使用迭代变量时的闭包问题

3.3.1 问题

for range 循环中,如果在闭包中使用迭代变量,可能会遇到意想不到的结果。因为闭包会捕获迭代变量的引用,而不是它的值 示例:

package main

import (
    "fmt"
)

func main() {
    var funcs []func()

    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() {
            fmt.Println(i)
        })
    }

    for _, f := range funcs {
        f()
    }
}

输出

3
3
3

3.3.2 解决方案

使用局部变量保存当前迭代变量的值

package main

import (
    "fmt"
)

func main() {
    var funcs []func()

    for i := 0; i < 3; i++ {
        i := i // 创建新的局部变量 i
        funcs = append(funcs, func() {
            fmt.Println(i)
        })
    }

    for _, f := range funcs {
        f()
    }
}

输出

0
1
2

3.4 修改切片中的元素

3.4.1 问题

for range 会创建每个元素的副本,而不是直接操作原始切片中的元素。因此,修改迭代变量不会影响原始切片 示例

package main

import (
    "fmt"
)

func main() {
    slice := []int{123}
    
    for _, v := range slice {
        v *= 10
    }
    
    fmt.Println(slice) // 输出: [1 2 3]
}

输出

[1 2 3]

3.4.2 解决方法

使用索引访问并修改原始切片中的元素

package main

import (
    "fmt"
)

func main() {
    slice := []int{123}
    
    for i := range slice {
        slice[i] *= 10
    }
    
    fmt.Println(slice) // 输出: [10 20 30]
}

输出

[10 20 30]

3.5 遍历字典时的顺序

3.5.1 问题

在 Go 中,使用 for range 遍历Map时,遍历顺序是随机的。每次运行程序时,顺序可能不同 示例

package main

import (
    "fmt"
)

func main() {
    dic := map[string]int{"a"1"b"2"c"3}

    for k, v := range dic {
        fmt.Printf("key: %s, value: %d\n", k, v)
    }
}

输出, 每次运行结果可能不同

key: a, value: 1
key: c, value: 3
key: b, value: 2

3.5.2 解决方法

如果需要特定的顺序,可以先对键进行排序,然后再遍历

package main

import (
    "fmt"
    "sort"
)

func main() {
    dic := map[string]int{"a"1"b"2"c"3}
    keys := make([]string0len(dic))

    for k := range dic {
        keys = append(keys, k)
    }

    sort.Strings(keys)

    for _, k := range keys {
        fmt.Printf("key: %s, value: %d\n", k, dic[k])
    }
}

输出

key: a, value: 1
key: b, value: 2
key: c, value: 3

对比学习

  1. C++: • std::map:有序容器,遍历顺序是键的升序。(内部使用红黑树实现) • std::unordered_map:无序容器,遍历顺序不可预测。(内部使用哈希表实现)
  2. Python: • Python 3.7 及更高版本:字典遍历顺序是插入顺序 • Python 3.6 及更早版本:字典遍历顺序可能是插入顺序,但并不保证

3.6 字符串遍历

3.6.1 问题

for range 遍历字符串时,每次迭代会返回 Unicode 代码点(rune),而不是字节。如果字符串包含多字节字符,这一点尤其重要 示例:

package main

import (
    "fmt"
)

func main() {
    str := "hello 世界"
    
    for i, r := range str {
        fmt.Printf("index: %d, rune: %c\n", i, r)
    }
}

输出

index: 0, rune: h
index: 1, rune: e
index: 2, rune: l
index: 3, rune: l
index: 4, rune: o
index: 5, rune:  
index: 6, rune: 世
index: 9, rune: 界

3.6.2 解决方法

理解 for range 返回的是 Unicode 代码点,而不是字节。如果需要按字节遍历,可以使用常规的 for 循环

package main

import (
    "fmt"
)

func main() {
    str := "hello 世界"
    
    for i := 0; i < len(str); i++ {
        fmt.Printf("index: %d, byte: %x\n", i, str[i])
    }
}

输出

index: 0byte68
index: 1byte65
index: 2byte6c
index: 3byte6c
index: 4byte6f
index: 5byte20
index: 6byte: e4
index: 7byte: b8
index: 8byte96
index: 9byte: e7
index: 10byte95
index: 11byte8c

3.7 总结

  1. Go 1.22之前通过for _, v := range arr遍历切片取不到所有变量的地址,而是同一个临时变量的地址
  2. 闭包中的迭代变量:迭代变量在闭包中被捕获,导致所有闭包共享同一个变量。
  3. 切片元素修改:for range 会创建元素的副本,直接修改迭代变量不会影响原切片。
  4. 字典遍历顺序:遍历字典时,顺序是随机的,每次运行可能不同。
  5. 字符串遍历:for range 遍历字符串时返回的是 Unicode 代码点(rune),而不是字节,可能导致多字节字符处理复杂。
  6. 删除切片元素: 在 for range 中删除切片元素可能导致意外行为或漏掉某些元素。
  7. 遍历修改映射(字典):在 for range 循环中修改字典(如添加或删除键值对)可能导致未定义行为或错误。

交流学习

如果您觉得文章有帮助,请帮忙转发给更多好友,或关注公众号:IT杨秀才,持续更新更多硬核文章,一起聊聊互联网网那些事儿!

转载自:https://juejin.cn/post/7416586466485403658
评论
请登录