返回首页

Golang入门:1.3 流程控制

在学习一门编程语言时,流程控制都是绕不过去的核心内容。程序之所以“有逻辑”,本质上就是因为它能够根据条件做判断、根据需要重复执行任务、在异常场景下进行兜底处理。

Go 语言在流程控制上看起来不算复杂,但它有几个非常鲜明的特点:语法简洁、约束明确、鼓励可读性优先,同时尽量减少“花哨但难维护”的写法。对于已经有其他语言基础、刚接触 Go 的读者来说,这一章不仅要会写,还要理解 Go 为什么这么设计。

本文会依次讲解:

  • 条件语句:if/else、带初始化的 if
  • 循环语句:for 的多种形态
  • switch 语句:表达式 switch、类型 switch
  • deferpanicrecover 机制

为什么要重视流程控制

流程控制决定了程序的执行路径。一个程序即使数据结构设计得再好,如果判断逻辑混乱、循环边界不清、错误处理方式随意,最终也会变得难以维护。

Go 在流程控制上的设计思路可以概括为下面几点:

设计特点 说明
语法统一 Go 只有一个循环关键字 for,减少语法分裂
强调简洁 没有三元表达式,鼓励清晰的条件判断
可读性优先 条件表达式不允许省略必要部分,也不鼓励过度嵌套
错误处理显式化 不主张把普通错误处理建立在异常机制上

理解这些设计原则后,再去看具体语法,就更容易明白 Go 的取舍。

一、条件语句:if / else

条件语句用于根据不同条件执行不同逻辑。Go 的 if 写法和很多语言类似,但也有两个明显特点:

  1. 条件表达式外不需要加括号
  2. 条件必须是布尔值,不能把整数、字符串等当成真假值使用

1. 基本 if / else 用法

下面的例子演示了如何根据分数判断考试结果:

package main

import "fmt"

func main() {
	score := 78

	if score >= 90 {
		fmt.Println("成绩等级:优秀")
	} else if score >= 60 {
		fmt.Println("成绩等级:及格")
	} else {
		fmt.Println("成绩等级:不及格")
	}
}

这段代码的执行逻辑非常直接:

  • 如果分数大于等于 90,输出“优秀”
  • 否则继续判断是否大于等于 60
  • 如果也不满足,就进入 else

