Go 控制流:让程序学会做选择
到目前为止,我们写的程序都是"一条路走到黑"——从第一行开始,一行一行地往下执行,直到结束。但现实世界中的问题往往不是这么简单的。
想想你每天早上的决策过程:如果下雨了,就带伞;如果没下雨,就不带。如果今天是工作日,就早起;如果是周末,就多睡一会儿。
程序也需要这样的"判断力"。这就是控制流(control flow)要做的事——让程序能够根据条件执行不同的代码,或者重复执行某段代码。
Go 语言在控制流方面的设计非常简洁。和很多语言不同,Go 只有 for 一种循环结构,没有 while、do-while。这让语言变得更简单,但你可能会担心:够用吗?
放心,够用,而且非常好用。让我们一个一个来看。
if/else 条件判断
基本的 if
if 是最简单的条件判断语句:如果条件为真,就执行某段代码。
age := 20
if age >= 18 {
fmt.Println("你已经成年了")
}
注意几个细节:
- 条件不需要用括号包起来。在 C、Java、JavaScript 中,你需要写
if (age >= 18),但 Go 不需要。 - 花括号是必须的。即使只有一行代码,也不能省略花括号。Go 的设计者认为这样可以避免一些常见的 bug。
- 左花括号
{必须在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 一种循环结构,没有 while、do-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 在这里反而是最简洁的方案。
但请记住:能用 break、continue、return 解决的问题,就不要用 goto。
标签(Label)
Go 支持给循环加标签,配合 break 和 continue 可以控制外层循环:
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 语言的控制流,主要内容包括:
- if/else:条件判断,支持初始化语句,Go 要求条件不加括号、花括号不能省略
- switch:多分支选择,默认自带 break,支持无条件 switch 和 type switch
- for:Go 唯一的循环结构,可以当 while 用,也可以做无限循环
- range:遍历字符串、数组、切片、map 的利器
- break/continue:控制循环的执行流程
- goto 和标签:用于特殊场景的控制流跳转,谨慎使用
Go 的控制流设计追求简洁统一。只有 for 一种循环、switch 默认 break,这些设计决策减少了你需要记住的规则数量,也减少了出错的可能性。
练习时间
- FizzBuzz:打印 1 到 100,但如果数字是 3 的倍数打印 “Fizz”,5 的倍数打印 “Buzz”,同时是 3 和 5 的倍数打印 “FizzBuzz”
- 素数判断:写一个程序判断一个数是不是素数(只能被 1 和自身整除的数)
- 斐波那契数列:用循环打印前 20 个斐波那契数(1, 1, 2, 3, 5, 8, 13…)
- 统计字符:给定一个字符串,统计其中字母、数字、空格和其他字符的数量
- 冒泡排序:用嵌套循环实现冒泡排序算法
下一篇预告
在下一篇文章中,我们将学习 Go 语言的函数。函数是程序的基本构建单元,你会学到:
- 函数的定义和调用
- 多返回值
- 命名返回值
- 可变参数
- 匿名函数和闭包
defer语句
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。