目 录CONTENT

文章目录
go

【Go】控制结构

hxuanyu
2025-12-30 / 0 评论 / 0 点赞 / 54 阅读 / 0 字

if语句

基本使用

Go 中的 if 语句格式如下:

if 布尔表达式 {
	// 表达式为 true 时执行的分支
}
// 执行完成后回到原流程继续执行

Go 的 if​ 语句左大括号必须与 if 关键字位于同一行,并且布尔表达式不需要用括号包围。

如果需要处理多个条件判断,可以使用逻辑运算符将条件连接成复合表达式:

if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" &&
    runtime.Compiler != "gccgo" {
    println("we are using standard go compiler on linux os for amd64")
}

Go 的操作符有固定的优先级(高→低):

优先级(高→低)

操作符(同一行同优先级)

5

*/%<<>>&&^

4

+-

3

==!=<<=>>=

2

&&

1

||

实际开发中,为避免复杂逻辑带来的误读,通常用括号将复合条件拆解为更清晰的子表达式,提高可读性并减少因优先级导致的误解。

除单分支结构外,if 还支持二分支和多分支结构:

if booleanExpression {
    // 分支1
} else {
    // 分支2
}
if booleanExpression1 {
    // 分支1
} else if booleanExpression2 {
    // 分支2
} else if booleanExpression3 {
    // 分支3
} else {
    // 分支4
}

多分支结构等价于以下二分支结构的嵌套:

if booleanExpression1 {
    // 分支1
} else {
    if booleanExpression2 {
        // 分支2
    } else {
        if booleanExpression3 {
            // 分支3
        } else {
            // 分支4
        }
    }
}

多分支结构会按书写顺序依次求值;一旦某个条件为 true,就执行对应分支并停止后续分支的求值。因此通常应将最可能成立的条件放在前面,以减少不必要的判断。

自用变量(初始化语句)

无论是单分支、二分支还是多分支结构,都可以在 if​ 后、条件表达式前加入初始化语句,并用分号 ;​ 与条件隔开。初始化语句中声明的变量作用域仅限于整个 if/else if/else 链对应的隐式代码块内(但需要注意遮蔽与可见性):

func main() {
    if a, c := f(), h(); a > 0 {
        println(a)
    } else if b := f(); b > 0 {
        println(a, b)
    } else {
        println(a, b, c)
    }
}

这种写法可能引入变量遮蔽(shadowing)或因作用域导致的“变量不可见”,实际使用时需格外注意。

快乐路径(Happy Path)

if 的单分支、二分支、多分支结构可读性依次下降。Go 更推荐通过“尽早返回/尽早失败(guard clause)”的方式,减少嵌套与多分支,让正常逻辑保持靠左。

常见写法如下:

func doSomething() error {
    if errorCondition1 {
        // 错误处理逻辑
        // ...
        return err1
    }

    // 成功处理逻辑
    // ...

    if errorCondition2 {
        // 更多错误处理逻辑
        // ...
        return err2
    }

    // 更多成功处理逻辑
    // ...
    return nil
}

其特点:

  • 以单分支为主,减少嵌套

  • 条件不满足时快速返回(失败路径尽早结束)

  • 成功路径代码始终“靠左”,便于从上到下阅读

  • 走到函数末尾通常表示成功完成

这种结构常被称为“快乐路径”,指代成功逻辑的主要执行路径。


for语句

Go 仅提供 for 作为循环语句。

经典使用形式

var sum int
for i := 0; i < 10; i++ {
    sum += i
}
println(sum)

其组成部分可理解为:

image-20251230140115-rlw0hyo.png
  • i := 0​:循环前置语句,在进入循环前执行一次,常用于初始化循环变量;其作用域仅在该 for 的隐式代码块内。

  • i < 10​:条件判断表达式;为 true​ 时执行循环体,为 false 时结束循环。

  • sum += i:循环体,每次执行称为一次迭代。

  • i++:循环后置语句,每次循环体执行完后执行,常用于更新循环变量。