这里有几个需要注意的点:

  • if 后面不写圆括号,例如 if (score >= 90) 虽然在一些语言里很常见,但 Go 不需要
  • 左花括号 { 必须和条件语句写在同一行
  • else 必须紧跟在上一个代码块后面,不能被自动换行断开

2. 逻辑运算配合 if 使用

实际开发中,一个判断往往不止一个条件。这时通常会结合逻辑运算符一起使用:

  • &&:并且
  • ||:或者
  • !:取反
package main

import "fmt"

func main() {
	age := 20
	hasIDCard := true

	if age >= 18 && hasIDCard {
		fmt.Println("允许进入考场")
	} else {
		fmt.Println("不允许进入考场")
	}
}

这个例子表示:年龄必须满 18 岁,并且持有身份证,才允许进入考场。

3. 带初始化语句的 if

这是 Go 中非常实用的一个写法。你可以在 if 判断之前,先执行一个初始化语句,这个变量只在当前 if/else 语句块中生效。

这种写法的优点是:

  • 缩小变量作用域
  • 避免临时变量污染外层代码
  • 让判断逻辑更集中
package main

import "fmt"

func main() {
	if length := len("Golang"); length > 5 {
		fmt.Println("字符串长度大于 5,长度为:", length)
	} else {
		fmt.Println("字符串长度不大于 5,长度为:", length)
	}
}

这里的 length := len("Golang") 就是初始化语句,后面的 length > 5 是判断条件。

要注意:

  • length 只能在这个 if...else 结构内部使用
  • 离开这个结构后,变量就不可见了

这种写法在处理文件读取、查询结果判断、错误检查时非常常见。

4. if 使用建议

对于初学者来说,if 虽然简单,但很容易写得越来越复杂。可以遵循下面这些建议:

建议 说明
保持条件简单 一个 if 里不要堆太多复杂逻辑
优先处理特殊情况 可以减少层层嵌套
善用初始化语句 让临时变量作用域更清晰
条件必须明确为布尔值 不要沿用其他语言的“隐式真假”习惯

二、循环语句:for 的多种形态

很多语言会把循环分成 forwhiledo...while 等不同形式,而 Go 只保留了一个关键字:for

这看起来像是“语法少了”,实际上是“统一了表达方式”。你只要掌握 for,就基本掌握了 Go 的循环体系。

1. 传统 for 循环

这是最接近 C、Java 风格的写法,通常用于明确知道循环次数的场景。

package main

import "fmt"

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

它由三部分组成:

  • 初始化语句:i := 1
  • 条件表达式:i <= 5
  • 后置操作:i++

执行流程如下:

  1. 先执行初始化语句
  2. 判断条件是否成立
  3. 条件成立则执行循环体
  4. 执行后置操作
  5. 再次判断条件,直到条件不成立为止

2. while 风格的 for

Go 没有 while 关键字,但可以通过只保留条件表达式来实现同样效果。

package main

import "fmt"

func main() {
	n := 1

	for n <= 5 {
		fmt.Println("当前数字:", n)
		n++
	}
}

这段代码等价于很多语言里的:

while (n <= 5) {
    ...
}

这种写法适合“循环次数不确定,但循环条件明确”的场景。

3. 无限循环

如果省略 for 的条件部分,就会形成无限循环。它常用于:

  • 持续监听任务
  • 服务端请求处理
  • 消费消息队列
  • 配合 break 在满足条件时退出
package main

import "fmt"

func main() {
	count := 1

	for {
		fmt.Println("当前计数:", count)
		if count == 3 {
			fmt.Println("达到退出条件,结束循环")
			break
		}
		count++
	}
}

这个例子里,for 本身不会结束,但当 count == 3 时,通过 break 主动退出。

4. break 和 continue

在循环中,breakcontinue 是最常见的两个控制语句:

  • break:立即结束整个循环
  • continue:跳过本次循环剩余代码,进入下一轮
package main

import "fmt"

func main() {
	for i := 1; i <= 5; i++ {
		if i == 3 {
			fmt.Println("跳过数字 3")
			continue
		}

		if i == 5 {
			fmt.Println("遇到数字 5,结束循环")
			break
		}

		fmt.Println("当前数字:", i)
	}
}

运行逻辑如下:

  • i == 3 时,不执行下面的打印数字逻辑,直接进入下一轮
  • i == 5 时,循环直接结束

5. range 循环:遍历数组、切片、字符串、map

虽然本节重点是流程控制,但提到 for 时,不能不说 range。它是 Go 中非常高频的遍历方式。

遍历切片

package main

import "fmt"

func main() {
	nums := []int{10, 20, 30}

	for index, value := range nums {
		fmt.Printf("下标=%d, 值=%d\n", index, value)
	}
}

遍历字符串

package main

import "fmt"

func main() {
	for index, r := range "Go语言" {
		fmt.Printf("位置=%d, 字符=%c\n", index, r)
	}
}

这里遍历的是 Unicode 字符,而不是简单的字节,这一点在处理中文时尤其重要。

遍历 map

package main

import "fmt"

func main() {
	userAges := map[string]int{
		"Alice": 24,
		"Bob":   27,
		"Carol": 22,
	}

	for name, age := range userAges {
		fmt.Printf("%s 的年龄是 %d\n", name, age)
	}
}

需要提醒的是:遍历 map 时,元素输出顺序通常不是固定的。Go 有意不保证 map 的遍历顺序,这也是为了避免开发者错误依赖“偶然顺序”。

6. for 循环总结

写法 示例形式 适用场景
传统循环 for i := 0; i < n; i++ 已知循环次数
while 风格 for condition 条件控制型循环
无限循环 for {} 持续执行直到主动退出
range 遍历 for k, v := range data 遍历集合类型

三、switch 语句

当一个变量需要根据多个分支进行判断时,switch 往往比一连串 if...else if...else 更清晰。

Go 的 switch 比很多语言更灵活,也更安全。它的两个典型特性是:

  1. 默认情况下每个 case 执行完会自动结束,不需要手动写 break
  2. case 后面可以写多个匹配值,也可以不带表达式

1. 表达式 switch

这是最常见的用法,根据某个表达式的值进入不同分支。

package main

import "fmt"

func main() {
	day := 3

	switch day {
	case 1:
		fmt.Println("星期一")
	case 2:
		fmt.Println("星期二")
	case 3:
		fmt.Println("星期三")
	case 4, 5:
		fmt.Println("星期四或星期五")
	default:
		fmt.Println("周末")
	}
}

这里的 case 4, 5 表示多个值共用同一段逻辑。

和很多语言不同,Go 中这个 switch 在匹配到某个分支后,不会自动“贯穿”执行后面的分支,因此一般不需要写 break

2. 不带表达式的 switch

Go 允许你省略 switch 后面的表达式,这样每个 case 都可以写成一个条件判断。它本质上等价于更整洁的 if...else if...else

package main

import "fmt"

func main() {
	score := 85

	switch {
	case score >= 90:
		fmt.Println("等级:优秀")
	case score >= 75:
		fmt.Println("等级:良好")
	case score >= 60:
		fmt.Println("等级:及格")
	default:
		fmt.Println("等级:不及格")
	}
}

这种写法在区间判断中非常自然。

3. 类型 switch

类型 switch 常用于接口值的具体类型判断。在你还没有深入学到接口时,可以先把它理解为:当一个变量可能保存不同类型的值时,可以通过类型 switch 来分别处理。

package main

import "fmt"

func describe(value interface{}) {
	switch v := value.(type) {
	case int:
		fmt.Printf("这是一个 int,值为 %d\n", v)
	case string:
		fmt.Printf("这是一个 string,值为 %s\n", v)
	case bool:
		fmt.Printf("这是一个 bool,值为 %t\n", v)
	default:
		fmt.Printf("未知类型:%T\n", v)
	}
}

func main() {
	describe(100)
	describe("hello go")
	describe(true)
	describe(3.14)
}

这里的关键点是:

  • value 的类型是 interface{},表示它可以接收任意类型的值
  • value.(type) 只能出现在类型 switch
  • 每个 case 对应一种具体类型

类型 switch 在框架开发、通用工具函数、序列化处理等场景中很常见。

4. switch 和 if 的选择建议

场景 更推荐的写法
两三个简单条件判断 if / else
同一个变量对应多个固定值 表达式 switch
区间判断较多 不带表达式的 switch
需要根据动态类型处理 类型 switch

四、defer、panic、recover 机制

这一部分是 Go 流程控制里很有代表性的内容。它们不只是“语法点”,更体现了 Go 对错误处理和程序控制的设计哲学。

在很多语言里,开发者习惯用 try/catch/finally 来处理异常和收尾逻辑。但 Go 没有这套机制,而是提供了:

  • defer:延迟执行,常用于资源释放和收尾动作
  • panic:触发严重错误,导致程序进入异常展开流程
  • recover:在合适位置捕获 panic,避免程序直接崩溃

Go 的核心思想不是“让异常成为常规控制流”,而是:

  • 普通错误,用显式返回值处理
  • 真正不可恢复或不该继续运行的问题,再考虑 panic
  • 必要时在系统边界处用 recover 做保护

这和很多依赖 try/catch 的语言有明显区别。

1. defer:延迟执行

defer 表示把某个语句延迟到当前函数执行结束前再执行。无论函数是正常返回,还是因为 panic 中断,只要已经注册了 defer,它通常都会被执行。

最常见用途包括:

  • 关闭文件
  • 释放锁
  • 记录日志
  • 打印函数退出信息
package main

import "fmt"

func main() {
	fmt.Println("开始执行 main")

	defer fmt.Println("defer:最后执行")

	fmt.Println("处理中...")
	fmt.Println("main 即将结束")
}

执行顺序是:

  1. 输出“开始执行 main”
  2. 注册 defer
  3. 输出“处理中...”
  4. 输出“main 即将结束”
  5. 函数返回前执行 defer

多个 defer 的执行顺序

多个 defer 会按照“后进先出”的顺序执行,也就是栈结构。

package main

import "fmt"

func main() {
	defer fmt.Println("第一个 defer")
	defer fmt.Println("第二个 defer")
	defer fmt.Println("第三个 defer")

	fmt.Println("main 函数执行中")
}

输出顺序会是:

  • main 函数执行中
  • 第三个 defer
  • 第二个 defer
  • 第一个 defer

2. panic:触发严重错误

panic 表示程序遇到了无法继续正常执行的严重问题。触发 panic 后,当前函数会立即停止后续普通语句执行,然后开始向外层函数逐层返回,同时执行已经注册的 defer

package main

import "fmt"

func divide(a, b int) int {
	if b == 0 {
		panic("除数不能为 0")
	}
	return a / b
}

func main() {
	fmt.Println("准备计算")
	result := divide(10, 0)
	fmt.Println("结果:", result)
}

这段代码中,如果除数为 0,就主动触发 panic。此时程序不会继续正常执行 fmt.Println("结果:", result)

需要特别强调:

  • panic 不是拿来替代普通错误处理的
  • 比如“用户输入不合法”“文件不存在”“网络请求失败”这类情况,通常应该返回 error
  • panic 更适合表示编程错误、系统状态损坏、严重不变量被破坏等场景

3. recover:捕获 panic

recover 的作用是拦截 panic,让程序从异常状态中恢复。但它只能在 defer 修饰的函数中生效。

package main

import "fmt"

func safeRun() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("捕获到 panic:", err)
		}
	}()

	fmt.Println("safeRun 开始执行")
	panic("发生了严重错误")
}

