目 录CONTENT

文章目录
go

【Go】控制结构

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

if语句

基本使用

Go中的if语句格式如下:

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

Go的if​语句的左大括号必须和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 boolean_expression {
    // 分支1
} else {
    // 分支2
}

if boolean_expression1 {
    // 分支1
} else if boolean_expression2 {
    // 分支2
} else if boolean_expression3 {
    // 分支3
} else {
    // 分支4
} 

其中多分支结构等效于以下二分支结构的嵌套组合:

if boolean_expression1 {
    // 分支1
} else {
    if boolean_expression2 {
        // 分支2
    } else { 
        if boolean_expression3 {
            // 分支3
        } else {
            // 分支4
        } 
    }
}

多分支结构在判断时会按照书写顺序依次评估每个布尔表达式的值,一旦发现某个布尔表达式的值为true​,则直接执行对应的代码块并停止后续的布尔表达式评估,因此在实际开发中,应将最有可能成立的布尔表达式放在前面,可以最大限度地减少后续分支的评估,从而提升if语句的整体执行效率。

自用变量

无论是单分支、二分支还是多分支结构,都可以在if​之后,布尔表达式之前声明一些变量声明,并与逻辑表达式使用分号;隔开,这些变量的作用域为其各自的隐式代码块中:

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)
    }
}

在声明这样的变量时,可能涉及到变量遮蔽问题,在实际使用时需要注意。

快乐路径

if语句的单分支、二分支和多分支结构的可读性依次递减,因此在日常编程中要减少多分支结构甚至二分支结构的使用以编写出更加优雅简洁的代码。

Go推荐采用以下形式的单分支语句结构:

func doSomething() error {
    if errorCondition1 {
    // 错误处理逻辑
    …
    return err1
    }
 
    // 成功处理逻辑
    …
    if errorCondition2 {
        // 更多错误处理逻辑
        …
        return err2
      }
    // 更多成功处理逻辑
    …
    return nil
}

这样的结构有以下特点:

  • 仅使用单分支控制结构
  • 当布尔表达式的值为false时,在单分支中快速返回。
  • 正常逻辑在代码布局上始终“靠左”排列,使读者可以从上到下看到函数正常逻辑的全貌。
  • 函数执行到最后一行代表一种成功状态。

这种方式被称为快乐路径,指的是那些代表成功的代码执行路径。

for语句

和其他主流编程语言不同,Go仅提供了for作为循环语句。

经典使用形式

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

可以拆解为以下语句:

image

其中i := 0​对应的组成部分在循环体之前执行,并且在整个for​语句中仅执行一次,也被称为循环前置语句。 通常用于声明循环变量或迭代变量,例如这里的整型变量i​。与if​语句类似,for​循环变量的作用域仅限于for语句所在的隐式代码块范围内。

i<0​对应的组成部分是用于决定循环是否继续进行的条件判断表达式。 这个布尔表达式的值为true​时,程序将进入循环体中执行;若值为false,则循环结束,循环体与循环后置语句均不再执行。

sum += i​对应的组成部分是for​语句的循环体,如果条件判断表达式的值为true,循环体就会被执行,每次这样的执行称为一次迭代。

i++​对应的组成部分会在每次循环体迭代之后执行,也被称为循环后置语句。通常用于更新循环变量。

Go的循环前置语句中支持声明多个循环变量,并应用于循环体及条件判断中。除了循环体是必须的以外,其余三个部分都是可选的,如果省略了循环前置语句或循环后置语句,for语句中的分号仍然需要保留,但是如果同时省略了循环前置语句和循环后置语句,那么可以省略分号,只保留条件判断表达式:

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

这种形式在日常编程中经常使用,当循环条件判断表达式的值始终为true时,可以进一步省略条件判断表达式:

for {
    // 循环体代码
}

for range循环

在Go中,对于切片的遍历可以使用以下方式:

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

这种遍历方式相对比较繁琐,需要自行定义循环前置语句和循环后置语句,并控制循环控制变量的自增,Go针对这种情况提供了更为简洁的形式:for range,上述代码等价于:

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

i​和v​对应的是经典for​语句形式中循环前置语句中的循环变量,它们的初始值分别为切片sl​的第一个元素的下标和该元素的值。隐含在for range​语义中的循环判断条件为是否已经遍历完切片sl​的所有元素。每次迭代后,for range​会自动取出切片sl​的下一个元素的下标和值,并分别赋值给循环变量i​和v

for range的变种

for range​可以同时遍历索引和值,但由于Go的变量检查机制,声明的变量必须被使用,如果实际上我们并不需要使用key​或value,可以使用下划线代替:

只需要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类型

对于string​类型,每次循环得到的value​是一个Unicode字符码点(rune类型),而不是字节。返回的第一个值i为该Unicode字符码点第一个字节在字符串内序列中的位置。使用经典for语句和for range语句形式对string类型进行循环操作的语义不同,

核心差异在于:​Go 的字符串是字节序列(UTF-8 编码约定),for range按“Unicode 码点(rune)”解码遍历,而经典 for循环通常按“字节(byte)”遍历(除非你自己在循环里做解码)。

下面用例子对比说明。


1) 经典 for i := 0; i < len(s); i++:按字节遍历
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 字符(如“语”“言”),UTF-8 会占多个字节,所以你会看到“乱码样”的单字节输出(因为 %q 在打印一个 byte 时只是把该字节当成字符展示)。

适用场景:你确实要处理​原始字节(协议、哈希、加密、按字节扫描等)。


