
前面我们已经学习了 Go 的变量、常量、数据类型以及输入输出。接下来开始学习 Go 程序如何根据不同条件执行不同代码:条件控制。
很多初学者会把条件控制理解成 if 和 else,但 Go 的控制结构还包括:
if:根据布尔表达式选择分支;for:重复执行代码,Go 没有单独的while;switch:进行多路分支;break、continue:改变循环和分支的执行流程;- select:等待多个 channel 通信操作;
defer:把函数调用推迟到当前函数返回前执行。
按照 Go 官方语言规范 的定义,if 语句根据布尔表达式的结果选择执行分支。Go 的控制结构和 C、Java 有些相似,但有几个非常明显的特点:条件表达式不需要括号,代码块的大括号必须存在,只有一种 for 循环,并且 switch 默认不会自动贯穿执行下一个 case。
本章按照"概念讲解 → 实际代码 → 运行结果 → 官方规则 → 常见坑点"的顺序展开。
Go 条件控制的共同语法特点
Go 的 if、for、switch 和 select 都不需要在条件外面包圆括号:
Go
if age >= 18 {
fmt.Println("成年人")
}
下面的写法是错误的:
Go
// if (age >= 18) { } // Go 不需要,也不推荐这样写
大括号必须和控制结构写在同一行:
Go
if age >= 18 {
fmt.Println("成年人")
}
不能把左大括号单独放到下一行,因为 Go 的分号自动插入规则会导致语法错误。官方 Effective Go 也明确建议控制结构的大括号紧跟在条件后面。
if 条件判断
最基本的 if 语句如下:
Go
package main
import "fmt"
func main() {
age := 20
if age >= 18 {
fmt.Println("已经成年")
}
}
运行结果:
Go
已经成年
if 后面的表达式必须是布尔表达式。Go 不允许把整数当成真假值:
Go
// if 1 { } // 编译错误:non-boolean condition in if statement
这和一些允许 0 表示 false、非零值表示 true 的语言不同【说的就是 C++ 和其他相关的语言】。Go 要求条件表达式明确返回 bool。
if ... else
当条件不满足时,可以使用 else:
Go
package main
import "fmt"
func main() {
score := 82
if score >= 60 {
fmt.Println("考试通过")
} else {
fmt.Println("需要补考")
}
}
运行结果:
Go
考试通过
else 必须和前一个 if 的右大括号放在同一条语句结构中:
Go
if score >= 60 {
fmt.Println("通过")
} else {
fmt.Println("不通过")
}
多分支 else if
Go
package main
import "fmt"
func main() {
score := 86
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
}
运行结果:
Go
良好
条件会从上到下依次判断,命中第一个为 true 的分支后,后面的分支不会继续执行。
if 中的初始化语句
Go 的 if 可以在条件前面添加一个简单语句,语句和条件之间使用分号分隔:【常用 - 搭配 err】
Go
package main
import "fmt"
func main() {
if value := 10 * 2; value > 15 {
fmt.Println("value 大于 15:", value)
}
}
运行结果:
Go
value 大于 15: 20
这里的 value 只在 if、else if 和 else 对应的代码块中有效:
Go
if value := 10 * 2; value > 15 {
fmt.Println(value)
}
// fmt.Println(value) // 编译错误:value 只在 if 语句内有效
这种写法在错误处理中特别常见:
Go
if err := doSomething(); err != nil {
return err
}
它把临时变量的作用域限制在当前判断中,避免变量污染外层函数。
条件表达式的组合
Go 使用 &&、|| 和 ! 组合布尔条件:
Go
package main
import "fmt"
func main() {
age := 20
hasTicket := true
isAdult := age >= 18
if isAdult && hasTicket {
fmt.Println("可以进入")
}
if age < 12 || age > 60 {
fmt.Println("符合优惠条件")
}
if !hasTicket {
fmt.Println("没有票")
}
}
运行结果:
Go
可以进入
&& 和 || 具有短路行为:【这个和 C++ 是一样的】
Go
if value != nil && value.IsValid() {
fmt.Println("value 有效")
}
当左侧已经能够决定结果时,右侧表达式不会执行。利用这个规则可以先判断指针或接口是否为 nil,再安全访问它的方法。
Go 只有一种 for 循环
Go 没有单独的 while 和 do...while,所有循环都使用 for。官方 Tour 也把 for 称为 Go 唯一的循环结构。
传统三段式 for
Go
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
运行结果:
Go
0
1
2
3
4
三部分分别是:
- 初始化语句:只执行一次;
- 条件表达式:每次循环开始前判断;
- 后置语句:每次循环体结束后执行。
注意,Go 的 for 语句也不需要括号:
Go
for i := 0; i < 5; i++ {
fmt.Println(i)
}
类似 while 的写法
省略初始化和后置语句,只保留条件:
Go
package main
import "fmt"
func main() {
count := 3
for count > 0 {
fmt.Println("倒计时:", count)
count--
}
}
运行结果:
Go
倒计时: 3
倒计时: 2
倒计时: 1
这就是 Go 中实现"当条件成立时循环"的方式。
无限循环
三个部分全部省略,就得到无限循环:
Go
for {
fmt.Println("持续执行")
}
无限循环通常需要配合 break、return 或 channel 通信,否则程序不会自然结束。
range 遍历集合
for range 是 Go 中遍历数组、切片、字符串、映射和 channel 的常见方式。
遍历切片
Go
package main
import "fmt"
func main() {
names := []string{"Go", "Rust", "C++"}
for index, name := range names {
fmt.Println(index, name)
}
}
运行结果:
Go
0 Go
1 Rust
2 C++
如果只需要元素,不需要下标,可以使用空白标识符:
Go
for _, name := range names {
fmt.Println(name)
}
如果只需要下标:
Go
for index := range names {
fmt.Println(index)
}
遍历映射
Go
scores := map[string]int{
"Go": 95,
"Rust": 90,
}
for language, score := range scores {
fmt.Println(language, score)
}
映射的遍历顺序没有保证,因此不要依赖输出顺序。不同运行过程可能得到不同顺序。
遍历字符串
Go
package main
import "fmt"
func main() {
for index, char := range "你好 Go" {
fmt.Printf("索引=%d 字符=%c\n", index, char)
}
}
运行结果:
Go
索引=0 字符=你
索引=3 字符=好
索引=6 字符=
索引=7 字符=G
索引=8 字符=o
遍历字符串时,第二个值是 rune,索引是 UTF-8 字节位置,不是简单的字符序号。这也是为什么中文字符的索引会跳过多个字节。
break 终止循环
break 会结束当前最近 的 for、switch 或 select:
Go
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
if i == 3 {
break
}
fmt.Println(i)
}
}
运行结果:
Go
0
1
2
在 switch 中,Go 默认执行完一个 case 后就离开 switch,因此通常不需要像 C 语言那样手动写 break。【核心差异点】
continue 跳过本轮循环
continue 会跳过当前循环剩余代码,直接进入下一轮:
Go
package main
import "fmt"
func main() {
for i := 1; i <= 5; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i)
}
}
运行结果:
Go
1
3
5
continue 只适用于循环,不能单独用于普通的 if 代码块。
标签和嵌套循环
嵌套循环中,普通 break 只会结束最近的一层循环。需要跳出外层循环时,可以使用标签:
Go
package main
import "fmt"
func main() {
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break Outer
}
fmt.Println(i, j)
}
}
}
运行结果:
bash
0 0
0 1
0 2
1 0
标签必须指向一个包围当前语句的 for、switch 或 select **。**标签应该只在确实能让控制流程更清楚时使用,过多标签会降低代码可读性。
switch 多路分支
switch 适合对一个值进行多路匹配:
Go
package main
import "fmt"
func main() {
day := 2
switch day {
case 1:
fmt.Println("星期一")
case 2:
fmt.Println("星期二")
case 3:
fmt.Println("星期三")
default:
fmt.Println("其他日期")
}
}
运行结果:
Go
星期二
Go 的 switch 有几个重要规则:
- case 从上到下依次判断;
- 命中第一个 case 后执行对应代码;
- 一个 case 执行完成后默认退出 switch;
- 可以使用逗号写多个匹配值;
- 可以使用
default处理没有匹配的情况。
Go
switch level {
case 1, 2, 3:
fmt.Println("初级")
case 4, 5:
fmt.Println("中级")
default:
fmt.Println("其他级别")
}
无表达式 switch
省略 switch 后面的表达式,就相当于 switch true,适合替代较长的 if else:
Go
package main
import "fmt"
func main() {
score := 86
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
}
运行结果:
Go
良好
switch 中的初始化语句
和 if 一样,switch 也可以先执行一个简单语句:
Go
switch value := 10 * 2; {
case value > 15:
fmt.Println("较大")
default:
fmt.Println("较小")
}
value 只在这个 switch 语句内部有效。
fallthrough
Go 的 switch 默认不会自动执行下一个 case。如果确实需要继续执行下一个 case,可以显式使用 fallthrough:
Go
package main
import "fmt"
func main() {
value := 1
switch value {
case 1:
fmt.Println("case 1")
fallthrough
case 2:
fmt.Println("case 2")
default:
fmt.Println("default")
}
}
运行结果:
Go
case 1
case 2
fallthrough 必须是当前 case 的最后一条语句,并且不能用于 type switch。实际业务中应谨慎使用,因为它会改变 Go 默认的"命中一个 case 就结束"的直觉。
类型 switch
如果一个接口变量中可能保存不同具体类型,可以使用 type switch:
Go
package main
import "fmt"
func describe(value any) {
switch v := value.(type) {
case string:
fmt.Println("字符串:", v)
case int:
fmt.Println("整数:", v)
case bool:
fmt.Println("布尔值:", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("未知类型: %T\n", v)
}
}
func main() {
describe("Go")
describe(100)
describe(true)
}
运行结果:
Go
字符串: Go
整数: 100
布尔值: true
type switch 的变量 v 在每一个 case 中会拥有对应类型。它和普通 switch 比较的是值不同,type switch 比较的是接口中动态值的类型。
Go 专门设计了 .(xxx) 这种类型断言语法,用括号区分:
.(int)→ 断言成 int(普通断言).(type)→ type switch 专用,获取动态类型
.(T)是语法符号 ,不是结构体字段访问!所以不能用.直接点 type。
select:等待 channel 通信
select 可以理解为"针对 channel 操作的 switch"。它等待多个通信 case,哪个 case 可以执行就选择哪个。
Go
package main
import "fmt"
func main() {
messages := make(chan string, 1)
messages <- "任务完成"
select {
case message := <-messages:
fmt.Println(message)
default:
fmt.Println("暂时没有消息")
}
}
运行结果:
Go
任务完成
如果多个 case 同时可执行,Go 会从可执行 case 中进行选择;如果没有 case 可执行且存在 default,就立即执行 default;如果没有 default,select 会阻塞等待。
Go
select {
case message := <-messages:
fmt.Println(message)
case <-time.After(time.Second):
fmt.Println("等待超时")
}
在实际并发程序中,select 常用于超时、取消、退出通知和同时等待多个 channel。
defer:延迟执行
defer 不是条件分支或循环,但属于 Go 控制流程中非常重要的语句。它会把函数调用推迟到当前函数即将返回时执行。
Go
package main
import "fmt"
func main() {
defer fmt.Println("最后执行")
fmt.Println("先执行")
}
运行结果:
Go
先执行
最后执行
多个 defer 按后进先出顺序执行:
Go
package main
import "fmt"
func main() {
defer fmt.Println("第一个 defer")
defer fmt.Println("第二个 defer")
fmt.Println("函数主体")
}
运行结果:
Go
函数主体
第二个 defer
第一个 defer
文件关闭、互斥锁解锁和资源清理通常使用 defer:
Go
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close()
需要注意,defer 保存的是调用行为和当时的参数值,不是"重新读取未来变量"的指令:
Go
func main() {
value := 1
defer fmt.Println(value)
value = 2
}
输出是 1,因为 defer 注册时已经计算了参数。
常见错误和避坑提醒
误区一:在 if 条件中写括号
Go 不要求也不鼓励这样写:
Go
// if (score >= 60) { }
直接写成:
Go
if score >= 60 {
}
误区二:忘记 if 条件必须是 bool
Go
// if score { } // 编译错误
应该明确写出比较表达式:
Go
if score > 0 {
}
误区三:以为 Go 有 while
Go 没有 while 关键字。下面是错误写法:
Go
// while count > 0 { }
应该使用:
Go
for count > 0 {
count--
}
误区四:以为 switch 会自动贯穿
Go 的 switch 命中一个 case 后默认结束,不会自动执行下一个 case。只有显式写 fallthrough 才会继续。
误区五:range 变量就是原集合元素
Go
numbers := []int{1, 2, 3}
for _, value := range numbers {
value = value * 10
}
这只修改了循环变量副本,不会修改切片。需要修改原切片时应该使用下标:
Go
for index := range numbers {
numbers[index] *= 10
}
误区六:忽略 map 遍历顺序
map 的 range 顺序没有保证。需要稳定展示时,应先取出 key、排序,再按排序后的 key 访问。
误区七:break 没有跳出想要的循环
嵌套循环里的普通 break 只跳出最近一层。需要跳出外层时,可以使用标签,或者把逻辑抽到函数中用 return 结束。
一份可以直接复制的综合示例
最后把 if、for、switch、continue 和 defer 放在一个小程序中:
control_demo.go
Go
package main
import "fmt"
func classify(score int) string {
switch {
case score >= 90:
return "优秀"
case score >= 60:
return "及格"
default:
return "不及格"
}
}
func main() {
defer fmt.Println("程序结束")
scores := []int{95, 58, 82}
total := 0
for index, score := range scores {
if score < 60 {
fmt.Printf("第 %d 位同学需要补考\\n", index+1)
continue
}
total += score
fmt.Printf("第 %d 位: %s\\n", index+1, classify(score))
}
if total > 0 {
fmt.Println("及格成绩总和:", total)
}
}
运行结果:
Go
第 1 位: 优秀
第 2 位同学需要补考
第 3 位: 及格
及格成绩总和: 177
程序结束
代码执行顺序是:
- 注册 defer;
- for range 依次遍历成绩;
- 分数低于 60 时使用 continue 跳过本轮;
- 其他分数交给无表达式 switch 分类;
- 循环结束后执行 if;
- main 返回前执行 defer。
总结
本章我们学习了 Go 条件控制的完整基础:
if根据布尔表达式选择分支;if可以带初始化语句,临时变量只在语句内部有效;- Go 只有一种
for循环,可以写成三段式、条件式和无限循环; range可以遍历数组、切片、字符串、map 和 channel;break结束当前循环,continue跳过当前轮次;switch默认不会自动 fallthrough,无表达式 switch 等价于 switch true;- type switch 用于判断接口动态类型;
select用于等待多个 channel 通信;defer把清理动作推迟到当前函数返回前执行;- 标签、fallthrough 和 defer 都应在能提升可读性的场景中使用。
真正理解 Go 条件控制之后,就不会把它简单看成一串 if else。更准确的理解是:if 负责二路判断,for 负责重复执行,switch 负责多路选择,select 负责并发通信选择,而 break、continue、defer 负责控制执行流程的边界。
官方资料: