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 的操作符有固定的优先级(高→低):
实际开发中,为避免复杂逻辑带来的误读,通常用括号将复合条件拆解为更清晰的子表达式,提高可读性并减少因优先级导致的误解。
除单分支结构外,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)
其组成部分可理解为:

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匹配时执行;不论其位置在哪里,只有在无匹配时才会执行。
执行流程:
计算
expr的值。按书写顺序依次比较每个
case 的表达式(或表达式列表中的每一项)是否与expr相等。命中第一个匹配分支后执行其代码,并退出
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”。
分支过多时,可考虑通过“接口 + 方法”替代类型分发以降低耦合。