2) for range:按 rune(Unicode 码点)遍历,并给出字节起始下标
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 在原字符串中的​字节起始下标
  • 每次迭代 i 会跳过该 rune 对应的 UTF-8 字节长度(1~4 字节)。

适用场景:你要按“人看到的字符/码点”处理字符串(分类、计数、遍历打印、Unicode 判断等)。


3) 一个最常见的坑:len(s) vs “字符数”
s := "语言"
fmt.Println(len(s))                    // 6(字节数)
fmt.Println(utf8.RuneCountInString(s)) // 2(rune 数)

len​ 永远是​字节数​;range​ 的次数等于​rune 数(注意:rune 数也不等于“用户感知字符数”,比如某些 emoji 由多个码点组成)。

迭代map类型

在Go中,唯一能够用于遍历map的方式是使用for range

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

迭代channel类型

除了string​、数组/切片及map类型变量以外,for range​还可以与channel类型配合使用。当channel作为for range​语句的操作对象时,for range语句会尝试从channel读取数据,其使用形式如下:

var c = make(chan int)
for v := range c {
    …
}

每次从channel读取的数据会被赋值给循环变量v​,并进入循环执行。如果channel中暂时没有数据可读,for range语句会在读操作出阻塞,直到channel关闭为止。

迭代整型

自Go1.22版本起,for range语句后可以跟整形表达式:


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

在迭代整型时,会先对range​表达式求值,对于整数求值结果,for range​语句会迭代对应的次数,循环变量的变化范围为0~n-1

lable​的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中还为continue​增加了label​标签的支持,label用于标记跳转目标位置,以上代码可以改为:

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语句的使用

对于continue​语句来说,无论是否带label,循环都会继续执行,但在日常开发中,有时需要中断整个循环,可以使用break语句:

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

对于嵌套的循环,如果需要退出内层循环的同时退出外层循环,也可以使用以下方式:

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​语句之前初始化一些变量。在大括号之后是一系列代码执行分支,每个分支以case​关键字开头,后面跟随一个表达式或一组由逗号分隔的表达式列表。此外还有一个默认分支default

执行流程为:

  1. switch​语句计算expr​的值,并一次将其余每个case中的表达式的值进行比较。
  2. 如果找到匹配的case​,即case​后面的表达式或者表达式列表中的任意一个表达式的值与expr​的值相等,那么执行对应的代码分支,随后退出switch语句。
  3. 如果所有case​表达式都无法与expr​表达式的结果匹配,那么程序会执行default​默认分支,并且结束switch语句

无论default​出现在什么位置,没有任何case匹配的情况下才会被执行。

灵活性

switch语句中的各表达式求值结果可以是各种类型的值,只要改类型支持比较操作即可

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​语句中表达式的类型为布尔类型时,如果求值结果始终为true​,那么可以省略switch语句之后的表达式:

// 包含initStmt语句的switch语句
switch initStmt; {
    case bool_expr1:
    case bool_expr2:
    …
}
// 不包含initStmt语句的switch语句
switch {
    case bool_expr1:
    case bool_expr2:
    …
}

switch语句支持声明临时变量

和if、for等控制结构语句一样,switch语句允许在其初始化语句(initStmt)中声明仅在该switch语句隐式代码块内有效的临时变量。这种就近声明的方式最大限度地缩小了变量的作用域。

case语句支持表达式列表

可以用表达式列表实现多个表达式共用同样的逻辑,而不是像其他语言一样一个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")
    }
}

取消了默认执行下一个case语句代码逻辑的语义。

Go中每个case​分支代码执行完成后会自动结束switch​语句,而不是自动执行下一个case​,如果实际使用中需要执行下一个case​分支的代码,可以使用关键字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")
    }
}

如果某个case​语句已经是switch​中的最后一个,并且后面没有default​分支,那么中该case​语句中不能使用fallthrough,否则会导致编译器错误。

type switch

什么是 type switch

type switch​ 用于对接口值的动态类型进行分支判断,只能写在 switch​ 后面使用形式 x.(type)​。它解决的问题是:当你手里有一个 interface{} 或某个接口类型值时,想根据其“运行时具体类型”执行不同逻辑。


基本语法

switch v := x.(type) {
case T1:
    // v 的类型是 T1
case T2, T3:
    // v 的类型是 T2 或 T3(此分支里 v 的静态类型是它们的共同接口或 switch 变量的类型,常见写法是分别写 case)
default:
    // 其他类型;v 的类型与 x 相同(接口类型)
}

要点:

  • x​ 必须是接口类型表达式(例如 any​ / interface{} / 自定义接口)。
  • .(type)只能出现在 type switch 中,不能单独使用。
  • v :=​ 可选;不写则用 switch x.(type) { ... }

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

  • x​ 的动态类型正好是某个 case 的类型,则匹配该分支。

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

    • var x any = nil​:动态类型为 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)) // v 是 string
case fmt.Stringer:
    fmt.Println(v.String()) // v 是 fmt.Stringer
}

常见用途

  • 解码/反序列化后(例如 any)做类型分发
  • 对错误类型做分支处理(按具体 error 类型)
  • 统一入口处理多种输入类型(string/[]byte/struct 等)

常见坑与注意事项

  • 只能用于接口值​:对非接口变量不能做 .(type)
  • 注意 nil 的两层含义:接口是否为 nil 与“接口里装的指针是否为 nil”是两回事。
  • 分支过多时,考虑用“接口+方法”替代类型分发(更符合面向接口设计)。

0

评论区