大家好,我是极客老墨。
写 Go 之前,我在 Java 里循环都是 for、while、do-while 三件套。转到 Go 发现只有一个 for,心想"这够用吗?"
结果发现 Go 的 for 能当 while 用,能当无限循环用,还能用 range 遍历。更绝的是 switch 默认不用 break,if 还能带初始化语句。
这篇就聊聊 Go 控制结构的几个巧妙设计。
if:条件判断不用括号
Go 的 if 语句很简洁,条件不需要括号:
1x := 10
2if x > 5 {
3 fmt.Println("x is large")
4}
5
6// if-else
7if x > 10 {
8 fmt.Println("x is large")
9} else {
10 fmt.Println("x is small")
11}
⚠️ 注意:大括号 {} 是必须的,且左大括号不能换行。
初始化语句:限制变量作用域
if 可以带初始化语句,变量作用域仅限于 if-else 块:
1// 常用于错误处理
2if err := doSomething(); err != nil {
3 fmt.Println("Error:", err)
4 return
5}
6
7// 常用于 map 查找
8if value, ok := m["key"]; ok {
9 fmt.Println("Found:", value)
10} else {
11 fmt.Println("Not found")
12}
💡 技巧:这种写法让变量作用域更小,代码更简洁。
switch:默认不用 break
Go 的 switch 跟其他语言不一样,匹配到一个 case 后自动停止:
1day := "Mon"
2switch day {
3case "Mon":
4 fmt.Println("Monday")
5 // 不需要 break,自动停止
6case "Tue":
7 fmt.Println("Tuesday")
8default:
9 fmt.Println("Other day")
10}
一个 case 多个值
1switch day {
2case "Sat", "Sun":
3 fmt.Println("Weekend")
4case "Mon", "Tue", "Wed", "Thu", "Fri":
5 fmt.Println("Weekday")
6}
无表达式 switch:替代复杂 if-else
switch 后可以没有表达式,直接在 case 中写条件:
1score := 85
2switch {
3case score >= 90:
4 fmt.Println("A")
5case score >= 80:
6 fmt.Println("B")
7case score >= 70:
8 fmt.Println("C")
9default:
10 fmt.Println("D")
11}
这比一堆 if-else 清晰多了。
fallthrough:强制执行下一个 case
如果想继续执行下一个 case,用 fallthrough:
1num := 1
2switch num {
3case 1:
4 fmt.Println("One")
5 fallthrough
6case 2:
7 fmt.Println("Two or after one")
8}
9// 输出: One
10// Two or after one
⚠️ 小坑:fallthrough 必须是 case 块的最后一条语句。
类型 switch:判断接口类型
1func checkType(i interface{}) {
2 switch v := i.(type) {
3 case int:
4 fmt.Printf("Integer: %d\n", v)
5 case string:
6 fmt.Printf("String: %s\n", v)
7 default:
8 fmt.Printf("Unknown type: %T\n", v)
9 }
10}
for:一个循环打天下
Go 只有 for 一种循环,但它有三种形式:
形式 1:标准 for 循环
1for i := 0; i < 5; i++ {
2 fmt.Println(i)
3}
形式 2:当 while 用
1count := 0
2for count < 5 {
3 fmt.Println(count)
4 count++
5}
形式 3:无限循环
1for {
2 fmt.Println("Loop forever")
3 break // 需要 break 才能退出
4}
range:遍历集合的利器
range 用于遍历数组、切片、字符串、map 和 channel:
1// 遍历切片
2nums := []int{1, 2, 3, 4, 5}
3for i, v := range nums {
4 fmt.Printf("Index: %d, Value: %d\n", i, v)
5}
6
7// 只要索引
8for i := range nums {
9 fmt.Println("Index:", i)
10}
11
12// 只要值(用 _ 忽略索引)
13for _, v := range nums {
14 fmt.Println("Value:", v)
15}
16
17// 遍历 map
18m := map[string]int{"a": 1, "b": 2}
19for key, value := range m {
20 fmt.Printf("%s: %d\n", key, value)
21}
range 的两个大坑
坑 1:值是拷贝
1type Person struct {
2 Name string
3 Age int
4}
5
6people := []Person{{"Alice", 20}, {"Bob", 25}}
7
8// 错误:修改的是拷贝
9for _, p := range people {
10 p.Age++ // 无效!
11}
12
13// 正确:使用索引
14for i := range people {
15 people[i].Age++
16}
坑 2:循环变量地址问题
1// 错误:所有指针指向同一个变量
2var ptrs []*int
3for _, v := range []int{1, 2, 3} {
4 ptrs = append(ptrs, &v) // 危险!
5}
6
7// 正确:创建新变量
8var ptrs []*int
9for _, v := range []int{1, 2, 3} {
10 v := v // 创建新变量
11 ptrs = append(ptrs, &v)
12}
💡 技巧:Go 1.22+ 修复了这个问题,循环变量会自动创建新实例,这个历史遗留的大坑终于填上了。
break 和 continue
1// break:跳出循环
2for i := 0; i < 10; i++ {
3 if i == 5 {
4 break
5 }
6 fmt.Println(i)
7}
8
9// continue:跳过本次迭代
10for i := 0; i < 10; i++ {
11 if i%2 == 0 {
12 continue // 跳过偶数
13 }
14 fmt.Println(i) // 只打印奇数
15}
标签:跳出多层循环
用标签可以跳出外层循环:
1outer:
2for i := 0; i < 3; i++ {
3 for j := 0; j < 3; j++ {
4 if i == 1 && j == 1 {
5 break outer // 跳出外层循环
6 }
7 fmt.Printf("i=%d, j=%d\n", i, j)
8 }
9}
goto:谨慎使用
Go 保留了 goto,但老墨不建议使用,这里的内容了解即可。可以使用 goto 的使用场景是错误处理:
1func processData() error {
2 file, err := openFile()
3 if err != nil {
4 goto cleanup
5 }
6
7 db, err := openDB()
8 if err != nil {
9 goto cleanup
10 }
11
12 return nil
13
14cleanup:
15 if file != nil {
16 file.Close()
17 }
18 if db != nil {
19 db.Close()
20 }
21 return err
22}
但是很明显,defer 更好理解和容易使用。
💡 技巧:现代 Go 代码中,defer 通常是更好的选择。
完整示例
把前面讲的知识点串起来,看个完整的例子:
1package main
2
3import "fmt"
4
5func main() {
6 // 1. if 初始化语句
7 if x := 10; x > 5 {
8 fmt.Println("x is large")
9 }
10
11 // 2. switch 多值 case
12 day := "Mon"
13 switch day {
14 case "Sat", "Sun":
15 fmt.Println("Weekend")
16 case "Mon", "Tue", "Wed", "Thu", "Fri":
17 fmt.Println("Weekday")
18 }
19
20 // 3. 无表达式 switch
21 score := 85
22 switch {
23 case score >= 90:
24 fmt.Println("A")
25 case score >= 80:
26 fmt.Println("B")
27 default:
28 fmt.Println("C")
29 }
30
31 // 4. for 的三种形式
32 // 标准 for
33 for i := 0; i < 3; i++ {
34 fmt.Print(i, " ")
35 }
36 fmt.Println()
37
38 // 当 while 用
39 count := 3
40 for count > 0 {
41 fmt.Print(count, " ")
42 count--
43 }
44 fmt.Println()
45
46 // 5. range 遍历
47 nums := []int{1, 2, 3}
48 for i, v := range nums {
49 fmt.Printf("nums[%d] = %d\n", i, v)
50 }
51
52 // 6. 标签跳出多层循环
53outer:
54 for i := 0; i < 3; i++ {
55 for j := 0; j < 3; j++ {
56 if i == 1 && j == 1 {
57 break outer
58 }
59 fmt.Printf("i=%d, j=%d\n", i, j)
60 }
61 }
62}
这个例子展示了:
- if 初始化语句
- switch 多值 case 和无表达式 switch
- for 的三种形式
- range 遍历
- 标签跳出多层循环
老墨总结
Go 控制结构的几个亮点:
- 一个 for 打天下:for 可以当 while 用,可以当无限循环用,还能用 range 遍历
- switch 不用 break:匹配到一个 case 后自动停止,代码更简洁
- if 初始化语句:限制变量作用域,让代码更清晰
- 无表达式 switch:替代复杂 if-else 链,可读性更好
- 标签跳出多层循环:避免复杂的标志变量
实战建议:
- 优先使用 range 遍历集合
- 注意 range 的值拷贝问题,修改元素时用索引
- 用无表达式 switch 替代复杂 if-else
- 跳出多层循环时用标签,不要用标志变量
- 避免滥用 goto,defer 通常是更好的选择
Go 的控制结构设计哲学是"少即是多",一个 for 搞定所有循环,让代码逻辑更清晰。
你在用 Go 时,最喜欢哪个控制结构特性?switch 不用 break 还是 for 当 while 用?欢迎评论区讨论!
极客老墨,继续折腾!
练习题
- 编写一个函数,使用无表达式 switch 判断分数等级(90以上A,80以上B,70以上C,60以上D,否则F)
- 使用 for 循环打印 1 到 100 中所有 3 的倍数(提示:用 continue 跳过非 3 的倍数)
- 编写一个函数,在二维切片中查找特定值,找到后立即返回位置(提示:使用标签和 break)
- 使用类型 switch 编写一个函数,根据不同类型打印不同信息(支持 int、string、bool、float64)
- 演示 range 的值拷贝陷阱,并给出正确的修改方式
- 使用 goto 实现一个简单的状态机(有 3 个状态:start、processing、done)
快来评论区秀出你的代码,大家一起讨论!
极客老墨,继续折腾!
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/06-control。