Go 控制流:让程序学会做选择

学习 Go 语言的 if/else、switch、for 循环等控制结构,掌握程序逻辑的基础

Go 控制流:让程序学会做选择

到目前为止,我们写的程序都是"一条路走到黑"——从第一行开始,一行一行地往下执行,直到结束。但现实世界中的问题往往不是这么简单的。

想想你每天早上的决策过程:如果下雨了,就带伞;如果没下雨,就不带。如果今天是工作日,就早起;如果是周末,就多睡一会儿。

程序也需要这样的"判断力"。这就是控制流(control flow)要做的事——让程序能够根据条件执行不同的代码,或者重复执行某段代码。

Go 语言在控制流方面的设计非常简洁。和很多语言不同,Go 只有 for 一种循环结构,没有 whiledo-while。这让语言变得更简单,但你可能会担心:够用吗?

放心,够用,而且非常好用。让我们一个一个来看。

if/else 条件判断

基本的 if

if 是最简单的条件判断语句:如果条件为真,就执行某段代码。

age := 20

if age >= 18 {
	fmt.Println("你已经成年了")
}

注意几个细节:

  1. 条件不需要用括号包起来。在 C、Java、JavaScript 中,你需要写 if (age >= 18),但 Go 不需要。
  2. 花括号是必须的。即使只有一行代码,也不能省略花括号。Go 的设计者认为这样可以避免一些常见的 bug。
  3. 左花括号 { 必须在 if 的同一行。这是 Go 的强制格式要求。

if-else

如果需要处理条件不满足的情况,加上 else

age := 15

if age >= 18 {
	fmt.Println("你可以考驾照了")
} else {
	fmt.Println("你还不能考驾照")
}

if-else if-else

多个条件的情况下,可以用 else if

score := 85

if score >= 90 {
	fmt.Println("优秀")
} else if score >= 80 {
	fmt.Println("良好")
} else if score >= 60 {
	fmt.Println("及格")
} else {
	fmt.Println("不及格")
}
// 输出:良好

if 的初始化语句

这是 Go 语言的一个独特特性:你可以在 if 语句中添加一个初始化语句,用分号和条件隔开。

if score := 85; score >= 90 {
	fmt.Println("优秀")
} else if score >= 80 {
	fmt.Println("良好")
}

在初始化语句中声明的变量(比如上面的 score),只在 if-else 的作用域内有效。一旦 if-else 执行完毕,这个变量就不存在了。

这个特性在实际开发中非常实用,特别是处理函数返回值的时候:

if err := doSomething(); err != nil {
	fmt.Println("出错了:", err)
}
// err 在这里已经不存在了

你不需要在外面单独声明一个 err 变量,代码更加整洁。

一个完整的例子

让我们写一个小程序,根据当前时间打印不同的问候语:

package main

import (
	"fmt"
	"time"
)

func main() {
	hour := time.Now().Hour()

	if hour < 6 {
		fmt.Println("凌晨了,快去睡觉!")
	} else if hour < 12 {
		fmt.Println("早上好!新的一天开始了")
	} else if hour < 14 {
		fmt.Println("中午好!该吃午饭了")
	} else if hour < 18 {
		fmt.Println("下午好!继续加油")
	} else if hour < 22 {
		fmt.Println("晚上好!放松一下吧")
	} else {
		fmt.Println("夜深了,早点休息")
	}
}

switch 多分支选择

当你有很多个条件需要判断时,写一长串 if-else if 会很丑陋。这时候 switch 就派上用场了。

基本的 switch

day := "Monday"

switch day {
case "Monday":
	fmt.Println("星期一,新的一周开始了")
case "Tuesday":
	fmt.Println("星期二")
case "Wednesday":
	fmt.Println("星期三")
case "Thursday":
	fmt.Println("星期四")
case "Friday":
	fmt.Println("星期五,快到周末了!")
case "Saturday", "Sunday":
	fmt.Println("周末!开心!")
default:
	fmt.Println("未知的一天")
}

注意 Go 的 switch 和其他语言的一个重要区别:Go 的 case 默认自带 break,不需要手动写。在 C 和 Java 中,每个 case 后面都需要写 break,否则会继续执行下一个 case(fall through)。Go 把这个反直觉的行为去掉了。

带表达式的 switch

if 一样,switch 也支持初始化语句:

switch hour := time.Now().Hour(); {
case hour < 12:
	fmt.Println("上午")
case hour < 18:
	fmt.Println("下午")
default:
	fmt.Println("晚上")
}

无条件 switch

switch 后面可以什么都不写,这时候它等同于 switch true,可以用来替代一长串 if-else if

age := 25

switch {
case age < 13:
	fmt.Println("儿童")
case age < 18:
	fmt.Println("青少年")
case age < 30:
	fmt.Println("青年")
case age < 60:
	fmt.Println("中年")
default:
	fmt.Println("老年")
}
// 输出:青年

fallthrough

虽然 Go 默认不会 fall through,但如果你确实需要这种行为,可以用 fallthrough 关键字:

num := 5

switch num {
case 5:
	fmt.Println("num 是 5")
	fallthrough
case 10:
	fmt.Println("这行也会执行")
	fallthrough
case 15:
	fmt.Println("这行也会执行")
default:
	fmt.Println("结束")
}

// 输出:
// num 是 5
// 这行也会执行
// 这行也会执行

⚠️ 踩坑提示fallthrough 必须是 case 的最后一条语句,它后面不能再有其他语句。而且 fallthrough 是无条件跳转到下一个 case,不会再判断下一个 case 的条件。

说实话,在实际开发中,fallthrough 很少用到。如果你发现自己需要用 fallthrough,先想想是不是有更简洁的写法。

type switch

switch 还可以用来判断变量的类型,这在后面学接口的时候会用到,先了解个大概:

func checkType(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("是整数:%d\n", v)
	case string:
		fmt.Printf("是字符串:%s\n", v)
	case bool:
		fmt.Printf("是布尔值:%v\n", v)
	default:
		fmt.Printf("未知类型:%T\n", v)
	}
}