func main() {
	safeRun()
	fmt.Println("main 继续执行")
}

这个例子里:

  • safeRun 中发生了 panic
  • 由于提前注册了 defer,并在其中调用了 recover()
  • 所以 panic 被捕获,程序没有直接崩溃
  • main 函数后续仍能继续运行

4. defer + panic + recover 联合示例

下面这个例子更完整地展示三者如何协同工作:

package main

import "fmt"

func testRecover() {
	defer fmt.Println("defer 1:最外层收尾")

	defer func() {
		if err := recover(); err != nil {
			fmt.Println("defer 2:成功恢复 panic,错误信息:", err)
		}
	}()

	defer fmt.Println("defer 3:panic 发生后依然会执行")

	fmt.Println("函数开始执行")
	panic("模拟运行时故障")
}

func main() {
	testRecover()
	fmt.Println("程序恢复后继续向下执行")
}

这个示例体现了几个关键事实:

  • 发生 panic 后,已注册的 defer 仍然会执行
  • defer 执行顺序仍然是后进先出
  • recover 成功后,程序可以回到调用者继续执行

5. 与 try/catch 的对比:为什么 Go 不鼓励把 panic 当常规异常机制

很多初学者在接触 panic/recover 时,会下意识把它类比成其他语言里的 throw/try-catch。这种类比有一定帮助,但不能完全等同。

