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,后来就养成了这个习惯。
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 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个时,switch比if-else可读性高得多。代码看起来像一张表格,一目了然。
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是栈式的,后注册的先执行。这个顺序其实很合理——你先用完的资源,后释放。
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 fmt.Println(time.Now()) 打印的是声明时的时间,不是函数结束时的时间。如果你想打印函数结束的时间,得用闭包:defer func() { fmt.Println(time.Now()) }()。
好了,控制结构就聊到这里。Go的设计哲学就是「少即是多」——一个for搞定所有循环,switch自动break,defer让资源管理变得优雅。这些特性看起来简单,但用好了能让代码质量提升一个档次。下一章我们聊聊函数和错误处理,那又是另一番天地了。