Go 运算符与表达式
Go 如何用运算符与表达式计算值:优先级、算术与溢出、比较与可比较性、短路逻辑、取址与 channel 接收,以及 ++/-- 为何是语句而非表达式。
[!info] Related notes
- 所属 MOC:Go 语言基础 MOC · Go 学习路线 MOC
- 前置概念:Go 基本类型、变量、常量与零值
- 后续概念:Go 流程控制、Go 函数、Go 指针、Go channel
- 容易混淆:类型转换与可赋值性、range 语义
Go 运算符与表达式
这个概念为什么会出现
学会声明变量和基本类型之后,下一步几乎立刻会碰到这些问题:
1 + 2 * 3到底是 7 还是 9?a && b()在a为 false 时,会不会还调用b?- 两个不同类型的整数能不能直接相加?
i++能不能写进更大的表达式?int8加到越界时会 panic 吗?- 字符串能不能用
+拼接? <-ch是表达式还是语句?
如果没有一套稳定的表达式求值规则,控制流、函数参数、错误判断和并发通信都会建立在含糊直觉上。
运算符与表达式不是“语法点缀”,而是 Go 计算值的基本规则层:它规定哪些运算合法、结果类型是什么、何时求值、何时短路、何时 panic。
[!abstract] 一句话理解 表达式用操作数和运算符计算出一个值;Go 的运算符集合刻意克制,优先级层级少,逻辑运算符短路,而
++/--与赋值是语句,不是可嵌入任意位置的表达式。
最小可运行示例
下面分别观察优先级、括号、短路条件和字符串拼接。保存为 main.go 后执行 go run main.go。
package main
import "fmt"
func main() {
a := 1 + 2*3
b := (1 + 2) * 3
ok := a > 5 && b < 20
text := "go" + "lang"
fmt.Println(a, b, ok, text)
}
输出:
7 9 true golang
乘法先于加法结合,括号改变结合方式;比较结果用于 &&,字符串 + 表示拼接。
核心概念与准确模型
表达式是什么
表达式通过对操作数应用运算符和函数来计算值。
操作数可以是:
- 字面量:
42、3.14、"hi"、true - 标识符:变量、常量、函数
- 选择、索引、切片、类型断言、函数调用
- 括号表达式:
(x + y)
比较运算产生无类型布尔值;逻辑运算要求布尔操作数,结果与操作数类型相同,可以保留自定义布尔类型。常量表达式在编译期求值,且精度规则与运行时变量算术不同(见后文溢出)。
运算符分类
| 类别 | 运算符 | 主要作用对象 |
|---|---|---|
| 算术 | + - * / % | 数值;+ 还可用于 string |
| 位运算 | & | ^ &^ << >> | 整数 |
| 比较 | == != < <= > >= | 可比较 / 可排序类型 |
| 逻辑 | && || ! | 布尔 |
| 取址/解引用 | & * | 可寻址值 / 指针 |
| 接收 | <- | channel(作一元表达式) |
| 赋值类 | = := += … | 语句,不是表达式运算符 |
| 自增自减 | ++ -- | 语句,且只能后缀 |
规范中的二元运算语法可概括为:
Expression = UnaryExpr | Expression binary_op Expression
UnaryExpr = PrimaryExpr | unary_op UnaryExpr
binary_op = "||" | "&&" | rel_op | add_op | mul_op
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-"
优先级与结合性
一元运算符优先级最高。二元运算符只有五级(数字越大绑定越紧):
| 优先级 | 运算符 |
|---|---|
| 5 | * / % << >> & &^ |
| 4 | + - | ^ |
| 3 | == != < <= > >= |
| 2 | && |
| 1 | || |
规则:
- 同级二元运算符从左到右结合:
x / y * z等于(x / y) * z - 可用括号强制顺序
++/--不是表达式,不参与优先级表- 赋值不是表达式,因此没有“赋值表达式的优先级”
// Effective Go 常举的例子:因为 + 优先级低于 <<,
// 下面更接近“先移位再相加”的阅读直觉
sum := x<<8 + y<<16
flowchart TB
subgraph unary["一元(最高)"]
U["+ - ! ^ * & <-"]
end
subgraph p5["优先级 5"]
M["* / % << >> & &^"]
end
subgraph p4["优先级 4"]
A["+ - | ^"]
end
subgraph p3["优先级 3"]
R["== != < <= > >="]
end
subgraph p2["优先级 2"]
AND["&&"]
end
subgraph p1["优先级 1"]
OR["||"]
end
unary --> p5 --> p4 --> p3 --> p2 --> p1
算术运算
适用类型
| 运算 | 整数 | 浮点 | 复数 | 字符串 |
|---|---|---|---|---|
+ | ✓ | ✓ | ✓ | ✓(拼接) |
- * / | ✓ | ✓ | ✓ | ✗ |
% | ✓ | ✗ | ✗ | ✗ |
| 位运算 / 移位 | ✓ | ✗ | ✗ | ✗ |
算术运算的结果类型通常与操作数类型一致;两个操作数必须类型兼容(常见要求是相同类型,或一方是可表示的无类型常量)。不同命名整数类型之间不会像 C 那样悄悄提升,通常需要显式转换。见 类型转换与可赋值性。
整数除法与取余
整数 / 向零截断:
5 / 3 == 1
-5 / 3 == -1
5 / -3 == -1
-5 / -3 == 1
取余满足:
(a/b)*b + a%b == a
非零余数的符号跟随被除数 a:
5 % 3 == 2
-5 % 3 == -2
5 % -3 == 2
-5 % -3 == -2
最小有符号整数除以 -1 是溢出特例:运行时 var x int8 = -128,x / -1 仍为 -128,x % -1 为 0;若写成常量运算,结果 128 不能表示为 int8,编译失败。
除零
| 运算 | 行为 |
|---|---|
| 常量表达式除以 0(包括浮点) | 编译错误 |
运行时整数 / 或 %,除数为 0 | panic |
| 运行时浮点除以 0 | 当前 Go 工具链可得到 ±Inf 或 NaN;规范将是否 panic 留给实现 |
溢出:常量 vs 变量
这是初学者最容易混淆的分界:
| 上下文 | 溢出行为 |
|---|---|
| 常量表达式 | 精确计算;装不进目标类型 → 编译错误 |
| 变量整数算术 | 按补码回绕;+ - * << 的有符号溢出不 panic |
| 运行时浮点运算 | 可出现 Inf / NaN |
const tooBig int8 = 128 // 编译错误:常量溢出
var x int8 = 127
x = x + 1 // 合法,结果为 -128(回绕)
规范明确:有符号整数的 +、-、*、<< 可以合法溢出,结果由表示法与操作数确定,不会因此触发运行时 panic。
位运算与移位
| 运算符 | 含义 |
|---|---|
& | 按位与 |
| | 按位或 |
^ | 按位异或;一元 ^x 为按位取反 |
&^ | bit clear:x &^ y 等于 x & (^y) |
<< | 左移 |
>> | 右移;有符号数为算术右移(符号扩展) |
移位要点(规范):
- 左操作数是整数(或可表示为整数的无类型常量)
- 右操作数可以是任意整数类型(Go 1.13 起允许有符号类型),或可表示为
uint的无类型常量;负常量计数编译失败,运行时负计数 panic - 移位计数 ≥ 左操作数类型位宽时,结果有明确定义(通常为 0;有符号负数算术右移可为 -1)
- 优先级与结合性不等于所有操作数的求值顺序;普通变量读取与调用的相对顺序不能仅从左右位置推断
fmt.Printf("%04b\n", 0b1111&^0b0101) // 1010,清掉 y 中为 1 的位
比较运算
比较结果是布尔值。
==/!=:要求类型可比较(comparable)< <= > >=:要求类型可排序(ordered)——整数、浮点数、字符串
| 类型 | 可比较 | 可排序 |
|---|---|---|
| bool | ✓ | ✗ |
| 整数 / 浮点 | ✓ | ✓ |
| complex | ✓ | ✗ |
| string | ✓ | ✓(字典序,按字节) |
| pointer | ✓ | ✗ |
| channel | ✓ | ✗ |
| interface | ✓(动态类型不可比较时可能 panic) | ✗ |
| struct | 字段均可比较则 ✓ | ✗ |
| array | 元素可比较则 ✓ | ✗ |
| slice / map / function | 只能与 nil 比较 | ✗ |
接口比较的细节、typed nil 等问题见 接口值与 nil。
slice、map 的相等性不要靠 == 判断内容。元素/值可比较时可用 slices.Equal / maps.Equal;其他情况用对应的 EqualFunc 或明确的比较逻辑。这些函数不会自动递归比较任意嵌套容器。
逻辑运算与短路
&& 条件与:左为 false 则不再求右
|| 条件或:左为 true 则不再求右
! 逻辑非
操作数必须是布尔。短路是语言规则,不是优化可选项:
if p != nil && p.ready {
// 仅当 p != nil 时才读取 p.ready
}
这直接支撑了 Go 常见的“先判空再读字段/再调方法”写法。
取址、解引用与 channel 接收
| 写法 | 角色 | 说明 |
|---|---|---|
&x | 表达式 | 取 x 的地址;x 必须可寻址,或为复合字面量 |
*p | 表达式 | 解引用;p == nil 时 panic |
<-ch | 表达式 | 从 channel 接收 |
ch <- v | 语句 | 向 channel 发送 |
可寻址性的完整讨论见 Go 指针;channel 语义见 Go channel。本篇只需建立边界:
- 接收可以出现在赋值右侧、函数参数等表达式位置
- 发送不能当作“返回值的表达式”嵌入
v := <-ch // 表达式
v, ok := <-ch // 多值接收,ok 为 false 仅表示通道已关闭且已耗尽;关闭后的缓冲值仍返回 true
ch <- v // 语句
赋值、自增:语句不是表达式
a = a + 1
a += 1
a++
关键限制:
++/--只能后缀,没有前缀++i- 它们是语句,不能写
x = i++、f(i++) a op= b语义上类似a = a op b,但a只求值一次- 左操作数须可寻址,或为 map 索引表达式
这与 C/JavaScript 不同:Go 故意禁止把自增塞进更大表达式,以减少“既改状态又产值”的阅读负担。需要交换或并行更新时,用多值赋值:
a, b = b, a
i, j = i+1, j-1
求值顺序
规范对求值顺序的核心保证是:
- 表达式、赋值、return 中的函数调用、方法调用、通信操作按词法从左到右发生
&&/||的右操作数按需求值- 赋值分两阶段:先按通常求值规则计算左侧索引/指针解引用的操作数及右侧表达式,再从左到右执行各项赋值
- 普通变量读取等非调用操作数与调用之间的相对顺序,在某些情形下未指定;移位也没有额外的“先左后右”通用保证
// 第一阶段确定左侧下标和右侧值;第二阶段从左到右赋值
i, x[i] = 1, 2 // 使用旧的 i 作为下标,然后 i 变成 1
不要依赖未指定顺序的副作用。需要确定顺序时,拆成多条语句。
从表面现象深入:规范保证与实现细节
本主题主要是语言规则,不必先下沉 Runtime。但仍要分清三层:
| 层次 | 例子 |
|---|---|
| 规范保证 | 短路、整数除法向零、变量整数溢出回绕、++ 是语句 |
| 当前实现 | 具体 CPU 指令如何实现移位与溢出;编译器是否折叠常量 |
| 理解用模型 | “表达式树从内到外计算” |
常量表达式在编译期以更高精度处理;实现至少需支持很大的整数常量精度(规范对编译器表示能力有下限要求)。因此:
const c = 1 << 100 // 可作为无类型常量存在
var n int = c // 若 int 装不下,编译失败
字符串 + 每次都产生新字符串值;编译器可能做拼接优化,但语义上字符串不可变。性能敏感路径应优先 strings.Builder 等 API,而不是臆测实现。
设计动机:Go 为什么这样设计
-
优先级层级少
只有五级二元优先级,并让移位与乘除同级、加减与位或/异或同级,减少“记一张巨表”的成本。Effective Go 也指出,因此常常比 C 少写括号。 -
++/--降为语句
去掉“在表达式里偷偷改状态”的写法,强制副作用显眼。 -
几乎不做数值类型提升
不同类型整数不能直接混算,把转换交给程序员。这与“显式优于隐式”一致,也和 可赋值性规则 一脉相承。 -
短路逻辑成为惯用防护
p != nil && p.ok是语言级安全阀,而不是风格建议。 -
溢出不 panic(变量整数)
这是语言规定的定宽整数行为,不等于业务计算安全;需要检测溢出时应显式检查。 -
发送是语句、接收是表达式
呼应 channel 的使用形态:接收常进入变量,发送是流程中的动作。
边界情况与反直觉行为
1. 不同类型不能直接运算
var a int32 = 1
var b int64 = 2
// sum := a + b // 编译错误
sum := int64(a) + b
2. 常量除零与运行时除零不同
// var i = 1 / 0 // 常量除零:编译错误
var z int
// _ = 1 / z // 运行时 panic
// var f = 1.0 / 0.0 // 同样是常量除零,编译错误
var zero float64
fmt.Println(1.0 / zero, zero / zero) // 当前工具链输出 +Inf NaN
3. 无类型常量很“宽”,一进变量就变“窄”
const n = 1 << 40
var x int32 = 1
// _ = x + n // 若 n 不能表示为 int32,编译错误
_ = x + int32(1<<20)
4. 有符号右移是算术右移
var n int8 = -8
fmt.Println(n >> 1) // -4,不是把符号位逻辑挪成大正数
5. slice / map / func 不能直接 == 比内容
// []int{1} == []int{1} // 非法
var s []int
fmt.Println(s == nil) // 只能与 nil 比
6. 接口比较可能 panic
当两个接口动态类型相同,但该类型不可比较(例如动态类型是 slice)时,== 可能 panic。日常代码应优先用类型断言或专门比较函数,而不是对任意 any 直接 ==。
7. &^ 容易被忽略
flags := 0b1111
flags = flags &^ 0b0100 // 关掉某一位
这是 Go 特有的 bit clear 运算符,读写位标志时很有用。
常见误区
[!warning] 常见误区:把
i++当表达式 错误理解:f(i++)、x = i++像 JS/C 一样合法。
正确模型:i++是语句,只产生副作用,不产生可用值。
原因:从其他语言迁移时的习惯残留。
[!warning] 常见误区:以为整数溢出会 panic 错误理解:
var x int8 = 127后执行x + 1会 panic。 正确模型:变量整数算术回绕;int8(127)+1则是有类型常量溢出,编译失败。 原因:把“安全算术”和“语言默认算术”混为一谈。
[!warning] 常见误区:用
&&/||连接非布尔值 错误理解:像 JS 那样x && y返回操作数本身。
正确模型:两端都必须是布尔,结果也是布尔。
原因:动态语言的 truthy 模型不适用。
[!warning] 常见误区:忘记乘法与移位同级 错误理解:
1 << 2 + 3一定是1 << (2+3)。
正确模型:<<与+不同级——<<更高,该式为(1<<2)+3。
原因:与某些语言的移位优先级直觉冲突。不确定就加括号。
[!warning] 常见误区:用
==比较 slice 内容 错误理解:两个长度元素都相同的 slice 可用==。
正确模型:slice 只能与nil比较。
原因:把“容器值”当成数组值。
与相邻概念的对比
| 概念 | 关系 | 关键差别 |
|---|---|---|
| 基本类型 | 运算符作用的对象 | 类型决定哪些运算符合法 |
| 转换与可赋值性 | 混类型运算前的门槛 | 运算前常先转换,而不是靠隐式提升 |
| 流程控制 | 表达式为条件提供布尔值 | if/for/switch 使用表达式,但本身是语句结构 |
| 指针 | &/* 的完整规则 | 本篇只给运算符位置,可寻址性另述 |
| channel | <- 的发送/接收分工 | 接收是表达式,发送是语句 |
| 函数 | 调用是主表达式 | 参数中的调用/通信按词法顺序执行,普通读取的相对顺序未必确定 |
与 JavaScript 对比(仅辅助直觉):
| 点 | JavaScript | Go |
|---|---|---|
| 真值 | 大量 truthy/falsy | 只有 bool |
&&/|| 结果 | 可能返回操作数 | 返回布尔 |
++ | 可作前/后缀表达式 | 仅后缀语句 |
| 整数溢出 | Number 是浮点;BigInt 另论 | 定宽整数回绕 |
| 类型混算 | 常隐式转换 | 基本要求类型一致 |
工程实践
-
条件先写清布尔
if err != nil、if len(s) == 0比“玩运算符技巧”更符合 Go 风格。 -
复杂优先级就加括号
即使你记得优先级,读者未必记得。移位与掩码混合时尤其如此。 -
位运算集中封装
对 flag 的&、|、&^可收进小函数或方法,避免魔法数散落。 -
不要用溢出当特性
回绕是确定语义,不代表业务上可以依赖“加满归零”除非协议明确。 -
字符串拼接
少量+清晰可读;循环内大量拼接改用strings.Builder或bytes.Buffer。 -
比较接口与容器要谨慎
测试里比较结构体/切片内容,优先go-cmp、手写字段比较或专门 equal 函数,而不是依赖==的侥幸。 -
把副作用请出表达式
需要调用并检查错误时,用短变量声明 +if,不要把带副作用的调用嵌进复杂算术里。
常见条件写法:
// 仅在 n 不等于该整数类型最小值时可用来取绝对值
if n < 0 {
n = -n
}
if c >= '0' && c <= '9' {
return c - '0'
}
可验证实验
下列实验可在本地
go run/go test中完成。2026-10-07 使用 Go 1.26.8 核验优先级、短路、移位计数、除零、整数溢出、赋值和关闭通道行为;编译失败片段分别测试,不与可运行示例混编。语言保证依据 Go 语言规范,浮点除零输出仅记录该实现行为。
实验 1:优先级
fmt.Println(1+2*3, 1<<2+3, 1<<(2+3))
- 预测:
7、7、32 - 观察:是否与预测一致
- 结论:
*/<<高于+;括号改变顺序
实验 2:短路
func boom() bool { panic("evaluated") }
func main() {
fmt.Println(false && boom())
fmt.Println(true || boom())
}
- 预测:打印两个布尔值,不 panic
- 结论:右操作数未求值
实验 3:++ 不能当表达式
i := 0
// fmt.Println(i++) // 应编译失败
i++
fmt.Println(i)
实验 4:整数除法与溢出
fmt.Println(5/3, -5/3)
var x int8 = 127
x++
fmt.Println(x)
- 预测:
1、-1、-128
实验 5:常量溢出 vs 变量溢出
// const c int8 = 128 // 应编译失败
var y int8 = 127
y += 1 // 应运行成功并回绕
本节总结
- 本质:表达式按规则从操作数计算值;运算符定义合法运算、结果类型与求值方式。
- 最重要规则:五级优先级、左结合;
&&/||短路;变量整数溢出回绕而常量溢出编译失败;++/--/赋值是语句。 - 最容易错:把其他语言的隐式转换、truthy、
i++表达式用法直接搬进 Go。 - 下一步:用这些表达式写出清晰的 控制流,并在 函数 中处理多返回值与错误。
自测题
概念题
- 为什么说
++在 Go 里不是运算符表达式? - 变量
int8从 127 再加 1 会怎样?常量int8(128)呢? - 哪些类型可以用
<比较?slice 能否==另一个 slice? <-ch与ch <- v在语法角色上有何不同?
代码推理题
package main
import "fmt"
func main() {
fmt.Println(1<<2*3)
fmt.Println(true || panicBool())
}
func panicBool() bool {
panic("nope")
}
- 能否编译?
- 若能,输出是什么?会不会 panic?
- 若把
||改成&&,行为如何?
再看:
var a int32 = 3
var b int = 4
fmt.Println(a + b)
能否编译?为什么?
工程思考题
- 审核一段把多个带副作用函数嵌在同一复杂表达式中的代码,你会怎么改?
- 设计一个权限 bit flag API:公开
Add/Remove/Has,内部用|与&^,为什么比让调用方直接写位运算更好?
参考答案
点击展开
概念题
- 规范将
++/--定义为语句,不能出现在需要值的表达式位置。 - 变量回绕到 -128;常量 128 不能表示为
int8,编译错误。 - 整数、浮点、字符串可排序;slice 不能与另一个 slice 用
==比内容,只能与nil比。 <-ch是表达式;ch <- v是语句。
代码推理题
1<<2*3:<<与*同级,左结合,等价(1<<2)*3 = 12。true || panicBool()短路,不调用panicBool,打印true。- 改成
true && panicBool()会求右操作数并 panic。 int32 + int类型不同,编译错误,需显式转换。
工程思考题
- 拆成按序语句,避免未指定求值顺序与难测副作用。
- 封装位运算可集中校验、命名语义、避免魔数,并减少优先级误用。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Go Programming Language Specification — Operators | 官方规范 | 运算符集合、优先级、结合性 |
| Spec — Arithmetic operators | 官方规范 | 除法、取余、移位、溢出、字符串 + |
| Spec — Comparison operators | 官方规范 | 可比较 / 可排序类型 |
| Spec — Logical operators | 官方规范 | && ` |
| Spec — Address operators | 官方规范 | & * |
| Spec — Order of evaluation | 官方规范 | 求值顺序与未指定情形 |
| Spec — Integer overflow / Constants 相关章节 | 官方规范 | 常量精确性与变量回绕分界 |
| Effective Go — Control structures | 官方文档 | 少括号、清晰条件、++ 非表达式、并行赋值 |
| go.dev/ref/spec(语言版本以页面标注为准,检索时为 go1.26 文档线) | 官方规范总入口 | 全文交叉核对 |
未把第三方博客结论当作规范。实现级优化(例如字符串拼接是否被编译器改写)未写成语言承诺。