Appearance
08|控制流
程序的核心逻辑由几类操作组成:判断条件是否成立,对一组数据逐个处理,在满足特定条件时提前退出或跳过当前项。Go 提供了 if、switch、for 三种控制结构,外加 defer 用于延迟执行清理动作。没有 while、do-while、until 等关键字,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 语言里的 for、while、do-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)
}无限循环需要内部用 break 或 return 退出,否则会一直运行。
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]
}