需要预先了解:


1. 运算符与流程控制

Go 的流程控制关键字只有 if、for、switch、select(并发场景用),没有 while、没有 do...while。这套精简的设计带来了一些与直觉不同的细节:if 可以带初始化语句、switch 默认不穿透、++ 是语句而不是表达式。


1.1 运算符

算术运算符

运算符 含义 示例 结果
+ 加法 1 + 2 3
- 减法/取负 5 - 3 2
* 乘法 2 * 3 6
/ 除法 7 / 2 3
% 取余 7 % 2 1

整数除法会截断小数部分,取余的结果符号与被除数一致:

fmt.Println(7/2, 7%2, -7/2, -7%2)
3 1 -3 -1

除数为常量 0 是编译错误,除数为变量 0 会在运行时 panic(integer divide by zero)。

位运算符

运算符 含义 示例 结果
& 按位与 5 & 3 1
| 按位或 5 | 3 7
^ 按位异或 5 ^ 3 6
&^ 位清除 5 &^ 3 4
<< 左移 1 << 3 8
>> 右移 10 >> 1 5

&^ 是 Go 独有的位清除运算符:a &^ b 表示"把 a 中 b 为 1 的位清零",等价于 a & (^b)。

比较与逻辑运算符

比较运算符有 ==、!=、<、<=、>、>=,结果是 bool。逻辑运算符有 &&、||、!,其中 && 和 || 会短路求值:

fmt.Println(1 < 2 && 2 < 3, !(1 < 2), 1 < 2 || 3 < 2)
true false true

优先级

从高到低共五档:

优先级 运算符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||

同优先级左结合。实际写代码时不要依赖这张表,混用多个运算符时加括号更清楚。

++ 与 -- 是语句

自增自减在 Go 里是语句,不是表达式,因此不能出现在赋值右边、不能参与运算、没有前置形式:

i := 0
i++
fmt.Println("i =", i)
j := i++ // 编译错误
syntax error: unexpected ++ at end of statement

1.2 if

if 的写法有三条约定:

  1. 条件不需要括号;
  2. 花括号必须有,即使只有一条语句;
  3. 条件必须是 bool 类型,不会把 0、空字符串等当作假。
if v > 5 {
	fmt.Println("v 大于 5")
}

带初始化语句的 if

if 可以在条件之前放一条初始化语句,用分号隔开:

if v := 10; v > 5 {
	fmt.Println("v 大于 5")
} else {
	fmt.Println("v 不大于 5")
}

这里的 v 的作用域被限制在整个 if-else 结构内,出了花括号就不可见:

fmt.Println(v) // 编译错误:undefined: v

这个写法最常见的用途是把"取值 + 判错"写在一行:

if err := doSomething(); err != nil {
	return err
}

好处是 err 不会污染外层作用域,也避免了下一次调用时误用上一次的错误变量。


1.3 for

Go 只有 for 一个循环关键字,它有三种形态。

1. 三段式:

sum := 0
for k := 0; k < 5; k++ {
	sum += k
}
fmt.Println("sum =", sum) // 10

2. 只保留条件:省略初始化和后置语句,条件成立就继续循环:

n := 3
for n > 0 {
	n--
}

3. 无条件(死循环):

for {
	// 需要配合 break 或 return 退出
}

三段式中初始化和后置语句都可以省略:for ; i < 10; { } 与 for i < 10 { } 等价。


1.4 range

range 用于遍历数组、切片、map、字符串、channel,以及整数(1.22+)和迭代器函数(1.23+)。它有两个返回值:下标(或键)与元素值。

nums := []int{10, 20, 30}
for idx, val := range nums {
	fmt.Println(idx, val)
}
0 10
1 20
2 30

不需要的返回值用 _ 占位:

for idx := range nums { }   // 只要下标
for _, val := range nums { } // 只要值

range 对元素是值拷贝

这是最容易出错的一点:range 返回的第二个值是该元素的副本,修改它不会影响原切片:

type Point struct{ X, Y int }

points := []Point{{1, 2}, {3, 4}}
for _, p := range points {
	p.X = 100 // 改的是副本
}
fmt.Println(points) // [{1 2} {3 4}]

想修改原元素,要用下标:

for idx := range points {
	points[idx].X = 100
}
fmt.Println(points) // [{100 2} {100 4}]

同理,如果元素是数组或大结构体,用 for _, v := range 会复制一份完整数据。只读取时用下标访问 points[idx] 可以避免拷贝。

遍历字符串

range 遍历字符串时按 rune(Unicode 码点)进行,下标是字节偏移:

for idx, r := range "a中" {
	fmt.Println(idx, string(r))
}
0 a
1 中

第二个字符从字节下标 1 开始,因为它占 3 个字节,下一个下标是 4。字符串的编码细节见 map 与字符串。

注:Go 1.22 起,for 循环的循环变量每一轮迭代都是新变量。这个改动解决了一个长期的闭包捕获问题,细节见 函数 与 并发设计模式与陷阱总结。


1.5 switch

switch 的基本形态:

switch x {
case 1:
	fmt.Println("一")
case 2:
	fmt.Println("二")
default:
	fmt.Println("其他")
}

与很多语言的直觉不同,Go 的每个 case 默认自带 break,执行完当前分支就结束整个 switch,不会继续往下走。

fallthrough

需要"穿透"到下一个分支时,显式写 fallthrough。它的语义是无条件进入下一个 case 的第一条语句,不检查下一个 case 的条件:

switch 1 {
case 1:
	fmt.Println("进入 case 1")
	fallthrough
case 2:
	fmt.Println("fallthrough 进入 case 2")
case 3:
	fmt.Println("不会执行")
}
进入 case 1
fallthrough 进入 case 2

注意 fallthrough 之后 case 2 不再做匹配判断,而且它不能用在最后一个分支里(编译错误:cannot fallthrough final case in switch)。

多值 case

一个 case 可以列出多个候选值,用逗号分隔:

switch 2 {
case 1, 2, 3:
	fmt.Println("命中 1、2、3 之一")
}

无表达式 switch

省略 switch 后面的表达式,就相当于对 true 做匹配,每个 case 写一个条件:

score := 85
switch {
case score >= 90:
	fmt.Println("优秀")
case score >= 80:
	fmt.Println("良好")
default:
	fmt.Println("继续加油")
}

这种写法把一长串 if-else if 收成一张表,按从上到下的顺序匹配,命中即止。

带初始化语句

和 if 一样,switch 也支持初始化语句:

switch v := 5 * 2; v {
case 10:
	fmt.Println("v 是 10")
}

1.6 标签、break 与 continue

break 和 continue 默认只作用于最内层循环。嵌套循环里想跳出外层,需要给外层加标签(label),然后在 break/continue 后面带上标签名:

outer:
for a := 0; a < 3; a++ {
	for b := 0; b < 3; b++ {
		if b == 2 {
			continue outer // 跳过内层剩余迭代,继续外层下一轮
		}
		if a == 2 {
			break outer // 直接跳出两层循环
		}
		fmt.Println(a, b)
	}
}
0 0
0 1
1 0
1 1

标签必须紧贴在 for、switch、select 语句之前,并且从标签到语句之间不能有空行(否则标签会指向空语句)。

goto 可以无条件跳转到同一函数内的标签,但限制很严:不能跳过变量声明、不能跳到其他函数或代码块内部。日常开发中几乎用不到,主要用于生成代码或极少数状态机场景:

c := 0
loop:
	if c < 3 {
		fmt.Println("goto c =", c)
		c++
		goto loop
	}

1.7 类型 switch 预告

switch 还有一种特殊形式,用来判断接口值的动态类型:

switch v := i.(type) {
case int:
	fmt.Println("是 int:", v)
case string:
	fmt.Println("是 string:", v)
}

i.(type) 只能出现在 switch 的表达式位置。它的完整用法和接口类型断言一起放在 接口 讲解。


1.8 练习

  1. 用三层嵌套循环和标签,打印出乘法表中"第一个大于 20 的乘积"并立即退出全部循环。
  2. 用无表达式 switch 把一段 if-else if 链改写成更紧凑的形式,比较两者的可读性。
  3. 声明一个 []struct{ Name string },先用 for range 修改 Name 观察是否生效,再用下标修改,解释两次输出的差异。