Go 运算符与表达式
Go 如何用运算符与表达式计算值:优先级、算术与溢出、比较与可比较性、短路逻辑、取址与 channel 接收,以及 ++/-- 为何是语句而非表达式。
[!info] 关联笔记
- 所属 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 的运算符集合刻意克制,优先级层级少,逻辑运算符短路,而
++/--与赋值是语句,不是可嵌入任意位置的表达式。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:促销活动页算价与资格判断
运营配置:基础价 + 件数 * 单价 得到小计;
有时要先加运费再乘折扣档位(括号改顺序);
再判断“小计是否过门槛 && 库存是否够”;
最后把品牌前缀和活动名拼成展示文案。
package main
import "fmt"
// calcPromoLine 模拟“活动页一行商品的价格与资格”。
//
// 业务意图:
// 1. 按优先级算小计;
// 2. 用括号表达“先加运费再乘档位”;
// 3. 组合比较 + 逻辑与决定是否可下单;
// 4. 用 + 拼展示标题。
//
// 教学点:乘法优先、括号改结合、比较产生 bool、字符串 + 是拼接。
func calcPromoLine() {
// 基础价 1 + 件数 2 * 单价 3 → 先 2*3,再 +1,得 7。
subtotal := 1 + 2*3
// (基础价+件数) * 档位:括号强制先加后乘,得 9。
tierPrice := (1 + 2) * 3
// 资格:小计过 5 元门槛,且档位价低于 20(库存/风控阈值)。
eligible := subtotal > 5 && tierPrice < 20
// 展示文案:品牌前缀 + 活动名。
title := "go" + "lang"
fmt.Println(subtotal, tierPrice, eligible, title)
}
func main() {
calcPromoLine()
}
建议运行:
go run .
期望输出:
7 9 true golang
结合场景再看四个关注点
-
乘法优先于加法
件数 × 单价先算,再加基础价,和日常“小计公式”一致。 -
括号改写结合顺序
需要“先合并再乘档位”时必须写括号,不能靠默记。 -
比较结果喂给
&&
subtotal > 5得到 bool,再和库存/阈值条件短路组合。 -
+对字符串是拼接
活动标题拼接不是数值加法,类型仍是string。
核心概念与准确模型
表达式是什么
表达式通过对操作数应用运算符和函数来计算值。
操作数可以是:
- 字面量:
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
除零
| 运算 | 行为 |
|---|---|
整数 / 或 %,除数为 0 | 运行时 panic |
| 浮点除以 0 | 按 IEEE 754 得到 ±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) |
<< | 左移 |
>> | 右移;有符号数为算术右移(符号扩展) |
移位要点(规范):
- 左操作数是整数(或可表示为整数的无类型常量)
- 右操作数是无符号整数,或可表示为
uint的无类型常量 - 移位计数 ≥ 左操作数类型位宽时,结果有明确定义(通常为 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 的相等性不要靠 == 判断内容,应使用专门逻辑或 maps.Equal / 自写比较。
逻辑运算与短路
&& 条件与:左为 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 表示是否仍从打开通道读到值
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(变量整数)
与“错误用返回值表达”的哲学一致:常规算术路径不靠异常控制;真正危险的除零才 panic。 -
发送是语句、接收是表达式
呼应 channel 的使用形态:接收常进入变量,发送是流程中的动作。
边界情况与反直觉行为
1. 不同类型不能直接运算
var a int32 = 1
var b int64 = 2
// sum := a + b // 编译错误
sum := int64(a) + b
2. 整数除零 panic,浮点除零不 panic
var i = 1 / 0 // 常量除零:编译期就能发现
var z int
// _ = 1 / z // 运行时 panic
var f = 1.0 / 0.0 // +Inf
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 错误理解:
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,不要把带副作用的调用嵌进复杂算术里。
标准库中的常见模式:
if n < 0 {
n = -n
}
if c >= '0' && c <= '9' {
return c - '0'
}
可验证实验
下列实验可在本地
go run/go test中完成。本笔记写作时依据 Go 语言规范 核对;若你的工具链版本不同,以当前go version与规范为准。
实验 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 文档线) | 官方规范总入口 | 全文交叉核对 |
未把第三方博客结论当作规范。实现级优化(例如字符串拼接是否被编译器改写)未写成语言承诺。