4、Go控制结构:if-else条件判断、for循环(三种形式)、switch语句、defer延迟调用

控制结构是编程语言的骨架。没有它们,代码就只能从上往下直直地跑,跟流水账似的。Go语言在这方面做得相当简洁——它去掉了括号,保留了核心,还塞了一个特别有意思的defer。今天我们就来把这几个家伙彻底聊透。

4.1 if-else:没有括号,但更清爽

Go的if条件判断,第一眼看上去有点怪——条件表达式不用括号包着。但你用习惯了就会发现,这其实让代码更干净。

if score >= 90 {
    fmt.Println("优秀")
} else if score >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}

我个人习惯在条件里直接写表达式,而不是先赋值再判断。Go还支持在if里声明一个局部变量,这个变量只在当前分支里有效:

if age := getUserAge(); age >= 18 {
    fmt.Println("成年了,年龄:", age)
} else {
    fmt.Println("未成年,年龄:", age)
}
// 这里 age 就访问不到了

这个特性我特别喜欢。它把变量的作用域限制在最小范围内,避免了变量污染。我在项目中遇到过好几次因为变量作用域过大导致的bug,后来就养成了这个习惯。

小技巧:if 里的初始化语句和条件判断用分号隔开。这个写法在 Go 里很常见,比如 for 循环里也是类似的风格。

4.2 for循环:三种形式,一种关键字

Go语言只有一个循环关键字——for。没有while,没有do-while。但别担心,一个for就能搞定所有场景。

形式一:标准三段式

这个跟C语言、Java的for循环几乎一样,只是没有括号:

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

嗯,这里要注意:i是在循环内部声明的,循环结束后就访问不到了。这跟if里的变量作用域规则是一致的。

形式二:类似 while

把条件单独拎出来,就是while的效果:

sum := 0
for sum < 100 {
    sum += rand.Intn(20)
}
fmt.Println("最终和:", sum)

我曾经在写一个重试逻辑时用过这种形式——不断重试直到成功或超时。代码读起来很自然。

形式三:无限循环

什么都不写,就是死循环:

for {
    // 一直跑,直到 break
    if shouldStop() {
        break
    }
}

你想想看,什么时候会用无限循环?服务器监听端口、消息队列消费、游戏主循环……这些场景下,for {}是最简洁的表达。

避坑指南:我曾经在 for 循环里直接修改了循环变量,导致 goroutine 闭包捕获了错误的变量值。后来我学会了在循环体内重新声明一个局部变量:for i := 0; i < 10; i++ { i := i; go func() { fmt.Println(i) }() }。Go 1.22 之后这个问题已经修复了,但老版本还是要小心。

4.3 switch语句:比if-else更优雅

Go的switch跟其他语言不太一样。它默认每个case后面自动break,不会穿透。如果你想穿透,得显式写fallthrough

switch day {
case "周一", "周二", "周三", "周四", "周五":
    fmt.Println("工作日")
case "周六":
    fmt.Println("周末第一天")
case "周日":
    fmt.Println("周末第二天")
default:
    fmt.Println("未知")
}

你看,多个条件可以写在一行,用逗号分隔。这比写一堆||清爽多了。

Go的switch还有一个很酷的用法——不带表达式,直接当if-else if链用:

score := 85
switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
case score >= 70:
    fmt.Println("C")
default:
    fmt.Println("D")
}

我个人觉得,当条件分支超过3个时,switchif-else可读性高得多。代码看起来像一张表格,一目了然。

注意:Go 的 switch 每个 case 匹配成功后不会自动执行下一个 case。如果你需要穿透,必须写 fallthrough。但说实话,我在实际项目中很少用 fallthrough,它容易让逻辑变得混乱。

4.4 defer延迟调用:Go的杀手锏

defer是Go语言里我最喜欢的特性之一。它能把一个函数调用延迟到当前函数返回之前执行。说白了,就是「不管函数怎么结束,最后一定要做的事」。

func readFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()  // 函数结束时自动关闭文件

    // 处理文件内容...
    return nil
}

你想想看,如果没有defer,你得在每个return之前手动写f.Close()。万一漏了一个,文件句柄就泄漏了。有了defer,资源释放的代码就写在资源获取的旁边,逻辑上更紧凑,也更安全。

defer的执行顺序

多个defer会按照后进先出的顺序执行。这个特性在释放多个资源时特别有用:

func copyFile(src, dst string) error {
    srcFile, _ := os.Open(src)
    defer srcFile.Close()

    dstFile, _ := os.Create(dst)
    defer dstFile.Close()

    // 复制数据...
    return nil
}

先打开的先关闭?不对。实际执行顺序是:先关闭目标文件,再关闭源文件。因为defer是栈式的,后注册的先执行。这个顺序其实很合理——你先用完的资源,后释放。

避坑指南:我曾经在 defer 里直接修改了函数的返回值,结果发现没生效。后来才明白,defer 是在 return 语句执行之后、函数返回之前执行的。如果你想在 defer 里修改返回值,得用命名返回值才行:
func example() (result int) {
    defer func() {
        result += 10  // 这个修改会生效
    }()
    return 5
}
这个函数返回的是15,不是5。嗯,这个坑我踩过一次就记住了。

defer的常见用途

场景 示例
关闭文件 defer f.Close()
解锁互斥锁 defer mu.Unlock()
记录函数耗时 defer timeTrack(time.Now(), "myFunc")
恢复panic defer func() { recover() }()

说白了,只要涉及「成对操作」——打开/关闭、加锁/解锁、开始/结束——都可以用defer来保证配对。这是Go语言里最实用的设计模式之一。

小技巧:defer 的参数是在声明时求值的,不是在执行时。比如 defer fmt.Println(time.Now()) 打印的是声明时的时间,不是函数结束时的时间。如果你想打印函数结束的时间,得用闭包:defer func() { fmt.Println(time.Now()) }()

好了,控制结构就聊到这里。Go的设计哲学就是「少即是多」——一个for搞定所有循环,switch自动break,defer让资源管理变得优雅。这些特性看起来简单,但用好了能让代码质量提升一个档次。下一章我们聊聊函数和错误处理,那又是另一番天地了。