大家好,我是极客老墨。

写 Go 之前,我在 Java 里循环都是 for、while、do-while 三件套。转到 Go 发现只有一个 for,心想"这够用吗?"

结果发现 Go 的 for 能当 while 用,能当无限循环用,还能用 range 遍历。更绝的是 switch 默认不用 breakif 还能带初始化语句。

这篇就聊聊 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 控制结构的几个亮点:

  1. 一个 for 打天下:for 可以当 while 用,可以当无限循环用,还能用 range 遍历
  2. switch 不用 break:匹配到一个 case 后自动停止,代码更简洁
  3. if 初始化语句:限制变量作用域,让代码更清晰
  4. 无表达式 switch:替代复杂 if-else 链,可读性更好
  5. 标签跳出多层循环:避免复杂的标志变量

实战建议

  • 优先使用 range 遍历集合
  • 注意 range 的值拷贝问题,修改元素时用索引
  • 用无表达式 switch 替代复杂 if-else
  • 跳出多层循环时用标签,不要用标志变量
  • 避免滥用 goto,defer 通常是更好的选择

Go 的控制结构设计哲学是"少即是多",一个 for 搞定所有循环,让代码逻辑更清晰。


你在用 Go 时,最喜欢哪个控制结构特性?switch 不用 break 还是 for 当 while 用?欢迎评论区讨论!

极客老墨,继续折腾!

练习题

  1. 编写一个函数,使用无表达式 switch 判断分数等级(90以上A,80以上B,70以上C,60以上D,否则F)
  2. 使用 for 循环打印 1 到 100 中所有 3 的倍数(提示:用 continue 跳过非 3 的倍数)
  3. 编写一个函数,在二维切片中查找特定值,找到后立即返回位置(提示:使用标签和 break)
  4. 使用类型 switch 编写一个函数,根据不同类型打印不同信息(支持 int、string、bool、float64)
  5. 演示 range 的值拷贝陷阱,并给出正确的修改方式
  6. 使用 goto 实现一个简单的状态机(有 3 个状态:start、processing、done)

快来评论区秀出你的代码,大家一起讨论!


极客老墨,继续折腾!

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/06-control


相关阅读