除循环体必须存在外,其余三个部分均可省略。若省略前置或后置语句,分号仍需保留;若同时省略前置与后置语句,则分号可省略,仅保留条件:

i := 0
for i < 10 {
    println(i)
    i++
}

若条件也省略,则形成无限循环:

for {
    // 循环体代码
}

for range 循环

遍历切片的经典写法:

var sl = []int{1, 2, 3, 4, 5}
for i := 0; i < len(sl); i++ {
    fmt.Printf("sl[%d] = %d\n", i, sl[i])
}

更简洁的写法是 for range

for i, v := range sl {
    fmt.Printf("sl[%d] = %d\n", i, v)
}

其中 i​ 是索引,v​ 是对应元素值;range 会按顺序遍历切片/数组(对数组或切片而言是确定顺序),直到遍历完成。

for range 的变种

由于 Go 要求声明的局部变量必须被使用,不需要的变量可用 _ 丢弃。

只需要 key(索引):

for k, _ := range sl1 {
    println(k)
}

可简写为:

for k := range sl1 {
    println(k)
}

只需要 value

for _, v := range sl1 {
    println(v)
}

都不需要:

for _, _ = range sl1 {
    println("hello world")
}

可简写为:

for range sl1 {
    println("hello world")
}

迭代 string 类型

Go 的字符串本质是字节序列(通常按 UTF-8 编码约定)。经典 for​ 循环通常按字节遍历;for range​ 会按 UTF-8 解码后的 Unicode 码点(rune)遍历。

经典 for:按字节遍历

s := "Go语言"

for i := 0; i < len(s); i++ {
    fmt.Printf("i=%d b=%#x char=%q\n", i, s[i], s[i])
}
  • s[i]​ 类型是 byte​(uint8​),表示第 i 个字节。

  • 非 ASCII 字符会占用多个字节,因此逐字节打印会出现“乱码样”的输出。

for range:按 rune 遍历,并给出该 rune 的字节起始下标

s := "Go语言"

for i, r := range s {
    fmt.Printf("i=%d r=%U char=%q\n", i, r, r)
}
  • r​ 类型是 rune​(int32),表示解码后的 Unicode 码点。

  • i 是该码点在原字符串中的字节起始下标,不是“第几个字符”。

  • 迭代步长取决于该 rune 的 UTF-8 字节长度(1~4)。

常见差异:len(s) 与“字符数”

s := "语言"
fmt.Println(len(s))                    // 6(字节数)
fmt.Println(utf8.RuneCountInString(s)) // 2(rune 数)

len​ 永远返回字节数;range 的迭代次数等于 rune 数。注意:rune 数也不一定等于“用户感知字符数”(某些 emoji/组合字符由多个码点组成)。

迭代 map 类型

遍历 map 常用 for range

var m = map[string]int{
    "Rob":  67,
    "Russ": 39,
    "John": 29,
}
for k, v := range m {
    println(k, v)
}

注意:map 的遍历顺序是随机的(实现层面刻意打乱),不要依赖其顺序。

迭代 channel 类型

for range 也可用于 channel:不断接收数据,直到 channel 被关闭并且缓冲区被读空。

var c = make(chan int)
for v := range c {
    // ...
    _ = v
}

当 channel 暂时无数据可读时会阻塞等待;当 channel 关闭且无更多值可读时循环结束。

迭代整型(Go 1.22+)

自 Go 1.22 起,for range 后可以跟整数表达式,表示循环执行对应次数:

package main

import "fmt"

func main() {
    n := 5
    for i := range n {
        fmt.Println(i)
    }
}

会先对 range​ 表达式求值;若结果为 n​,则循环变量 i​ 取值范围为 0​ 到 n-1

带 label 的 continue 语句

当需要中断本次迭代并进入下一次迭代时,可使用 continue

var sum int
var sl = []int{1, 2, 3, 4, 5, 6}
for i := 0; i < len(sl); i++ {
    if sl[i]%2 == 0 {
        // 忽略切片中值为偶数的元素
        continue
    }
    sum += sl[i]
}
println(sum) // 输出:9

Go 支持带 label​ 的 continue,用于跳转到指定循环的下一次迭代:

func main() {
    var sum int
    var sl = []int{1, 2, 3, 4, 5, 6}
loop:
    for i := 0; i < len(sl); i++ {
        if sl[i]%2 == 0 {
            continue loop
        }
        sum += sl[i]
    }
    println(sum) // 输出:9
}

label 常用于复杂的嵌套循环,直接跳转到外层循环继续下一次迭代:

func main() {
    var sl = [][]int{
        {1, 34, 26, 35, 78},
        {3, 45, 13, 24, 99},
        {101, 13, 38, 7, 127},
        {54, 27, 40, 83, 81},
    }
outerloop:
    for i := 0; i < len(sl); i++ {
        for j := 0; j < len(sl[i]); j++ {
            if sl[i][j] == 13 {
                fmt.Printf("found 13 at [%d, %d]\n", i, j)
                continue outerloop
            }
        }
    }
}

break 语句的使用

break 用于退出当前循环:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    firstEven := -1
    // 找出整型切片 sl 中的第一个偶数
    for i := 0; i < len(sl); i++ {
        if sl[i]%2 == 0 {
            firstEven = sl[i]
            break
        }
    }
    println(firstEven) // 输出:6
}

在嵌套循环中,也可以配合 label 一次性退出外层循环:

var gold = 38

func main() {
    var sl = [][]int{
        {1, 34, 26, 35, 78},
        {3, 45, 13, 24, 99},
        {101, 13, 38, 7, 127},
        {54, 27, 40, 83, 81},
    }
outerloop:
    for i := 0; i < len(sl); i++ {
        for j := 0; j < len(sl[i]); j++ {
            if sl[i][j] == gold {
                fmt.Printf("found gold at [%d, %d]\n", i, j)
                break outerloop
            }
        }
    }
}

switch语句

基本使用

Go 的 switch 用于多分支场景,其一般形式如下:

switch initStmt; expr {
case expr1:
    // 执行分支1
case expr2:
    // 执行分支2
case expr3_1, expr3_2, expr3_3:
    // 执行分支3
case expr4:
    // 执行分支4
// ...
case exprN:
    // 执行分支N
default:
    // 执行默认分支
}
  • initStmt​(可选):在进入 switch 前执行的初始化语句,常用于就近声明变量。

  • expr​:switch 表达式。

  • case:每个分支可跟一个表达式或逗号分隔的表达式列表。

  • default​:当没有任何 case 匹配时执行;不论其位置在哪里,只有在无匹配时才会执行。

执行流程:

  1. 计算 expr 的值。

  2. 按书写顺序依次比较每个 case​ 的表达式(或表达式列表中的每一项)是否与 expr 相等。

  3. 命中第一个匹配分支后执行其代码,并退出 switch​;若无匹配则执行 default(如存在)并退出。

灵活性

switch​ 中的比较要求相关类型可比较(comparable)。例如结构体只要其所有字段都可比较,就可以用于 switch​/case 的相等比较:

type person struct {
    name string
    age  int
}

func main() {
    p := person{"tom", 13}
    switch p {
    case person{"tony", 33}:
        println("match tony")
    case person{"tom", 13}:
        println("match tom")
    case person{"lucy", 23}:
        println("match lucy")
    default:
        println("no match")
    }
}

switch​ 省略表达式时,等价于 switch true { ... }​,常用于用多个布尔条件替代冗长的 if-else if 链:

// 包含 initStmt 的 switch
switch initStmt; {
case boolExpr1:
case boolExpr2:
    // ...
}

// 不包含 initStmt 的 switch
switch {
case boolExpr1:
case boolExpr2:
    // ...
}

switch​ 支持在 initStmt​ 中声明仅在该 switch 隐式代码块内有效的临时变量,有助于缩小变量作用域并提升可读性。

case 支持表达式列表,便于多个值复用同一段逻辑:

func checkWorkday(a int) {
    switch a {
    case 1, 2, 3, 4, 5:
        println("It is a work day")
    case 6, 7:
        println("it is a weekend day")
    default:
        println("Do you live on Earth")
    }
}

Go 的 switch​ 默认不会贯穿到下一个 case​(不会隐式 fallthrough)。如需执行下一分支代码,可显式使用 fallthrough

func case1() int {
    println("eval case1 expr")
    return 1
}
func case2() int {
    println("eval case2 expr")
    return 2
}
func switchexpr() int {
    println("eval switch expr")
    return 1
}
func main() {
    switch switchexpr() {
    case case1():
        println("exec case1")
        fallthrough
    case case2():
        println("exec case2")
        fallthrough
    default:
        println("exec default")
    }
}

注意:

  • fallthrough​ 不会重新判断下一个 case 的表达式,而是直接进入下一个分支执行其代码。

  • fallthrough​ 不能出现在 switch​ 的最后一个分支(且后面没有 default)中,否则会编译报错。

type switch

什么是 type switch

type switch​ 用于对接口值的动态类型做分支判断,只能使用形式 x.(type)​,并且只能出现在 type switch​ 中。它适用于当你持有一个 any​(interface{})或某个接口类型值时,需要根据其运行时具体类型执行不同逻辑。

基本语法

switch v := x.(type) {
case T1:
    // v 的类型是 T1
case T2:
    // v 的类型是 T2
case T3:
    // v 的类型是 T3
default:
    // 其他类型;v 的类型与 x 相同(接口类型)
}

要点:

  • x​ 必须是接口类型表达式(例如 any​ / interface{} / 自定义接口)。

  • .(type)​ 只能用于 type switch,不能单独使用。

  • v :=​ 可选;也可写为 switch x.(type) { ... }

匹配规则(按动态类型匹配)

  • x​ 的动态类型与某个 case 类型相同,则匹配该分支。

  • case nil​ 可用于匹配接口值本身为 nil 的情况:

    • var x any = nil​:命中 case nil

    • var p *int = nil; var x any = p​:接口值非 nil(动态类型为 *int​),命中 case *int​,而不是 case nil

示例:

func f(x any) {
    switch x.(type) {
    case nil:
        fmt.Println("nil interface")
    case *int:
        fmt.Println("*int (may be nil pointer inside interface)")
    }
}

与类型断言对比

类型断言用于判断某一个目标类型:

v, ok := x.(T)

type switch 用于判断多个候选类型并分支处理:

switch v := x.(type) { /* ... */ }

case 中能写哪些类型

  • 具体类型:int​、string​、MyStruct

  • 指针类型:*MyStruct

  • 接口类型:如 io.Reader(表示动态类型实现了该接口)

  • 也可写 case any​ 作为几乎兜底的分支(通常用 default 更清晰)

分支内变量 v 的类型

case T:​ 分支内,v​ 会被视为 T,可直接调用该类型的方法或访问字段:

switch v := x.(type) {
case string:
    fmt.Println(len(v))
case fmt.Stringer:
    fmt.Println(v.String())
}

常见用途与注意事项

  • 反序列化/解码后(any)做类型分发

  • 对错误类型按具体实现做分支处理

  • 统一入口处理多种输入类型

注意:

  • type switch 只能用于接口值。

  • 区分“接口是否为 nil”与“接口中持有的指针是否为 nil”。

  • 分支过多时,可考虑通过“接口 + 方法”替代类型分发以降低耦合。

0
博主关闭了所有页面的评论