运算符与流程控制
需要预先了解:
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 的写法有三条约定:
- 条件不需要括号;
- 花括号必须有,即使只有一条语句;
- 条件必须是
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 练习
- 用三层嵌套循环和标签,打印出乘法表中"第一个大于 20 的乘积"并立即退出全部循环。
- 用无表达式
switch把一段if-else if链改写成更紧凑的形式,比较两者的可读性。 - 声明一个
[]struct{ Name string },先用for range修改Name观察是否生效,再用下标修改,解释两次输出的差异。



