for loop and range

·go
#fundamentals

for 循环和 range 关键字

在 Go 语言中,循环结构只有一种关键字—— for

通过不同的写法和变体,for 能够完成所有循环需求

其中最常用的两种形式是传统的 for 循环(类似其他语言里的 for 循环或 while 循环)和 range 形式(主要用于遍历数组、切片、字符串、map 和通道)

传统的 for 循环

基本语法

Go 语言中最常见的 for 循环写法与 C 语言类似,形式如下:

for initialization; condition; post {
    // 循环体
}
  • initialization:初始化语句,只在循环开始前执行一次(如:声明并赋值一个计数器)

  • condition:循环条件,每次循环开始前都会判断,如果条件为 true,则执行循环体,否则退出循环

  • post:循环后语句,每次循环体执行结束后自动执行(如:计数器递增)

示例:

for i := 0; i < 10; i++ {
    fmt.Println("当前 i =", i)
}

在这个例子中,i 从 0 开始,每次循环后 i++,直至 i < 10 不成立,循环退出

省略部分语法

Go 的 for 循环允许省略三个部分中的任意部分,这里有几种常见的变体:

类 while 循环

如果省略初始化和后置语句,则可以将 for 用作 while 循环,其语法形式:

i := 0
for i < 10 {
    fmt.Println("i =", i)

    i++  // 手动更新变量
}

这种写法类似于其他语言中的 while 循环,条件为 i < 10,注意更新变量的逻辑需要写在循环体内,否则可能陷入无限循环

无限循环

如果省略所有三个部分,便可以构造一个无限循环:

for {
    // 无限循环体
    // 通常需要在循环内调用 break 或 return 跳出循环
}

无限循环在服务器、事件循环、定时任务等场景中比较常用

正确使用时,一般会在循环体内用 breakreturn 来跳出循环

控制循环流程

在 for 循环中,还可以使用以下关键字控制循环流程:

  • break:用于立即退出当前循环

  • continue:用于跳过当前循环余下的部分,直接进入下一次循环

  • goto:尽量不推荐使用,但在某些场景下也可以用来转移到指定标签处

示例:

for i := 0; i < 10; i++ {
    if i == 5 {
        continue  // 当 i == 5 时,跳过本次循环
    }

    if i == 8 {
        break  // 当 i == 8 时,退出整个循环
    }

    fmt.Println(i)
}

range 关键字

range 是 Go 专门用于遍历数据结构(如数组、切片、字符串、map 和通道)的语法糖

使用 range 循环时,可以在每次迭代中获得每个元素的值,以及该元素的索引或键(取决于被遍历的数据结构)

数组和切片

基本语法
for index, value := range arrOrSlice {
    // index 为索引,value 为对应的值
}

如果不需要索引,可以使用下划线 _ 省略:

for _, value := range arrOrSlice {
    fmt.Println(value)
}

同样,如果只需要索引,可以只写一个变量

for index := range arrOrSlice {
    fmt.Println("索引:", index)
}

示例:

nums := []int{10, 20, 30, 40}

for i, num := range nums {
    fmt.Printf("下标:%d, 数值:%d\n", i, num)
}

其中,%d 是用来打印十进制整数的占位符,/n 是换行符,表示在输出后换行

字符串

遍历字符串时,range按照 Unicode 码点(rune)来遍历,并返回字符的索引和字符本身

str := "你好, Go!"

for index, char := range str {
    fmt.Printf("字符索引:%d, 字符:%c\n", index, char)
}

其中,%c 是用来打印单个字符的占位符,适用于 rune 类型或 Unicode 码点

也就是说如果传入整数,会被解释为码点然后打印对应的字符(65 -> A)

注意:

  • 字符串里的每个“字符”可能包含多个字节(尤其是非 ASCII 字符)

  • index 返回的是字节索引,而非字符数量的索引

map

遍历 map 时,range 返回键和值,顺序是不确定的map 的遍历顺序是随机的)

m := map[string]int{
    "apple":  1,
    "banana": 2,
    "orange": 3,
}

for key, value := range m {
    fmt.Printf("键:%s, 值:%d\n", key, value)
}

其中 %s 是用来打印字符串类型变量的占位符

range 与传统 for 循环的区别和适用场景

适用场景

传统 for 循环:

当你需要操作循环变量(如需手动控制循环计数、跳出循环、或修改计数器)或需要根据下标进行随机访问时,传统 for 循环非常适合

例如遍历时需要修改数组中某些元素的位置或顺序

range 循环:

对于只需要遍历集合中每个元素,并且不关心具体下标(或下标只是辅助信息)的情况,range 循环更为简洁和安全

特别是遍历数组、切片、字符串、map 时,range自动处理边界和长度问题

注意点

值拷贝问题

对于数组、切片、字符串等数据类型,使用 range 得到的元素是值的拷贝

如果你需要在循环体内修改切片中的元素,可能需要使用索引访问来进行修改

示例:

// 错误示范:修改不会反映到原始切片中
nums := []int{1, 2, 3, 4}

for _, num := range nums {
    num = num * 2 // 这里只是修改了拷贝
}

fmt.Println(nums) // 结果依然是 [1 2 3 4]

// 正确方法:使用索引
for i := range nums {
    nums[i] = nums[i] * 2
}

fmt.Println(nums) // 结果是 [2 4 6 8]
循环顺序

对于 maprange 循环遍历顺序不固定,尤其 map 顺序随机,如果有顺序要求需要额外排序或使用其他数据结构

遍历字符串时的索引

对于字符串,range 返回的是字节索引,如果需要对每个 rune 做一些操作,注意不要把索引当作字符位置

综合示例

以下是一个包含多种循环写法的完整示例,展示了传统 for 循环与 range 的用法:

package main

import (
    "fmt"
)

func main() {
    // 传统 for 循环示例
    fmt.Println("传统 for 循环:")

    for i := 0; i < 5; i++ {
        if i == 3 {
            continue  // 跳过 i==3 的情况
        }

        fmt.Println("i =", i)
    }

    // 类似 while 的 for 循环
    fmt.Println("\nwhile 风格的 for 循环:")

    j := 0
    for j < 5 {
        fmt.Println("j =", j)

        j++
    }

    // 无限循环 + break 示例
    fmt.Println("\n无限循环示例:")

    k := 0
    for {
        fmt.Println("k =", k)
        k++

        if k >= 3 {
            break
        }
    }

    // range 遍历切片
    fmt.Println("\nrange 遍历切片:")

    nums := []int{10, 20, 30, 40}
    for i, num := range nums {
        fmt.Printf("索引:%d, 数值:%d\n", i, num)
    }

    // range 遍历字符串
    fmt.Println("\nrange 遍历字符串:")

    s := "Hello, 世界"
    for i, ch := range s {
        fmt.Printf("索引:%d, 字符:%c\n", i, ch)
    }

    // range 遍历 map
    fmt.Println("\nrange 遍历 map:")

    m := map[string]int{"apple": 5, "banana": 3, "orange": 8}
    for key, value := range m {
        fmt.Printf("键:%s, 值:%d\n", key, value)
    }
}

总结

传统 for 循环适用于需要精确控制初始化、条件和迭代操作的场景,能够轻松实现类似 C 语言的 for 循环、while 循环或无限循环

range 循环是 Go 的语法糖,能简化对数组、切片、字符串、map 的遍历,能自动提供索引和副本,便于快速迭代,但需要注意数据是否为拷贝(尤其是需要修改原数据时)