下面是一个更准确的对比:

对比项 Go 的 panic/recover 常见语言的 try/catch
主要用途 处理严重异常或程序不变量被破坏 常作为通用异常处理机制
日常错误处理 error 返回值为主 可能大量依赖异常抛出
恢复位置 通常在 defer 中恢复 catch 代码块中捕获
设计倾向 不鼓励把异常作为常规流程控制 某些语言中异常更常见

Go 的设计哲学非常明确:普通错误应该被显式处理,而不是隐藏在异常控制流里。

这样设计有几个好处:

  1. 代码阅读者一眼就能看出哪里可能出错
  2. 错误处理路径更加显式,不容易被忽略
  3. 不会因为滥用异常而让控制流变得隐蔽

所以在 Go 中,比较推荐的思路是:

  • 能返回 error,就优先返回 error
  • 只有在真正“程序已经不应该继续执行”的情况下才使用 panic
  • 在服务入口、协程边界、任务调度框架等位置,视情况用 recover 做保护

五、一个综合示例:把流程控制串起来

学习完单独语法后,最好再看一个综合示例,把 ifforswitchdefer 串起来理解。

package main

import "fmt"

func processNumbers(nums []int) {
	defer fmt.Println("处理结束,执行收尾工作")

	for _, num := range nums {
		if num < 0 {
			fmt.Println("跳过负数:", num)
			continue
		}

		switch {
		case num == 0:
			fmt.Println("数字是 0")
		case num%2 == 0:
			fmt.Println(num, "是偶数")
		default:
			fmt.Println(num, "是奇数")
		}
	}
}

func main() {
	nums := []int{1, 2, 0, -3, 5, 8}
	processNumbers(nums)
}

这个例子里:

  • for range 负责遍历数据
  • if 负责过滤负数
  • continue 负责跳过当前项
  • switch 负责分类输出
  • defer 负责函数结束时的统一收尾

这就是流程控制在真实代码中的典型组合方式。

六、结语

Go 的流程控制语法不算多,但非常实用。它的设计重点不在“提供尽可能多的语法糖”,而在“让常见逻辑写得足够直接、稳定、易读”。

回顾一下本文的重点:

  • if / else 用于条件分支,支持带初始化语句的写法
  • for 是 Go 唯一的循环关键字,但可以覆盖传统循环、while 风格和无限循环
  • switch 不仅能判断值,还能做区间判断和类型判断
  • defer 适合做收尾,panic 表示严重异常,recover 用于受控恢复
  • Go 不鼓励把 panic/recover 当成日常错误处理手段,这与很多依赖 try/catch 的语言有本质区别

如果你已经理解了这些内容,那么在阅读和编写 Go 代码时,就能更自然地掌握程序“该怎么走、什么时候停、出错后怎么收尾”。这也是写出清晰 Go 代码的重要一步。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

Golang入门:1.2 基本语法与数据类型

下一篇

Golang入门:1.4 复合数据类型