导航菜单

流程控制

流程控制(Control Flow)

流程控制语句决定了程序的执行顺序。Go 语言的流程控制主要包括条件判断(if/switch)和循环(for),以及 defer 和 goto 等特殊控制语句。

if / else 条件语句

if 语句是 Go 中最基本的条件判断语句。Go 的 if 与 C/Java 的主要区别是:条件表达式不需要小括号,但大括号 {} 是必需的

基本语法

x := 42

if x > 50 {
    fmt.Println("x 大于 50")
} else if x > 30 {
    fmt.Println("x 大于 30")
} else {
    fmt.Println("x 不大于 30")
}
// 输出:x 大于 30

if 的初始化语句

if 语句可以包含一个初始化语句,通常用于错误处理:

// 经典的错误处理模式
if err := doSomething(); err != nil {
    fmt.Printf("操作失败: %v\n", err)
    return
}
fmt.Println("操作成功")
// 也可以声明局部变量
if v := computeValue(); v > threshold {
    fmt.Printf("值 %d 超过阈值\n", v)
}
// v 在此处不可访问
// 错误:条件必须为 bool
// if 1 { }            // 编译错误
// if x = 5 { }       // 编译错误(是赋值,不是比较)

// 正确:条件必须为 bool
if x == 5 { }
if err != nil { }

switch 语句

Go 的 switch 语句比 C/Java 更加灵活和安全:

基本语法

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 与 C/Java 的区别

特性GoC/Java
自动 break✅ 匹配后自动跳出❌ 需要 break,否则 fall-through
条件表达式可以是任意类型仅整数/字符/枚举
case 值可以是任意可比较类型有限类型
多值匹配case "a", "b":不支持

switch 初始化语句

if 类似,switch 也可以包含初始化语句:

switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("macOS")
case "linux":
    fmt.Println("Linux")
case "windows":
    fmt.Println("Windows")
default:
    fmt.Printf("其他系统: %s\n", os)
}

无条件的 switch

switch 可以不带任何值,类似 if-else if-else 链:

score := 85

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

fallthrough

使用 fallthrough 让程序继续执行下一个 case(无条件):

n := 2

switch n {
case 1:
    fmt.Println("一")
    fallthrough
case 2:
    fmt.Println("二")
    fallthrough
case 3:
    fmt.Println("三")
default:
    fmt.Println("其他")
}
// 输出:
// 二
// 三

type switch

type switch 是 Go 特有的语法,用于判断接口值的实际类型:

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

func main() {
    checkType(42)         // 整数: 42
    checkType("hello")    // 字符串: hello
    checkType(3.14)       // 浮点数: 3.140000
    checkType(true)       // 布尔值: true
    checkType([]int{1})   // 未知类型: []int
}

for 循环

for 是 Go 中唯一的循环关键字。Go 没有 whiledo-while,但 for 可以实现所有循环形式。

经典 for 循环

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

类 while 的 for 循环

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

n := 10
for n > 0 {
    fmt.Println(n)
    n--
}

无限循环

省略所有三个部分,就变成无限循环:

for {
    // 无限循环,需要 break 或 return 来退出
    fmt.Println("循环中...")
    break
}

for range 遍历

for range 用于遍历各种数据结构:

// 遍历切片
nums := []int{10, 20, 30}
for index, value := range nums {
    fmt.Printf("索引: %d, 值: %d\n", index, value)
}

// 遍历映射
m := map[string]int{"Alice": 90, "Bob": 85}
for key, value := range m {
    fmt.Printf("键: %s, 值: %d\n", key, value)
}

// 遍历字符串(按 rune 遍历)
s := "Hello世界"
for index, r := range s {
    fmt.Printf("索引: %d, 字符: %c\n", index, r)
}

// 遍历通道
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
    fmt.Println(v)
}
// 只需要值,忽略索引
for _, v := range nums {
    fmt.Println(v)
}

// 只需要索引,忽略值
for i := range nums {
    fmt.Println(i)
}

// 遍历映射时,只需要值
for _, score := range studentScores {
    fmt.Println(score)
}

break 与 continue

break 用于提前终止循环,continue 用于跳过当前迭代:

// break:打印到 5 时停止
for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    fmt.Println(i)  // 输出 0, 1, 2, 3, 4
}

// continue:跳过偶数
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue
    }
    fmt.Println(i)  // 输出 1, 3, 5, 7, 9
}

带标签的 break 和 continue

通过标签可以控制嵌套循环的跳出:

outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer  // 跳出外层循环
        }
        fmt.Printf("(%d, %d)\n", i, j)
    }
}
// 输出:
// (0, 0)
// (0, 1)
// (0, 2)
// (1, 0)

defer 语句

defer(延迟执行)

defer 语句将一个函数调用推迟到当前函数返回之前执行。常用于资源释放、解锁、关闭文件等清理操作,确保即使发生 panic 也能执行清理逻辑。

基本用法

func main() {
    defer fmt.Println("3. 最后执行")
    defer fmt.Println("2. 倒数第二个执行")
    fmt.Println("1. 最先执行")
}
// 输出:
// 1. 最先执行
// 2. 倒数第二个执行
// 3. 最后执行

defer 的执行顺序

多个 defer 语句按照**后进先出(LIFO)**的顺序执行,类似栈的操作:

