流程控制
流程控制语句决定了程序的执行顺序。Go 语言的流程控制主要包括条件判断(if/switch)和循环(for),以及 defer 和 goto 等特殊控制语句。
if / else 条件语句
if 语句是 Go 中最基本的条件判断语句。Go 的 if 与 C/Java 的主要区别是:条件表达式不需要小括号,但大括号 {} 是必需的。
基本语法
x := 42
if x > 50 {
fmt.Println("x 大于 50")
} else if x > 30 {
fmt.Println("x 大于 30")
} else {
fmt.Println("x 不大于 30")
}
// 输出:x 大于 30if 的初始化语句
if 语句可以包含一个初始化语句,通常用于错误处理:
// 经典的错误处理模式
if err := doSomething(); err != nil {
fmt.Printf("操作失败: %v\n", err)
return
}
fmt.Println("操作成功")// 也可以声明局部变量
if v := computeValue(); v > threshold {
fmt.Printf("值 %d 超过阈值\n", v)
}
// v 在此处不可访问Go 的 if 规则
- 条件表达式不需要小括号:
if x > 0 { ... }(不是if (x > 0) { ... }) - 大括号
{}必须存在,且else必须与}在同一行 - 条件表达式的结果必须是
bool类型,Go 不会对非布尔值进行隐式转换
// 错误:条件必须为 bool
// if 1 { } // 编译错误
// if x = 5 { } // 编译错误(是赋值,不是比较)
// 正确:条件必须为 bool
if x == 5 { }
if err != nil { }switch 语句
Go 的 switch 语句比 C/Java 更加灵活和安全:
基本语法
day := "Monday"
switch day {
case "Monday":
fmt.Println("工作日:周一")
case "Tuesday":
fmt.Println("工作日:周二")
case "Wednesday":
fmt.Println("工作日:周三")
case "Thursday":
fmt.Println("工作日:周四")
case "Friday":
fmt.Println("工作日:周五")
case "Saturday", "Sunday": // 多个值用逗号分隔
fmt.Println("周末")
default:
fmt.Println("未知")
}Go switch 与 C/Java 的区别
| 特性 | Go | C/Java |
|---|---|---|
| 自动 break | ✅ 匹配后自动跳出 | ❌ 需要 break,否则 fall-through |
| 条件表达式 | 可以是任意类型 | 仅整数/字符/枚举 |
| case 值 | 可以是任意可比较类型 | 有限类型 |
| 多值匹配 | case "a", "b": | 不支持 |
自动 break
Go 的 switch 匹配某个 case 后会自动跳出,不需要(也不应该)写 break。如果需要”贯穿”到下一个 case,需要显式使用 fallthrough 关键字。
switch 初始化语句
与 if 类似,switch 也可以包含初始化语句:
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("macOS")
case "linux":
fmt.Println("Linux")
case "windows":
fmt.Println("Windows")
default:
fmt.Printf("其他系统: %s\n", os)
}无条件的 switch
switch 可以不带任何值,类似 if-else if-else 链:
score := 85
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
// 输出:良好fallthrough
使用 fallthrough 让程序继续执行下一个 case(无条件):
n := 2
switch n {
case 1:
fmt.Println("一")
fallthrough
case 2:
fmt.Println("二")
fallthrough
case 3:
fmt.Println("三")
default:
fmt.Println("其他")
}
// 输出:
// 二
// 三fallthrough 的行为
fallthrough 会无条件执行下一个 case,不会检查下一个 case 的条件。即使下一个 case 的条件不满足,也会执行其代码块。
type switch
type switch 是 Go 特有的语法,用于判断接口值的实际类型:
func checkType(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("整数: %d\n", v)
case string:
fmt.Printf("字符串: %s\n", v)
case float64:
fmt.Printf("浮点数: %f\n", v)
case bool:
fmt.Printf("布尔值: %t\n", v)
default:
fmt.Printf("未知类型: %T\n", i)
}
}
func main() {
checkType(42) // 整数: 42
checkType("hello") // 字符串: hello
checkType(3.14) // 浮点数: 3.140000
checkType(true) // 布尔值: true
checkType([]int{1}) // 未知类型: []int
}type switch 中 v 的类型
在 switch v := i.(type) 中,v 在每个 case 中的类型就是该 case 指定的类型,可以直接使用该类型的方法。
for 循环
for 是 Go 中唯一的循环关键字。Go 没有 while 和 do-while,但 for 可以实现所有循环形式。
经典 for 循环
for i := 0; i < 10; i++ {
fmt.Println(i)
}类 while 的 for 循环
省略初始化和后置语句,for 就变成了 while:
n := 10
for n > 0 {
fmt.Println(n)
n--
}无限循环
省略所有三个部分,就变成无限循环:
for {
// 无限循环,需要 break 或 return 来退出
fmt.Println("循环中...")
break
}无限循环的用途
无限循环常用于服务器的主循环、事件监听等场景。Go 中 for {} 比 while true 更简洁。
for range 遍历
for range 用于遍历各种数据结构:
// 遍历切片
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
// 遍历映射
m := map[string]int{"Alice": 90, "Bob": 85}
for key, value := range m {
fmt.Printf("键: %s, 值: %d\n", key, value)
}
// 遍历字符串(按 rune 遍历)
s := "Hello世界"
for index, r := range s {
fmt.Printf("索引: %d, 字符: %c\n", index, r)
}
// 遍历通道
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
fmt.Println(v)
}忽略不需要的变量
使用 _ 忽略不需要的索引或值:
// 只需要值,忽略索引
for _, v := range nums {
fmt.Println(v)
}
// 只需要索引,忽略值
for i := range nums {
fmt.Println(i)
}
// 遍历映射时,只需要值
for _, score := range studentScores {
fmt.Println(score)
}break 与 continue
break 用于提前终止循环,continue 用于跳过当前迭代:
// break:打印到 5 时停止
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i) // 输出 0, 1, 2, 3, 4
}
// continue:跳过偶数
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i) // 输出 1, 3, 5, 7, 9
}带标签的 break 和 continue
通过标签可以控制嵌套循环的跳出:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // 跳出外层循环
}
fmt.Printf("(%d, %d)\n", i, j)
}
}
// 输出:
// (0, 0)
// (0, 1)
// (0, 2)
// (1, 0)defer 语句
defer 语句将一个函数调用推迟到当前函数返回之前执行。常用于资源释放、解锁、关闭文件等清理操作,确保即使发生 panic 也能执行清理逻辑。
基本用法
func main() {
defer fmt.Println("3. 最后执行")
defer fmt.Println("2. 倒数第二个执行")
fmt.Println("1. 最先执行")
}
// 输出:
// 1. 最先执行
// 2. 倒数第二个执行
// 3. 最后执行defer 的执行顺序
多个 defer 语句按照**后进先出(LIFO)**的顺序执行,类似栈的操作:
func example() {
defer fmt.Println("first defer")
defer fmt.Println("second defer")
defer fmt.Println("third defer")
fmt.Println("function body")
}
// 输出:
// function body
// third defer
// second defer
// first deferdefer 与资源释放
defer 最常见的用途是确保资源被正确释放:
func readFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // 确保函数返回时关闭文件
// 读取文件内容...
return nil
}
func process() {
mu.Lock()
defer mu.Unlock() // 确保函数返回时解锁
// 处理逻辑...
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer resp.Body.Close() // 确保关闭响应体
// 处理响应...
}defer 的参数求值时机
defer 语句中的函数参数在 defer 执行时(而非调用时)就已经求值:
func main() {
x := 10
defer fmt.Println("defer:", x) // x 在 defer 声明时就已求值为 10
x = 20
fmt.Println("current:", x)
}
// 输出:
// current: 20
// defer: 10defer 与循环结合的注意事项
在循环中使用 defer 时,所有延迟的函数调用会在函数返回时才执行(而非每次循环结束时)。如果循环次数很大,可能导致资源延迟释放:
// 错误示范:所有文件在函数结束时才关闭
func processFiles(files []string) {
for _, f := range files {
file, _ := os.Open(f)
defer file.Close() // 所有 close 延迟到函数返回时执行
// 处理文件...
}
}
// 正确做法:将循环体提取为单独的函数
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 每个文件处理完后立即关闭
// 处理文件...
return nil
}
func processFiles(files []string) {
for _, f := range files {
processFile(f)
}
}goto 语句
goto 语句可以无条件跳转到同一函数内的标签处。Go 保留了 goto 但不推荐使用。
func main() {
i := 0
loop:
if i < 5 {
fmt.Println(i)
i++
goto loop
}
}goto 的限制
Go 对 goto 有严格限制:
- 不能跳过变量的声明
- 不能跳入新的作用域
- 不能从外层作用域跳入内层作用域
// 错误:goto 跳过了 j 的声明
goto skip
j := 10 // 编译错误:goto skip jumps over declaration of j
skip:
fmt.Println(j)goto 的合理使用场景
虽然 goto 通常不推荐使用,但在某些特定场景下可以简化代码,例如统一的错误处理。但在大多数情况下,使用函数封装或 if/else 替代是更好的选择。
// goto 用于统一错误处理(旧式风格,不推荐)
func process() error {
if err := step1(); err != nil {
goto cleanup
}
if err := step2(); err != nil {
goto cleanup
}
return nil
cleanup:
// 清理资源
return err
}
// 推荐的现代写法:使用 defer
func process() (err error) {
defer cleanup() // 使用 defer 进行清理
if err = step1(); err != nil {
return err
}
if err = step2(); err != nil {
return err
}
return nil
}练习题
练习 1:FizzBuzz
编写 FizzBuzz 程序:打印 1 到 30 的数字,但如果是 3 的倍数打印 “Fizz”,5 的倍数打印 “Buzz”,同时是 3 和 5 的倍数打印 “FizzBuzz”。请使用 for 循环和条件判断实现。
解题思路:先判断 15 的倍数(即同时是 3 和 5 的倍数),再判断 3 的倍数,最后判断 5 的倍数。
代码:
package main
import "fmt"
func main() {
for i := 1; i <= 30; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}关键点:i%15 == 0 必须放在最前面,因为 15 的倍数同时也是 3 和 5 的倍数。也可以使用 if-else if-else 替代 switch。
练习 2:defer 执行顺序
以下代码的输出是什么?请逐步分析执行过程。
package main
import "fmt"
func trace(name string) func() {
fmt.Printf("进入 %s\n", name)
return func() {
fmt.Printf("退出 %s\n", name)
}
}
func main() {
defer trace("main")()
fmt.Println("main 开始执行")
defer trace("步骤 A")()
defer trace("步骤 B")()
defer trace("步骤 C")()
fmt.Println("main 结束执行")
}解题思路:理解 defer 的参数求值时机和 LIFO 执行顺序。
输出:
进入 main
main 开始执行
进入 步骤 A
进入 步骤 B
进入 步骤 C
main 结束执行
退出 步骤 C
退出 步骤 B
退出 步骤 A
退出 main分析:
defer trace("main")()—trace("main")立即执行(打印”进入 main”),返回的匿名函数被 defer 延迟fmt.Println("main 开始执行")— 立即执行defer trace("步骤 A")()— 同上,打印”进入 步骤 A”,返回函数延迟defer trace("步骤 B")()— 打印”进入 步骤 B”,返回函数延迟defer trace("步骤 C")()— 打印”进入 步骤 C”,返回函数延迟fmt.Println("main 结束执行")— 立即执行- 函数返回,按 LIFO 顺序执行 defer:退出 C → 退出 B → 退出 A → 退出 main
练习 3:for range 遍历字符串
以下代码的输出是什么?解释 for range 遍历字符串时索引和值的含义。
package main
import "fmt"
func main() {
s := "Go语言"
fmt.Println("len(s) =", len(s))
for i, r := range s {
fmt.Printf("i=%d, r='%c', r=%d\n", i, r, r)
}
}输出:
len(s) = 8
i=0, r='G', r=71
i=1, r='o', r=111
i=2, r='语', r=35821
i=5, r='言', r=35328分析:
"Go语言"中,G和o各占 1 字节,语和言各占 3 字节(UTF-8 编码)len(s)返回字节数:2 + 3 + 3 = 8for range遍历时,i是字节索引,r是 rune 类型的 Unicode 码点语从字节索引 2 开始(占 3 字节),所以下一个字符言从索引 5 开始
