GO [ 条件/循环控制 ]

前面我们已经学习了 Go 的变量、常量、数据类型以及输入输出。接下来开始学习 Go 程序如何根据不同条件执行不同代码:条件控制

很多初学者会把条件控制理解成 ifelse,但 Go 的控制结构还包括:

  • if:根据布尔表达式选择分支;
  • for:重复执行代码,Go 没有单独的 while
  • switch:进行多路分支;
  • breakcontinue:改变循环和分支的执行流程;
  • select:等待多个 channel 通信操作;
  • defer:把函数调用推迟到当前函数返回前执行。

按照 Go 官方语言规范 的定义,if 语句根据布尔表达式的结果选择执行分支。Go 的控制结构和 C、Java 有些相似,但有几个非常明显的特点:条件表达式不需要括号,代码块的大括号必须存在,只有一种 for 循环,并且 switch 默认不会自动贯穿执行下一个 case。

本章按照"概念讲解 → 实际代码 → 运行结果 → 官方规则 → 常见坑点"的顺序展开。

Go 条件控制的共同语法特点

Go 的 ifforswitchselect 都不需要在条件外面包圆括号:

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 只在 ifelse ifelse 对应的代码块中有效:

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 没有单独的 whiledo...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("持续执行")
}

无限循环通常需要配合 breakreturn 或 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 会结束当前最近forswitchselect

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
程序结束

代码执行顺序是:

  1. 注册 defer;
  2. for range 依次遍历成绩;
  3. 分数低于 60 时使用 continue 跳过本轮;
  4. 其他分数交给无表达式 switch 分类;
  5. 循环结束后执行 if;
  6. main 返回前执行 defer。

总结

本章我们学习了 Go 条件控制的完整基础:

  1. if 根据布尔表达式选择分支;
  2. if 可以带初始化语句,临时变量只在语句内部有效;
  3. Go 只有一种 for 循环,可以写成三段式、条件式和无限循环;
  4. range 可以遍历数组、切片、字符串、map 和 channel;
  5. break 结束当前循环,continue 跳过当前轮次;
  6. switch 默认不会自动 fallthrough,无表达式 switch 等价于 switch true;
  7. type switch 用于判断接口动态类型;
  8. select 用于等待多个 channel 通信;
  9. defer 把清理动作推迟到当前函数返回前执行;
  10. 标签、fallthrough 和 defer 都应在能提升可读性的场景中使用。

真正理解 Go 条件控制之后,就不会把它简单看成一串 if else。更准确的理解是:if 负责二路判断,for 负责重复执行,switch 负责多路选择,select 负责并发通信选择,而 break、continue、defer 负责控制执行流程的边界。

官方资料:

相关推荐
茉莉玫瑰花茶1 小时前
GO [ 数据类型 ]
go
云浪21 小时前
Go Context 到底是什么?
后端·go
newerp1 天前
系统调用与 M/P 解绑
后端·程序员·go
newerp1 天前
抢占式调度实现细节
后端·程序员·go
小小龙学IT1 天前
Go 语言 net/http 网络编程深度解析:从 Handler 到生产级 HTTP 服务
go
名字还没想好☜1 天前
Go 实现指数退避重试:context 取消、抖动 jitter 与什么时候别重试
后端·golang·go
我不会起名字3221 天前
一天一道力扣Hot100(37):深度优先算法--括号生成
java·数据结构·c++·后端·python·算法·go
ITKEY_1 天前
macOS搭建Go的开发环境
go
我不会起名字3222 天前
一天一道力扣Hot100(36):深度优先算法---组合总和
数据结构·c++·后端·python·算法·go