3、Go流程控制:if-else条件判断、for循环、switch语句、defer延迟调用

流程控制,说白了就是让代码按你的想法走。Go语言在这块做得非常简洁,没有while,没有do-while,也没有三元表达式。刚开始从其他语言转过来时,我确实有点不习惯。但用久了你会发现,这种「少即是多」的设计,反而让代码更清晰。

3.1 if-else条件判断

Go的if语句有个很特别的地方——条件表达式不需要括号。我个人习惯直接写,不加括号,这样代码看着更清爽。

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

嗯,这里要注意:else if 和 else 必须跟前面的右花括号在同一行。Go强制要求花括号的格式,不能换行。我第一次写Go时就被这个坑过,编译报错还找了半天原因。

if 的特殊用法:带初始化语句

Go的if可以像for一样,在条件前加一个初始化语句。这个特性我在项目中经常用,尤其是处理函数返回值时。

if err := doSomething(); err != nil {
    fmt.Println("出错了:", err)
    return
}
// 注意:err 的作用域只在 if 块内

我曾经在代码审查时看到有人把err定义在if外面,然后在if里面又重复判断。其实完全没必要,用这种写法既简洁又安全。

3.2 for循环

Go只有for这一种循环关键字。但它有三种形态,覆盖了所有场景。

3.2.1 经典for循环

跟C语言一样,三个部分:初始化、条件、后置。不过没有括号。

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

你想想看,为什么Go不提供while?其实for去掉初始化和后置,就是while了。

// 相当于 while
for sum < 1000 {
    sum += sum
}

3.2.2 range循环

range是Go的杀手锏之一。遍历数组、切片、map、字符串、通道,都用它。

// 遍历切片
names := []string{"张三", "李四", "王五"}
for index, name := range names {
    fmt.Printf("第%d个是%s\n", index+1, name)
}

// 遍历map
scores := map[string]int{"语文": 90, "数学": 95}
for subject, score := range scores {
    fmt.Printf("%s: %d分\n", subject, score)
}

// 遍历字符串(按rune)
for i, ch := range "你好Go" {
    fmt.Printf("位置%d: %c\n", i, ch)
}

避坑指南:range遍历map时,顺序是不确定的。我曾经在项目中写了一个依赖map顺序的逻辑,结果测试时偶尔通过偶尔失败。后来查了半天才发现是map遍历顺序的问题。如果你需要固定顺序,请先对key排序。

3.2.3 无限循环

有时候我们需要一直跑,直到某个条件触发。Go的写法很直接:

for {
    // 一直执行
    if shouldStop() {
        break
    }
}

我个人习惯在无限循环里加个sleep,防止CPU跑满。当然,如果是等待通道消息,那就不需要了。

3.3 switch语句

Go的switch比很多语言都灵活。它有两种形式:表达式switch和类型switch。

3.3.1 表达式switch

最常用的形式。注意:Go的switch默认每个case后面自动break,不需要手动写。如果你想要穿透,得用fallthrough关键字。

day := 3
switch day {
case 1:
    fmt.Println("周一")
case 2:
    fmt.Println("周二")
case 3:
    fmt.Println("周三")
    // 这里不会自动进入下一个case
default:
    fmt.Println("其他")
}

为什么会这样设计?因为Go团队发现,大部分情况下break才是我们想要的。少数的穿透场景用fallthrough显式声明,反而更清晰。

switch还可以不带表达式,直接当if-else链用:

score := 85
switch {
case score >= 90:
    fmt.Println("优秀")
case score >= 60:
    fmt.Println("及格")
default:
    fmt.Println("不及格")
}

3.3.2 类型switch

这个特性我在处理接口类型时经常用。它可以根据接口变量的实际类型来执行不同分支。

var x interface{} = 42
switch v := x.(type) {
case int:
    fmt.Printf("整数: %d\n", v)
case string:
    fmt.Printf("字符串: %s\n", v)
case bool:
    fmt.Printf("布尔: %v\n", v)
default:
    fmt.Printf("未知类型: %T\n", v)
}

注意:类型switch只能在接口类型上使用。如果你对一个具体类型做类型断言,编译会报错。我曾经看到有人对string类型做类型switch,结果编译不过,一脸懵。

3.4 defer延迟调用

defer是Go最优雅的特性之一。它保证函数返回前一定会执行。最常见的用途就是释放资源。

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

我个人习惯把defer紧跟在资源获取之后。这样一眼就能看出资源什么时候释放,代码的可维护性大大提高。

3.4.1 defer的执行顺序

多个defer会按后进先出的顺序执行。说白了就是最后一个defer最先执行。

func main() {
    defer fmt.Println("第一个defer")
    defer fmt.Println("第二个defer")
    defer fmt.Println("第三个defer")
}
// 输出:
// 第三个defer
// 第二个defer
// 第一个defer

为什么会这样?因为defer本质上是把函数调用压入栈中,函数返回时再从栈顶弹出执行。

3.4.2 defer的坑

defer有个经典陷阱:defer的参数在声明时求值

func main() {
    x := 10
    defer fmt.Println(x)  // 输出10,不是20
    x = 20
}

我曾经在项目中写了一个defer来记录函数执行时间,结果发现时间总是0。后来才意识到,time.Now()在defer声明时就已经求值了。正确的做法是用闭包:

func main() {
    start := time.Now()
    defer func() {
        fmt.Println("耗时:", time.Since(start))
    }()
    // 执行一些操作...
}

我的建议:defer虽然好用,但别滥用。在循环里用defer要特别小心,因为defer直到函数返回才执行。如果循环次数很多,defer会堆积大量未执行的函数,可能导致内存问题。这种情况下,我一般直接调用函数,不用defer。

3.5 小结

Go的流程控制就这些了。没有while,没有do-while,没有三元表达式。但就是这几个关键字,配合defer的优雅设计,已经能写出非常清晰的代码。你想想看,是不是越简单的设计,反而越不容易出错?

下一章我们会聊函数,包括多返回值、可变参数、闭包这些Go的特色。到时候见。