Go 语言流程控制

基本概念

流程控制指使用选择、循环和跳转等结构来控制程序执行流程,以响应不同的输入和条件。对应到 Go 语言中,流程控制语句分为四类:条件语句、选择语句、循环语句和跳转语句。

条件语句

条件语句根据条件表达式结果来执行不同代码块。

单条件分支

如果条件语句仅有一个条件分支,则称为单分支选择结构。单分支选择结构使用单个 if 语句实现:

if condition {
    // 条件为真时执行的代码
}

if 语句检查条件表达式 condition 的值,为 true 时执行代码块中代码,否则直接跳过:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// 使用 if 语句判断系统是否需要特殊处理
	if runtime.GOOS == "linux" {
		fmt.Println("Linux 平台区分大小写,统一转为小写。")
	}

	// 程序继续执行其他任务
	fmt.Println("正式开始运行")
}

双条件分支

如果条件语句有两个条件分支,则称为双分支选择结构。双分支选择结构使用 if...else 语句来实现:

if condition {
    // 条件为真时执行
} else {
    // 条件为假时执行
}

if 语句基础上,当 conditionfalse 时,会执行 else 语句块中代码:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// 使用 if...else 语句判断是否要启用多线程
	if runtime.NumCPU() > 1 {
		fmt.Println("启动多线程运行")
	} else {
		fmt.Println("使用单线程运行")
	}
}

多条件分支

如果条件语句有多个分支条件,则称该为多分支选择结构。多分支选择结构使用 else if 语句来扩展条件语句:

if condition1 {
    // 条件 1 为真时执行
} else if condition2 {
    // 条件 1 为假且条件 2 为真时执行
} else {
    // 上述条件均为假时执行
}

当有多个条件需要判断时,else if 语句能在前面判断条件为 false 时接着判断下一个条件:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 获取当前时间(24 小时制)
	hour := time.Now().Hour()

	// 使用多个 else if 判断当前时间段
	if hour < 6 {
		fmt.Println("Good night!")
	} else if hour < 12 {
		fmt.Println("Good morning!")
	} else if hour < 18 {
		fmt.Println("Good afternoon!")
	} else {
		fmt.Println("Good evening!")
	}
}

初始化语句

条件语句支持在条件检查前执行一个初始化子语句(表达式),用于声明局部变量。局部变量只在 ifelse 代码块中有效:

if initialization; condition {
    // 条件为真时执行
}

初始化子语句用于处理错误情况,能使代码更精简:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 使用初始化子语句,获取文件打开结果
	if file, err := os.Open("filename"); err == nil {
		// 文件打开成功,在这里操作
		defer file.Close()
	} else {
		fmt.Println(err)
	}
}

选择语句

选择语句和条件语句同属于条件选择语句,条件语句多用于单分支和双分支选择条件,选择语句表达多分支判断时更清晰。

Go 语言改进了传统 switch 语法设计,每个 case 是独立代码块,运行完自动结束语句,不需要显式使用 break 语句跳出。选择语句又分为三种主要格式或用法:有表达式切换、无表达式切换和类型切换。

有表达式

最常用格式,在 switch 后跟一个表达式,通常是一个变量。然后根据这个表达式的值来选择执行哪个 case

switch expression {
case value1:
    // 代码块1
case value2, value3:
    // 代码块2
default:
    // 默认代码块
}
  • expression:需要评估的表达式。
  • case value1case value2, value3…:与表达式值进行匹配的常量或变量,类型必须一致,不允许重复判断。每个 case 可以拥有多个匹配值,只要匹配中任意一个都会执行代码块。
  • default:可选,没有任何 case 匹配时执行的代码块,类似于 else 语句。也可以不放在最后,但每个 switch 语句只能存在一个默认分支。

如下面代码用来判断系统类型:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	os := runtime.GOOS

	switch os {
	case "linux":
		fmt.Println("Linux 平台区分大小写,统一转为小写。")
	case "windows":
		fmt.Println("Windows 平台不区分大小写,无需转换。")
	case "darwin", "freebsd":
		fmt.Println("不支持此系统平台,程序即将退出。")
	}
}

无表达式

switch 语句后可以不带表达式,而是在每个 case 中使用条件表达式判断,类似于 if...else if 语句:

switch {
case condition1:
    // 当 condition1 为真时执行
case condition2:
    // 当 condition2 为真时执行
default:
    // 无条件匹配时执行
}

下面用选择语句替代多分支条件语句:

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()

	switch {
	case t.Hour() < 6:
		fmt.Println("晚安")
	case t.Hour() < 12:
		fmt.Println("早安")
	case t.Hour() < 18:
		fmt.Println("下午好")
	default:
		fmt.Println("晚上好")
	}
}

类型切换