checkType(42)        // 是整数:42
checkType("hello")   // 是字符串:hello
checkType(true)      // 是布尔值:true

for 循环

终于到了循环。Go 语言只有 for 一种循环结构,没有 whiledo-while。但是别担心,Go 的 for 非常灵活,可以覆盖所有循环场景。

基本的 for 循环

Go 的 for 循环由三部分组成:初始化语句、条件表达式、后置语句,中间用分号隔开。

for i := 0; i < 5; i++ {
	fmt.Println("第", i+1, "次循环")
}

// 输出:
// 第 1 次循环
// 第 2 次循环
// 第 3 次循环
// 第 4 次循环
// 第 5 次循环

if 一样,条件表达式不需要括号。

只有条件的 for(相当于 while)

如果你省略初始化语句和后置语句,for 就变成了 while

n := 10

for n > 0 {
	fmt.Println(n)
	n--
}
fmt.Println("发射!")

这就是为什么 Go 不需要 while——for 已经涵盖了它的功能。

无限循环

如果你连条件都省略,就得到了一个无限循环:

for {
	fmt.Println("这个循环永远不会停止...")
	// 记得加个 break 或者 return,否则真的不会停
}

无限循环在服务端编程中很常见,比如一个服务器需要一直运行,等待客户端的连接。

用 break 退出循环

break 可以提前终止循环:

for i := 0; i < 100; i++ {
	if i == 5 {
		break  // 当 i 等于 5 时退出循环
	}
	fmt.Println(i)
}

// 输出:0 1 2 3 4

用 continue 跳过当前迭代

continue 跳过当前这次迭代,直接进入下一次:

for i := 0; i < 10; i++ {
	if i%2 == 0 {
		continue  // 跳过偶数
	}
	fmt.Println(i)
}

// 输出:1 3 5 7 9

range 遍历

range 是 Go 语言中遍历集合数据的利器。它可以用于字符串、数组、切片、map 等。

遍历字符串:

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

// 输出:
// 索引 0: H
// 索引 1: e
// 索引 2: l
// 索引 3: l
// 索引 4: o

注意 range 遍历字符串时,返回的是 Unicode 字符(rune),而不是字节。这对于处理中文等非 ASCII 字符特别有用:

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

// 输出:
// 索引 0: 你
// 索引 3: 好
// 索引 6: 世
// 索引 9: 界

注意索引的跳跃——因为每个中文字符占 3 个字节。

遍历数组/切片:

fruits := []string{"苹果", "香蕉", "橙子", "葡萄"}

for i, fruit := range fruits {
	fmt.Printf("%d: %s\n", i, fruit)
}

// 输出:
// 0: 苹果
// 1: 香蕉
// 2: 橙子
// 3: 葡萄

如果你不需要索引,可以用 _ 忽略:

for _, fruit := range fruits {
	fmt.Println(fruit)
}

如果你只需要索引,可以省略第二个变量:

for i := range fruits {
	fmt.Println(i)
}

遍历 map:

scores := map[string]int{
	"张三": 85,
	"李四": 92,
	"王五": 78,
}

for name, score := range scores {
	fmt.Printf("%s 的成绩是 %d\n", name, score)
}

⚠️ 注意:map 的遍历顺序是随机的!每次运行结果可能不同。如果你需要按特定顺序遍历,需要先把 key 取出来排序。

goto:谨慎使用

Go 语言保留了 goto 语句,但大多数情况下不建议使用。goto 会让代码的控制流变得难以追踪,产生所谓的"面条代码"。

不过在某些特定场景下(比如跳出多层嵌套循环),goto 可以让代码更简洁:

package main

import "fmt"

func main() {
	// 在二维数组中查找某个值
	matrix := [][]int{
		{1, 2, 3},
		{4, 5, 6},
		{7, 8, 9},
	}

	target := 5

	for i := 0; i < len(matrix); i++ {
		for j := 0; j < len(matrix[i]); j++ {
			if matrix[i][j] == target {
				fmt.Printf("找到了!位置是 [%d][%d]\n", i, j)
				goto found
			}
		}
	}
	fmt.Println("没找到")
	return

found:
	fmt.Println("查找完毕")
}

