1. 引言
循环是编程语言中控制程序重复执行某段代码的核心结构。Go语言的设计哲学强调简洁与高效,因此在循环语句的设计上与其他语言(如C、Java、Python)有明显不同。Go语言只提供了一种循环结构:for循环。这种看似简单的设计,通过灵活的语法变体,足以覆盖所有常见的循环场景,包括传统的条件循环、无限循环以及遍历集合的迭代循环。
本文将全面解析Go语言中的循环语句,涵盖for循环的三种基本形式、用于遍历的range关键字,以及break、continue、goto等循环控制语句。通过丰富的代码示例,帮助你掌握Go语言循环的精髓。
2. for循环的三种基本形式
Go语言的for循环有三种写法,分别对应不同的使用场景。
2.1 基本for循环(类似C语言的for)
这种形式包含初始化语句、条件表达式和后续语句,用分号分隔。
go
package main
import "fmt"
func main() {
// 形式一:完整的for循环
for i := 0; i < 5; i++ {
fmt.Printf("i = %d\n", i)
}
// 输出:
// i = 0
// i = 1
// i = 2
// i = 3
// i = 4
}
要点:
- 初始化语句(
i := 0)仅在循环开始时执行一次。 - 条件表达式(
i < 5)在每次迭代前求值,为true则执行循环体。 - 后续语句(
i++)在每次循环体执行完毕后执行。 - 初始化语句中声明的变量(如
i)其作用域仅限于该for循环内部。
2.2 条件for循环(类似while循环)
省略初始化语句和后续语句,只保留条件表达式。此时for循环的功能等同于其他语言中的while循环。
go
package main
import "fmt"
func main() {
count := 5
// 形式二:仅包含条件的for循环(类似while)
for count > 0 {
fmt.Printf("倒计时: %d\n", count)
count--
}
fmt.Println("发射!")
}
2.3 无限循环
省略所有三个部分,形成一个无限循环。必须在循环体内使用break语句或其他方式退出,否则程序将永远运行。
go
package main
import (
"fmt"
"time"
)
func main() {
// 形式三:无限循环
ticker := 0
for {
fmt.Printf("心跳 %d\n", ticker)
ticker++
time.Sleep(1 * time.Second) // 等待1秒
// 退出条件
if ticker >= 5 {
fmt.Println("心跳停止")
break
}
}
}
3. 使用range进行遍历
range关键字是Go语言中用于遍历数组、切片、字符串、映射(map)和通道(channel)的利器。它会在每次迭代中返回索引(或键)和值。
3.1 遍历数组和切片
go
package main
import "fmt"
func main() {
fruits := []string{"苹果", "香蕉", "橙子"}
// 遍历切片,获取索引和值
for index, fruit := range fruits {
fmt.Printf("fruits[%d] = %s\n", index, fruit)
}
// 如果只需要值,用下划线忽略索引
for _, fruit := range fruits {
fmt.Printf("水果: %s\n", fruit)
}
// 如果只需要索引
for index := range fruits {
fmt.Printf("索引: %d\n", index)
}
}
3.2 遍历字符串
遍历字符串时,range会迭代Unicode码点(rune),而不是字节。这对于处理中文等多字节字符非常安全。
go
package main
import "fmt"
func main() {
str := "Hello, 世界"
// 遍历字符串,i是字节索引,c是rune(Unicode码点)
for i, c := range str {
fmt.Printf("str[%d] = %c (Unicode: %U)\n", i, c, c)
}
}
3.3 遍历映射(map)
遍历map时,每次迭代的顺序是随机的(Go语言的故意设计,以防止开发者依赖特定顺序)。
go
package main
import "fmt"
func main() {
scoreMap := map[string]int{
"Alice": 95,
"Bob": 87,
"Cathy": 92,
}
for name, score := range scoreMap {
fmt.Printf("%s 的分数是 %d\n", name, score)
}
}
3.4 遍历通道(channel)
range会从通道中持续接收值,直到通道被关闭。
go
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // 必须关闭通道,range才能结束
for value := range ch {
fmt.Println("从通道接收到:", value)
}
}
4. 循环控制语句
4.1 break语句
break用于立即终止最内层的for、switch或select语句的执行。
go
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
fmt.Println("遇到5,提前终止循环")
break
}
fmt.Println(i)
}
}
带标签的break:可以终止外层循环。
go
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
fmt.Printf("在 i=%d, j=%d 处跳出外层循环\n", i, j)
break OuterLoop
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
}
4.2 continue语句
continue用于跳过当前循环的剩余语句,直接进入下一次迭代。
go
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
if i%2 == 0 { // 跳过偶数
continue
}
fmt.Println("奇数:", i)
}
}
同样,continue也可以使用标签来指定跳转到哪个循环的下一次迭代。
4.3 goto语句
goto语句可以将控制无条件转移到同一函数内的带标签语句。虽然在现代编程中不鼓励滥用,但在某些错误处理或跳出多层嵌套的场景下,它可以使代码更清晰。
go
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
fmt.Println("跳到循环外")
goto Exit
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
Exit:
fmt.Println("已通过goto跳出")
}
5. 嵌套循环与常见模式
5.1 嵌套循环示例
go
package main
import "fmt"
func main() {
// 打印九九乘法表
for i := 1; i <= 9; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d×%d=%-2d ", j, i, i*j) // %-2d 表示左对齐,占2位
}
fmt.Println() // 换行
}
}
5.2 使用循环处理切片(过滤、映射)
go
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 过滤:找出偶数
var evens []int
for _, num := range numbers {
if num%2 == 0 {
evens = append(evens, num)
}
}
fmt.Println("偶数:", evens)
// 映射:将每个数平方
squares := make([]int, len(numbers))
for i, num := range numbers {
squares[i] = num * num
}
fmt.Println("平方:", squares)
}
6. 性能注意事项与最佳实践
-
避免在循环内频繁分配内存 :例如,在循环内反复使用
append时,如果可能,尽量预先分配好切片的容量。go// 不佳 var result []int for i := 0; i < 1000; i++ { result = append(result, i*2) // 可能导致多次重新分配 } // 更佳 result := make([]int, 0, 1000) // 预先分配容量 for i := 0; i < 1000; i++ { result = append(result, i*2) } -
遍历大切片时考虑使用索引 :如果不需要修改值,使用
for i := 0; i < len(slice); i++比for _, v := range slice在极少数性能敏感场景下可能略有优势,因为后者会产生值的副本。但通常可读性优先,差异可忽略。 -
小心循环变量的捕获(在闭包中):在Go 1.22版本之前,在goroutine或闭包中使用循环变量是一个常见的坑。务必注意。
go// Go 1.21及之前:有问题 for _, v := range []int{1, 2, 3} { go func() { fmt.Println(v) // 可能全部打印3 }() } // 修正:将变量作为参数传入 for _, v := range []int{1, 2, 3} { go func(val int) { fmt.Println(val) // 正确打印1,2,3 }(v) }(Go 1.22及之后版本修复了此问题,循环变量每次迭代会创建新实例)
7. 总结
Go语言通过单一的for关键字,配合灵活的语法和强大的range关键字,提供了清晰、高效且安全的循环机制。记住核心几点:
- 只有
for,没有while和do-while,但for可以模拟它们。 range是遍历容器和通道的首选,它安全、简洁,并能正确处理Unicode。- 善用
break、continue和带标签的控制流来处理复杂的循环逻辑。 - 注意循环变量的作用域和闭包捕获问题,尤其是在旧版本Go中。
掌握这些循环语句,你就能优雅地处理Go程序中的重复性任务。