类型切换是使用 switch 结构来检查接口变量的动态类型,并根据类型执行不同代码块:

package main

import "fmt"

func typeSwitch(i interface{}) {
    switch v := i.(type) { // 如果没用到变量 v,可以直接写成 switch i.(type) {}
	case int:
		fmt.Printf("%v 乘以 2 等于 %v\n", v, v*2)
	case string:
		fmt.Printf("字符串 %q 长度为 %v 字节\n", v, len(v))
	default:
		fmt.Printf("不能处理类型:%T\n", v)
	}
}

func main() {
	typeSwitch(21)      // 输出:21 乘以 2 等于 42
	typeSwitch("hello") // 输出:字符串 "hello" 长度为 5 字节
	typeSwitch(true)    // 输出:不能处理类型:bool
}

类型判断中,不允许使用 fallthrough 关键字。

初始化语句

和条件语句一样,选择语句支持将一个初始化子语句写在关键字 switch 之后,初始化变量的作用域被限制在选择语句内部,避免外部使用这些变量:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	// 无表达式中初始化子语句,获取当前时间并赋值给 t
	switch t := time.Now(); {
	case t.Hour() > 9:
		fmt.Println("努力工作")
	default:
		fmt.Println("晚安睡觉")
	}

	// 有表达式中初始化子语句,使用相同局部变量名 t
	switch t := runtime.NumCPU(); t {
	case 1:
		fmt.Println("使用单线程运行")
	default:
		fmt.Println("启动多线程运行")
	}
}

注意在无表达式 switch 中,初始化子语句后一定要有分号 ,不可以省略。

穿透

选择语句关键字 fallthrough 用于实现特殊控制流。规则如下:

  • 定义位置fallthrough 必须是 case 代码块中最后一个语句。不能用在最后一个 casedefault 块中,因为没有下一个 case 块可以执行。
  • 无条件执行:在使用 fallthrough 关键字的 case 块执行完毕后,下一个 case 的条件不会被检查,块内代码会无条件执行。

使用 fallthrough 可以模拟传统编程语言中 switch 语句的穿透行为,在实际编程中一般用不到:

package main

import (
	"fmt"
)

func main() {
	i := 2
	switch i {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
		fallthrough
	case 3: // 下降到这一分支
		fmt.Println("three")
	case 4: // 但不会影响到这一分支
		fmt.Println("four")
	default:
		fmt.Println("something else")
	}
}

循环语句

循环语句用来重复执行语句块,直到满足停止条件。Go 语言中没有 do...while 语句,因此 for 语句应用更广泛。

基本循环

for 基本循环结构包含初始化语句、条件语句和后续语句。语句之间使用分号来分隔,三个语句顺序不能错位:

for initialization; condition; post {
    // 循环体
}
  • initialization:初始化表达式仅在初次迭代时执行,给循环变量赋初值。
  • condition:条件表达式会在每次迭代前检查,为真时执行循环体中语句,否则跳出循环。
  • post:后续操作在每次迭代后执行,一般修改循环变量的值,然后跳到条件表达式继续评估。

基本循环与其他语言中的 for 循环用法类似,用于精确控制循环次数:

package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		// Go 1.22 之后,每次迭代的循环变量都是唯一的,因此会打印出不同指针值
		// 在这之前 i 只会初始化一次,每次循环只是对同个 i 进行赋值
		fmt.Println(&i, i)
	}
}

可以有多个循环变量同时参与循环控制:

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	fmt.Printf("请猜一个 0 到 100 之间的整数:\n")
	for n, i := rand.Intn(100)+1, 0; i != n; fmt.Scan(&i) {
		switch {
		case i > n:
			fmt.Println("猜大了,请继续:")
		case i < n:
			fmt.Println("猜小了,请继续:")
		}
	}
	fmt.Printf("猜中了")
}

循环变量一般习惯使用 ijk 等较短的名称命名计数器,不要在循环体内修改计数器。

条件循环

for 语句可以省略初始化语句和后续语句,得到类似 while 语句的条件循环结构:

for condition {
    // 循环体
}

这种形式只在循环顶部检查条件,用于循环次数不确定的场景。例如等待用户输入,直到输入特定内容,结束循环:

package main

import (
	"bufio"
	"fmt"
	"os"
	"time"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Println("输入 exit 退出循环:")

	for scanner.Scan() { // 循环读取用户输入
		input := scanner.Text() // 获取用户输入的文本
		// 当用户输入 exit 时,倒计时并退出循环
		if input == "exit" {
			// 嵌套循环退出倒计时
			fmt.Println("退出倒计时:")
			i := 5
			for i > 0 {
				fmt.Println(i)
				time.Sleep(1 * time.Second)
				i--
			}
			fmt.Println("退出循环")
			break
		}
		fmt.Println("输入内容:", input)
	}

	if err := scanner.Err(); err != nil {
		fmt.Fprintln(os.Stderr, "读取错误:", err)
	}
}

