Go语言循环语句
Go语言循环语句
Go只有一个循环关键字——for。没有 while、没有 do-while,但通过 for 的不同写法可以覆盖所有循环场景。简洁统一,这就是Go的风格。
for 循环的四种写法
1. 经典三段式
与C/Java的 for 结构一致,由初始化、条件、后置语句三部分组成:
for i := 0; i < 5; i++ { |
与 if 一样,条件不需要小括号,花括号必须有。
三个部分都可以省略,但分号不能省:
i := 0 |
2. while 模式
Go没有 while 关键字,但省略初始化和后置语句后,for 就是 while:
n := 1 |
等价于其他语言的:
// Java |
3. 无限循环
省略所有部分,for 就是无限循环:
for { |
等价于其他语言的 while(true)。在服务端编程中很常见,比如持续监听请求、消费消息队列等。
4. do-while 模式
Go没有 do-while,但可以用无限循环 + 尾部条件判断来模拟——循环体至少执行一次:
i := 0 |
等价于其他语言的:
// Java |
for range 遍历
for range 是Go遍历集合的标准方式,适用于数组、切片、map、字符串和channel。
遍历数组和切片
nums := []int{10, 20, 30, 40, 50} |
遍历 map
m := map[string]int{ |
注意:map的遍历顺序是随机的,每次运行结果可能不同。如果需要有序遍历,先对key排序:
keys := make([]string, 0, len(m)) |
遍历字符串
for range 遍历字符串时,按UTF-8字符(rune) 遍历,而非按字节:
s := "Go语言" |
索引 i 是该字符在字符串中的字节起始位置,不是第几个字符。中文字符在UTF-8中占3个字节,所以索引会”跳跃”。
break 和 continue
break:终止循环
for i := 0; i < 10; i++ { |
continue:跳过本次迭代
for i := 0; i < 10; i++ { |
嵌套循环中的 break:标签(Label)
默认情况下,break 和 continue 只作用于最内层循环。要跳出外层循环,需要使用标签:
outer: |
不使用标签时:
for i := 0; i < 3; i++ { |
continue 同样支持标签,跳到外层循环的下一次迭代:
outer: |
for range 的常见陷阱
陷阱一:循环变量是副本
range 会将每个元素复制到循环变量中,修改循环变量不会影响原集合:
type User struct{ Name string } |
正确做法——通过索引修改:
for i := range users { |
陷阱二:遍历中取地址
Go 1.22之前,循环变量在整个循环中是同一个变量,每次迭代覆盖值。取地址会导致所有指针指向同一个变量:
// Go 1.21及之前的行为 |
Go 1.22起,每次迭代的循环变量是独立的,上面的代码会正确输出 1 2 3。但为了兼容性和代码清晰度,建议仍然使用局部变量或索引:
for _, v := range nums { |
陷阱三:遍历 map 时删除元素
Go允许在遍历 map 时删除元素,这是安全的:
m := map[string]int{"a": 1, "b": 2, "c": 3} |
但在遍历 map 时新增元素,新元素可能出现也可能不出现在后续迭代中,行为是不确定的。应避免在遍历中新增。
性能提示
提前获取长度
遍历切片时,len() 在每次循环条件判断时都会被调用。虽然对切片来说开销很小(内联优化),但在某些特殊场景下提前取出可读性更好:
// 常规写法,通常足够 |
预分配切片容量
如果在循环中向切片 append,提前分配容量可以避免多次扩容:
// 不推荐:多次扩容 |
面试高频题
Q1:Go有几种循环?为什么没有 while?
答:Go只有 for 一种循环关键字,通过不同写法覆盖所有场景:三段式 for i := 0; i < n; i++、while模式 for condition {}、无限循环 for {}、以及 for range 遍历集合。不提供 while 和 do-while 是Go”一种事情只有一种做法”的设计哲学——减少选择成本,降低心智负担。
Q2:for range 遍历切片时,修改元素能生效吗?
nums := []int{1, 2, 3} |
答:输出 [1 2 3],修改不生效。range 将每个元素复制到循环变量 v 中,修改 v 只是修改副本。要修改原切片,必须用索引:for i := range nums { nums[i] *= 2 }。如果切片存储的是指针类型,通过 v 修改指向的对象是可以生效的。
Q3:for range 遍历 map 的顺序是固定的吗?
答:不固定。Go的map遍历顺序是随机的,这是运行时故意引入的随机化,防止开发者依赖遍历顺序。即使map内容不变,两次遍历的顺序也可能不同。如果需要有序遍历,应先将key取出到切片中排序,再按排序后的key遍历。
Q4:下面代码输出什么?
s := "Go语言" |
答:输出 4。for range 遍历字符串时按UTF-8字符(rune)遍历,不是按字节。"Go语言" 包含4个字符(G、o、语、言),虽然占8个字节(2+3+3),但 range 迭代4次。如果用 len(s) 得到的是字节数8。
Q5:break 和 continue 在嵌套循环中的行为?
答:默认只作用于最内层循环。break 终止最内层循环,continue 跳过最内层循环的当前迭代。如果需要控制外层循环,使用标签(label):在外层循环前定义标签如 outer:,然后用 break outer 跳出外层循环,或 continue outer 跳到外层循环的下一次迭代。
Q6:Go 1.22 对循环变量做了什么改变?
答:Go 1.22之前,for range 的循环变量在整个循环中是同一个变量,每次迭代覆盖其值。这导致在闭包或取地址时,所有引用都指向最后一次迭代的值,是Go最常见的 bug 之一。Go 1.22起,每次迭代的循环变量是独立的新变量,闭包和取地址都能正确捕获当前迭代的值。这是一个不向后兼容的语义变更,通过 go.mod 中的Go版本声明来控制生效。
Q7:在 for range 遍历切片时 append 元素,会发生什么?
s := []int{1, 2, 3} |
答:输出 [1 2 3 10 20 30],只循环3次。for range 在开始时就确定了遍历的长度(基于进入循环时的切片长度),循环过程中 append 的元素不会影响迭代次数。这与直接用 for i := 0; i < len(s); i++ 不同——后者每次检查 len(s) 的当前值,会导致无限循环。
Q8:下面的无限循环怎么安全退出?
for { |
答:常见的退出方式有三种。第一,break 直接退出循环。第二,return 直接退出函数。第三,配合 context 实现优雅退出:
for { |
在服务端编程中,推荐使用 context 方式,它支持超时控制和级联取消。
小结
| 概念 | 要点 |
|---|---|
| for 三段式 | for init; condition; post {},唯一的循环关键字 |
| while 模式 | for condition {},省略初始化和后置语句 |
| 无限循环 | for {},配合 break/return/context 退出 |
| do-while 模式 | for { ... if cond { break } },循环体至少执行一次 |
| for range | 遍历数组、切片、map、字符串、channel 的标准方式 |
| 字符串遍历 | range 按 rune 遍历,索引是字节位置 |
| break/continue | 默认作用于最内层循环,用标签控制外层循环 |
| 循环变量陷阱 | range 的循环变量是副本;Go 1.22 起每次迭代独立 |
| map 遍历顺序 | 随机的,需要有序则先排序 key |
下一篇将介绍Go的函数与方法。