func example() {
    defer fmt.Println("first defer")
    defer fmt.Println("second defer")
    defer fmt.Println("third defer")

    fmt.Println("function body")
}
// 输出:
// function body
// third defer
// second defer
// first defer

defer 与资源释放

defer 最常见的用途是确保资源被正确释放:

func readFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()  // 确保函数返回时关闭文件

    // 读取文件内容...
    return nil
}

func process() {
    mu.Lock()
    defer mu.Unlock()  // 确保函数返回时解锁

    // 处理逻辑...
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer resp.Body.Close()  // 确保关闭响应体

    // 处理响应...
}

defer 的参数求值时机

defer 语句中的函数参数在 defer 执行时(而非调用时)就已经求值:

func main() {
    x := 10
    defer fmt.Println("defer:", x)  // x 在 defer 声明时就已求值为 10
    x = 20
    fmt.Println("current:", x)
}
// 输出:
// current: 20
// defer: 10
// 错误示范:所有文件在函数结束时才关闭
func processFiles(files []string) {
    for _, f := range files {
        file, _ := os.Open(f)
        defer file.Close()  // 所有 close 延迟到函数返回时执行
        // 处理文件...
    }
}

// 正确做法:将循环体提取为单独的函数
func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()  // 每个文件处理完后立即关闭
    // 处理文件...
    return nil
}

func processFiles(files []string) {
    for _, f := range files {
        processFile(f)
    }
}

goto 语句

goto 语句可以无条件跳转到同一函数内的标签处。Go 保留了 goto不推荐使用

func main() {
    i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }
}
// 错误:goto 跳过了 j 的声明
goto skip
j := 10  // 编译错误:goto skip jumps over declaration of j
skip:
fmt.Println(j)
// goto 用于统一错误处理(旧式风格,不推荐)
func process() error {
    if err := step1(); err != nil {
        goto cleanup
    }
    if err := step2(); err != nil {
        goto cleanup
    }
    return nil
cleanup:
    // 清理资源
    return err
}

// 推荐的现代写法:使用 defer
func process() (err error) {
    defer cleanup()  // 使用 defer 进行清理

    if err = step1(); err != nil {
        return err
    }
    if err = step2(); err != nil {
        return err
    }
    return nil
}

练习题

练习 1:FizzBuzz

编写 FizzBuzz 程序:打印 1 到 30 的数字,但如果是 3 的倍数打印 “Fizz”,5 的倍数打印 “Buzz”,同时是 3 和 5 的倍数打印 “FizzBuzz”。请使用 for 循环和条件判断实现。

参考答案

解题思路:先判断 15 的倍数(即同时是 3 和 5 的倍数),再判断 3 的倍数,最后判断 5 的倍数。

代码

package main

import "fmt"

func main() {
    for i := 1; i <= 30; i++ {
        switch {
        case i%15 == 0:
            fmt.Println("FizzBuzz")
        case i%3 == 0:
            fmt.Println("Fizz")
        case i%5 == 0:
            fmt.Println("Buzz")
        default:
            fmt.Println(i)
        }
    }
}

关键点i%15 == 0 必须放在最前面,因为 15 的倍数同时也是 3 和 5 的倍数。也可以使用 if-else if-else 替代 switch

练习 2:defer 执行顺序

以下代码的输出是什么?请逐步分析执行过程。

package main

import "fmt"

func trace(name string) func() {
    fmt.Printf("进入 %s\n", name)
    return func() {
        fmt.Printf("退出 %s\n", name)
    }
}

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

    defer trace("步骤 A")()
    defer trace("步骤 B")()
    defer trace("步骤 C")()

    fmt.Println("main 结束执行")
}
参考答案

解题思路:理解 defer 的参数求值时机和 LIFO 执行顺序。

输出

进入 main
main 开始执行
进入 步骤 A
进入 步骤 B
进入 步骤 C
main 结束执行
退出 步骤 C
退出 步骤 B
退出 步骤 A
退出 main

分析

  1. defer trace("main")()trace("main") 立即执行(打印”进入 main”),返回的匿名函数被 defer 延迟
  2. fmt.Println("main 开始执行") — 立即执行
  3. defer trace("步骤 A")() — 同上,打印”进入 步骤 A”,返回函数延迟
  4. defer trace("步骤 B")() — 打印”进入 步骤 B”,返回函数延迟
  5. defer trace("步骤 C")() — 打印”进入 步骤 C”,返回函数延迟
  6. fmt.Println("main 结束执行") — 立即执行
  7. 函数返回,按 LIFO 顺序执行 defer:退出 C → 退出 B → 退出 A → 退出 main

练习 3:for range 遍历字符串

以下代码的输出是什么?解释 for range 遍历字符串时索引和值的含义。

package main

import "fmt"

func main() {
    s := "Go语言"

    fmt.Println("len(s) =", len(s))

    for i, r := range s {
        fmt.Printf("i=%d, r='%c', r=%d\n", i, r, r)
    }
}
参考答案

输出

len(s) = 8
i=0, r='G', r=71
i=1, r='o', r=111
i=2, r='语', r=35821
i=5, r='言', r=35328

分析

  • "Go语言" 中,Go 各占 1 字节, 各占 3 字节(UTF-8 编码)
  • len(s) 返回字节数:2 + 3 + 3 = 8
  • for range 遍历时,i字节索引rrune 类型的 Unicode 码点
  • 从字节索引 2 开始(占 3 字节),所以下一个字符 从索引 5 开始

搜索