无限循环

如果进一步忽略 for 语句的条件表达式,会形成无限循环结构:

for {
    // 循环体
}

无限循环可以用来持续监听或者将退出条件放在循环体中:

package main

import (
	"fmt"
	"time"
)

func main() {
	for {
		fmt.Println("每隔十秒,无限打印当前时间", time.Now())
		time.Sleep(10 * time.Second)
	}
}

遍历集合

for 循环可以配合 range 关键字,来遍历数组、切片、字符串、映射或通道类型中的元素:

package main

import "fmt"

func main() {
	for i, s := range []string{"a", "b", "c", ""} {
		fmt.Println("索引:", i) // 分别打印 0,1,2,3
		fmt.Println("值:", s)  // 分别打印 a b c 
	}
}

range 子语句作用类似于迭代器,针对不同遍历对象会返回不同值:

遍历对象 第一个返回值 第二个返回值
字符串 索引 索引对应的值,类型为字符
数组和切片 索引 索引对应的值
映射 键对应的值
通道 元素,通道内的数据

对于空映射或切片、空数组、空字符串等情况,for 语句会直接结束。如果数组指定了长度,则 for 会循环执行长度相等的次数:

package main

import "fmt"

func main() {
	// 不会执行
	for _, x := range []int{} {
		fmt.Printf("执行结果:%v\n", x)
	}

	// 循环 4 次,只有索引 0 为 1,其他索引返回 0
	for i, x := range [4]int{1} {
		fmt.Printf("索引 %d 的值:%v\n", i, x)
	}
}

跳转语句

跳转语句用于跳出流程、跳过循环或跳转到指定标签位置。Go 语言中有三个跳转语句:gotobreakcontinue

标签

标签用来在函数内标记位置,由自定义标识符后跟一个冒号 : 组成,需要和跳转语句配合使用:

  • goto 使用goto 后面跟标签名,程序执行将跳转到标签位置。
  • breakcontinue 使用:在循环嵌套或条件分支结构中,使用 breakcontinue 只作用于最内层循环,而配合标签可以跳转到具体的循环层次。

标签名建议使用大写字母和数字。

跳出

break 语句用于跳出包含它的最内层循环或选择结构。具体来说:

  • for 循环中:中断当前循环,不再执行后续迭代。
  • switch 语句中:从一个分支明确跳出整个 switch 语句。
  • select 语句中:结束 select 语句。

break 主要是用在循环语句中:

package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		if i == 5 {
			break // 到 i 等于 5 时跳出循环
		}
		fmt.Println(i) // 从 0 打印到 4
	}
	fmt.Println("继续执行代码")
}

和标签搭配使用,可以跳出指定循环:

package main

import "fmt"

func main() {
OuterLoop: // 标签用于外层循环
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if j == 2 {
				break OuterLoop // 跳出外层循环
			}
			fmt.Printf("i = %d, j = %d\n", i, j) // 只打印两行结果
		}
	}
	fmt.Println("继续执行代码")
}

没有标签时,break 在内层 for 循环 2 次后中断,但外层还是会循环 3 次,总共打印 6 行结果。标签用在这里,直接跳出外层 for 循环,可减少很多判断代码。

跳过

continue 语句用于跳过当前循环迭代剩余语句,直接开始下一次迭代:

package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		if i%2 == 0 {
			continue // i 等于偶数时跳过打印
		}
		fmt.Println(i) // 输出奇数 1 和 3
	}
	fmt.Println("继续执行代码")
}

配合标签,用于跳转到指定层级继续循环:

package main

import "fmt"

func main() {
OuterLoop: // 外层循环标签
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if j == 2 {
				continue OuterLoop // 条件满足直接跳到外层循环继续迭代
			}
			fmt.Printf("i = %d, j = %d\n", i, j) // 打印 6 行结果
		}
	}
	fmt.Println("继续执行代码")
}

代码只改了跳出关键字,和 break 比起来,continue 不会中断循环语句,仅仅是跳过本次循环,继续从标签处开始迭代。

跳到

goto 语句用于无条件跳转到同一函数内的指定标签处。goto 语句可以用在任何地方,但不能跳过变量声明,也不能跳进一个代码块内部:

package main

import "fmt"

func test() {
L1:
	goto L1
}

func main() {
	fmt.Println("正常代码")
	//goto L1 // 不能跨函数跳转
	goto L2
	fmt.Println("无效代码")
	//a := 4 // 不能跳过变量声明
L2:
	fmt.Println("跳到此处")
}

使用 go 语句会造成代码难以追踪和维护,非必需不使用。