Skip to content

08|控制流

程序的核心逻辑由几类操作组成:判断条件是否成立,对一组数据逐个处理,在满足特定条件时提前退出或跳过当前项。Go 提供了 ifswitchfor 三种控制结构,外加 defer 用于延迟执行清理动作。没有 whiledo-whileuntil 等关键字,for 一个关键字覆盖了所有循环场景。

一、if 条件判断

if 的条件表达式不需要括号,但代码块必须用 {}

go
package main

import "fmt"

func main() {
	cpuUsage := 82

	if cpuUsage >= 90 {
		fmt.Println("critical")
	} else if cpuUsage >= 70 {
		fmt.Println("warning")
	} else {
		fmt.Println("ok")
	}
}

if 可以带一个短语句,在判断条件之前执行一个表达式。这个特性常用于错误处理,让 err 的作用域限制在 if 语句内部:

go
if err := checkDisk("/"); err != nil {
	fmt.Println("check failed:", err)
}
// err 在这里不可见,避免后续误用

短语句里的变量只在 if-else 块内有效。如果需要在后面继续使用,不能用短语句形式,要单独声明。

二、switch

switch 比多个 if-else 更清晰,尤其是判断一个值落在多个离散区间时。Go 的 switch 默认在每个 case 末尾自动 break,不需要手动写。如果希望继续执行下一个 case,用 fallthrough

go
package main

import "fmt"

func main() {
	status := 503

	switch status {
	case 200:
		fmt.Println("ok")
	case 301, 302:
		fmt.Println("redirect")
	case 404:
		fmt.Println("not found")
	case 500, 502, 503:
		fmt.Println("server error")
	default:
		fmt.Println("unknown")
	}
}

case 可以放多个值,用逗号分隔。default 处理所有未匹配的情况。

switch 也可以不带表达式,充当多路 if-else 使用:

go switch
	case cpuUsage >= 90:
		fmt.Println("critical")
	case cpuUsage >= 70:
		fmt.Println("warning")
	default:
		fmt.Println("ok")
	}

判断变量类型时,switch 配合 .(type) 做类型断言:

go
func describe(v interface{}) {
	switch val := v.(type) {
	case string:
		fmt.Println("string:", val)
	case int:
		fmt.Println("int:", val)
	default:
		fmt.Printf("unknown: %T\n", val)
	}
}

三、for 循环

Go 只有 for 一种循环结构,通过不同写法覆盖 C 语言里的 forwhiledo-while 三种场景。

标准 for

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

条件 for(相当于 while)

go
retry := 0
for retry < 3 {
	fmt.Println("retry", retry)
	retry++
}

无限循环

go
for {
	fmt.Println("running")
	time.Sleep(1 * time.Second)
}

无限循环需要内部用 breakreturn 退出,否则会一直运行。

range 遍历

遍历切片、数组、映射、字符串、通道时,用 range

go
services := []string{"nginx", "mysql", "redis"}

for index, service := range services {
	fmt.Println(index, service)
}

只要值、不需要索引时,用 _ 忽略:

go
for _, service := range services {
	fmt.Println(service)
}

遍历映射时,range 返回键和值:

go
labels := map[string]string{
	"env":  "prod",
	"team": "ops",
}

for key, value := range labels {
	fmt.Println(key, value)
}

range 遍历映射的顺序不固定,每次运行可能不同。如果需要固定顺序,先取出所有键排序后再遍历。

四、break、continue 与 label

break 跳出当前循环,continue 跳过本次循环的剩余代码,直接进入下一次迭代:

go
for i := 0; i < 10; i++ {
	if i%2 == 0 {
		continue // 跳过偶数
	}
	if i > 7 {
		break // i=8,9 时退出
	}
	fmt.Println(i)
}

嵌套循环中,break 默认只跳出最内层。如果需要跳出外层循环,给外层循环加 label

go
outer:
for _, host := range hosts {
	for _, port := range ports {
		if !checkPort(host, port) {
			break outer // 直接跳出外层 hosts 循环
		}
	}
}

label 也可以和 continue 配合,跳过外层循环的当前迭代。

五、defer

defer 把函数调用推迟到当前函数返回前执行,常用于关闭文件、关闭连接、释放锁。它的执行顺序是后进先出——多个资源依次打开时,关闭顺序会反过来:

go
package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("/etc/hosts")
	if err != nil {
		fmt.Println("open failed:", err)
		return
	}
	defer file.Close() // 函数返回前关闭文件

	data := make([]byte, 1024)
	_, err = file.Read(data)
	if err != nil {
		fmt.Println("read failed:", err)
		return
	}
	fmt.Println(string(data))
} // file.Close() 在这里执行

defer 在注册时就确定了参数值,而不是在真正执行时求值:

go
func main() {
	i := 0
	defer fmt.Println(i) // 输出 0,不是 1
	i++
}

这个特性容易踩坑。如果 defer 里需要用到最终值,传指针或闭包:

go
func main() {
	i := 0
	defer func() { fmt.Println(i) }() // 输出 1
	i++
}

defer 有轻微的性能开销,但在 IO 操作和锁保护场景里,它带来的代码安全性远大于性能损耗。运维和后端开发中常见的资源泄漏,有一半是因为打开后忘了关闭;defer 在打开资源的同一行就注册关闭动作,能有效减少这类遗漏。

循环内部频繁 defer 会堆积大量延迟调用,直到函数返回时才一起执行。循环次数很多时,应该在循环内部显式清理,而不是依赖 defer

六、常见错误

循环变量捕获

循环里启动 goroutine 时,如果闭包直接引用循环变量,可能所有 goroutine 读到同一个值:

go
for _, host := range hosts {
	go func() {
		check(host) // 危险:host 可能被后续迭代覆盖
	}()
}

正确做法是把循环变量作为参数传入:

go
for _, host := range hosts {
	go func(h string) {
		check(h)
	}(host)
}

Go 1.22 及以后版本对 range 循环变量做了调整,每次迭代产生独立变量,降低了这个坑的出现概率。但显式传参仍然是最安全的写法。

range 修改原值

range 返回的是元素的副本,修改副本不会影响原切片或数组:

go
hosts := []string{"web01", "web02"}
for _, host := range hosts {
	host = "new-" + host // 只修改了副本
}
fmt.Println(hosts) // 仍然是 ["web01", "web02"]

需要修改原值时,用索引访问:

go
for i := range hosts {
	hosts[i] = "new-" + hosts[i]
}