Go语言循环的坑,你踩过多少?

目录

1. for循环的形式

2. for range

3. for range的坑

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

3.2 循环是否会停止

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

3.3.1 问题

3.3.2 解决方案

3.4 修改切片中的元素

3.4.1 问题

3.4.2 解决方法

3.5 遍历字典时的顺序

3.5.1 问题

3.5.2 解决方法

3.6 字符串遍历

3.6.1 问题

3.6.2 解决方法

3.7 总结

交流学习


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 := 0; i < 10; i++ {
    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([]string, 0)
   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{1, 2}
    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{1, 2}
    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{1, 2, 3}
for i := range v {
    v = append(v, i)
}

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

v := []int{1, 2, 3}
length := len(v)
for i := 0; i < 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{1, 2, 3}
    
    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{1, 2, 3}
    
    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([]string, 0, len(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: 0, byte: 68
index: 1, byte: 65
index: 2, byte: 6c
index: 3, byte: 6c
index: 4, byte: 6f
index: 5, byte: 20
index: 6, byte: e4
index: 7, byte: b8
index: 8, byte: 96
index: 9, byte: e7
index: 10, byte: 95
index: 11, byte: 8c

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杨秀才,持续更新更多硬核文章,一起聊聊互联网网那些事儿!

公众号二维码min

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值