在这个例子中,如果用 break 只能跳出一层循环,要实现同样的效果就需要额外的标志变量。goto 在这里反而是最简洁的方案。

但请记住:能用 breakcontinuereturn 解决的问题,就不要用 goto

标签(Label)

Go 支持给循环加标签,配合 breakcontinue 可以控制外层循环:

package main

import "fmt"

func main() {
outer:
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if i == 1 && j == 1 {
				fmt.Println("跳出外层循环!")
				break outer  // 跳出标记为 outer 的循环
			}
			fmt.Printf("i=%d, j=%d\n", i, j)
		}
	}
}

// 输出:
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// 跳出外层循环!

这比 goto 更结构化,是处理多层循环退出的推荐方式。

实战:猜数字游戏

让我们用今天学到的控制流知识,写一个完整的猜数字游戏:

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	// 生成随机数
	rand.Seed(time.Now().UnixNano())
	secret := rand.Intn(100) + 1  // 1 到 100 之间

	fmt.Println("=== 猜数字游戏 ===")
	fmt.Println("我已经想好了 1 到 100 之间的一个数字")
	fmt.Println("你有 7 次机会来猜它")

	maxAttempts := 7

	for attempt := 1; attempt <= maxAttempts; attempt++ {
		fmt.Printf("\n第 %d 次猜测,请输入一个数字: ", attempt)

		var guess int
		fmt.Scan(&guess)

		// 检查输入是否合法
		if guess < 1 || guess > 100 {
			fmt.Println("请输入 1 到 100 之间的数字!")
			attempt--  // 不合法输入不算次数
			continue
		}

		// 判断
		switch {
		case guess == secret:
			fmt.Printf("🎉 恭喜你!答案就是 %d,你用了 %d 次就猜到了!\n", secret, attempt)
			return
		case guess < secret:
			fmt.Println("太小了!再大一点")
		case guess > secret:
			fmt.Println("太大了!再小一点")
		}

		// 提示剩余次数
		remaining := maxAttempts - attempt
		if remaining > 0 {
			fmt.Printf("你还有 %d 次机会\n", remaining)
		}
	}

	fmt.Printf("\n😢 很遗憾,次数用完了!答案是 %d\n", secret)
}

这个程序用到了:

  • for 循环控制游戏次数
  • if 判断输入是否合法
  • switch 判断猜测结果
  • continue 跳过无效输入
  • return 提前结束程序

嵌套循环的小技巧

嵌套循环是编程中经常用到的模式。这里分享一个经典的例子——打印九九乘法表:

package main

import "fmt"

func main() {
	for i := 1; i <= 9; i++ {
		for j := 1; j <= i; j++ {
			fmt.Printf("%d×%d=%-4d", j, i, i*j)
		}
		fmt.Println()  // 换行
	}
}

输出结果:

1×1=1
1×2=2   2×2=4
1×3=3   2×3=6   3×3=9
1×4=4   2×4=8   3×4=12  4×4=16
1×5=5   2×5=10  3×5=15  4×5=20  5×5=25
...

%-4d 是一个格式化技巧:- 表示左对齐,4 表示占 4 个字符宽度,这样输出会更整齐。

小结

今天我们学习了 Go 语言的控制流,主要内容包括:

  1. if/else:条件判断,支持初始化语句,Go 要求条件不加括号、花括号不能省略
  2. switch:多分支选择,默认自带 break,支持无条件 switch 和 type switch
  3. for:Go 唯一的循环结构,可以当 while 用,也可以做无限循环
  4. range:遍历字符串、数组、切片、map 的利器
  5. break/continue:控制循环的执行流程
  6. goto 和标签:用于特殊场景的控制流跳转,谨慎使用

Go 的控制流设计追求简洁统一。只有 for 一种循环、switch 默认 break,这些设计决策减少了你需要记住的规则数量,也减少了出错的可能性。

练习时间

  1. FizzBuzz:打印 1 到 100,但如果数字是 3 的倍数打印 “Fizz”,5 的倍数打印 “Buzz”,同时是 3 和 5 的倍数打印 “FizzBuzz”
  2. 素数判断:写一个程序判断一个数是不是素数(只能被 1 和自身整除的数)
  3. 斐波那契数列:用循环打印前 20 个斐波那契数(1, 1, 2, 3, 5, 8, 13…)
  4. 统计字符:给定一个字符串,统计其中字母、数字、空格和其他字符的数量
  5. 冒泡排序:用嵌套循环实现冒泡排序算法

下一篇预告

在下一篇文章中,我们将学习 Go 语言的函数。函数是程序的基本构建单元,你会学到:

  • 函数的定义和调用
  • 多返回值
  • 命名返回值
  • 可变参数
  • 匿名函数和闭包
  • defer 语句

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页