Go 运算符与表达式

Go 如何用运算符与表达式计算值:优先级、算术与溢出、比较与可比较性、短路逻辑、取址与 channel 接收,以及 ++/-- 为何是语句而非表达式。

#type / concept #status / growing #tech / dev #resource / go

[!info] 关联笔记

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

结合场景再看四个关注点

  1. 乘法优先于加法
    件数 × 单价先算,再加基础价,和日常“小计公式”一致。

  2. 括号改写结合顺序
    需要“先合并再乘档位”时必须写括号,不能靠默记。

  3. 比较结果喂给 &&
    subtotal > 5 得到 bool,再和库存/阈值条件短路组合。

  4. + 对字符串是拼接
    活动标题拼接不是数值加法,类型仍是 string

核心概念与准确模型

表达式是什么

根据 Go 语言规范 · Expressions

表达式通过对操作数应用运算符和函数来计算值。

操作数可以是:

  • 字面量:423.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||

规则:

  1. 同级二元运算符从左到右结合:x / y * z 等于 (x / y) * z
  2. 可用括号强制顺序
  3. ++ / -- 不是表达式,不参与优先级表
  4. 赋值不是表达式,因此没有“赋值表达式的优先级”
// 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 得到 ±InfNaN,不因此 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)
<<左移
>>右移;有符号数为算术右移(符号扩展)

移位要点(规范):

  1. 左操作数是整数(或可表示为整数的无类型常量)
  2. 右操作数是无符号整数,或可表示为 uint 的无类型常量
  3. 移位计数 ≥ 左操作数类型位宽时,结果有明确定义(通常为 0;有符号负数算术右移可为 -1)
  4. 右操作数在移位表达式中后于左操作数求值
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++

关键限制:

  1. ++ / -- 只能后缀,没有前缀 ++i
  2. 它们是语句,不能写 x = i++f(i++)
  3. a op= b 语义上类似 a = a op b,但 a 只求值一次
  4. 左操作数须可寻址,或为 map 索引表达式

这与 C/JavaScript 不同:Go 故意禁止把自增塞进更大表达式,以减少“既改状态又产值”的阅读负担。需要交换或并行更新时,用多值赋值:

a, b = b, a
i, j = i+1, j-1

求值顺序

规范对求值顺序的核心保证是:

  1. 表达式、赋值、return 中的函数调用、方法调用、通信操作词法从左到右发生
  2. && / || 的右操作数按需求值
  3. 移位的右操作数在左操作数之后求值
  4. 赋值时,右侧先算完,再写入左侧
  5. 非调用操作数与调用之间的相对顺序,在某些情形下未指定
// 多值赋值:先完整计算右侧,再写入
i, x[i] = 1, 2 // 使用旧的 i 作为下标,然后 i 变成 1

不要依赖未指定顺序的副作用。需要确定顺序时,拆成多条语句。

从表面现象深入:规范保证与实现细节

本主题主要是语言规则,不必先下沉 Runtime。但仍要分清三层:

层次例子
规范保证短路、整数除法向零、变量整数溢出回绕、++ 是语句
当前实现具体 CPU 指令如何实现移位与溢出;编译器是否折叠常量
理解用模型“表达式树从内到外计算”

常量表达式在编译期以更高精度处理;实现至少需支持很大的整数常量精度(规范对编译器表示能力有下限要求)。因此:

const c = 1 << 100 // 可作为无类型常量存在
var n int = c      // 若 int 装不下,编译失败

字符串 + 每次都产生新字符串值;编译器可能做拼接优化,但语义上字符串不可变。性能敏感路径应优先 strings.Builder 等 API,而不是臆测实现。

设计动机:Go 为什么这样设计

  1. 优先级层级少
    只有五级二元优先级,并让移位与乘除同级、加减与位或/异或同级,减少“记一张巨表”的成本。Effective Go 也指出,因此常常比 C 少写括号。

  2. ++/-- 降为语句
    去掉“在表达式里偷偷改状态”的写法,强制副作用显眼。

  3. 几乎不做数值类型提升
    不同类型整数不能直接混算,把转换交给程序员。这与“显式优于隐式”一致,也和 可赋值性规则 一脉相承。

  4. 短路逻辑成为惯用防护
    p != nil && p.ok 是语言级安全阀,而不是风格建议。

  5. 溢出不 panic(变量整数)
    与“错误用返回值表达”的哲学一致:常规算术路径不靠异常控制;真正危险的除零才 panic。

  6. 发送是语句、接收是表达式
    呼应 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 对比(仅辅助直觉):

JavaScriptGo
真值大量 truthy/falsy只有 bool
&&/|| 结果可能返回操作数返回布尔
++可作前/后缀表达式仅后缀语句
整数溢出Number 是浮点;BigInt 另论定宽整数回绕
类型混算常隐式转换基本要求类型一致

工程实践

  1. 条件先写清布尔
    if err != nilif len(s) == 0 比“玩运算符技巧”更符合 Go 风格。

  2. 复杂优先级就加括号
    即使你记得优先级,读者未必记得。移位与掩码混合时尤其如此。

  3. 位运算集中封装
    对 flag 的 &|&^ 可收进小函数或方法,避免魔法数散落。

  4. 不要用溢出当特性
    回绕是确定语义,不代表业务上可以依赖“加满归零”除非协议明确。

  5. 字符串拼接
    少量 + 清晰可读;循环内大量拼接改用 strings.Builderbytes.Buffer

  6. 比较接口与容器要谨慎
    测试里比较结构体/切片内容,优先 go-cmp、手写字段比较或专门 equal 函数,而不是依赖 == 的侥幸。

  7. 把副作用请出表达式
    需要调用并检查错误时,用短变量声明 + 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))
  • 预测:7732
  • 观察:是否与预测一致
  • 结论:*/<< 高于 +;括号改变顺序

实验 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。
  • 下一步:用这些表达式写出清晰的 控制流,并在 函数 中处理多返回值与错误。

自测题

概念题

  1. 为什么说 ++ 在 Go 里不是运算符表达式?
  2. 变量 int8 从 127 再加 1 会怎样?常量 int8(128) 呢?
  3. 哪些类型可以用 < 比较?slice 能否 == 另一个 slice?
  4. <-chch <- v 在语法角色上有何不同?

代码推理题

package main

import "fmt"

func main() {
    fmt.Println(1<<2*3)
    fmt.Println(true || panicBool())
}

func panicBool() bool {
    panic("nope")
}
  1. 能否编译?
  2. 若能,输出是什么?会不会 panic?
  3. 若把 || 改成 &&,行为如何?

再看:

var a int32 = 3
var b int = 4
fmt.Println(a + b)

能否编译?为什么?

工程思考题

  1. 审核一段把多个带副作用函数嵌在同一复杂表达式中的代码,你会怎么改?
  2. 设计一个权限 bit flag API:公开 Add/Remove/Has,内部用 |&^,为什么比让调用方直接写位运算更好?

参考答案

点击展开

概念题

  1. 规范将 ++/-- 定义为语句,不能出现在需要值的表达式位置。
  2. 变量回绕到 -128;常量 128 不能表示为 int8,编译错误。
  3. 整数、浮点、字符串可排序;slice 不能与另一个 slice 用 == 比内容,只能与 nil 比。
  4. <-ch 是表达式;ch <- v 是语句。

代码推理题

  1. 1<<2*3<<* 同级,左结合,等价 (1<<2)*3 = 12
  2. true || panicBool() 短路,不调用 panicBool,打印 true
  3. 改成 true && panicBool() 会求右操作数并 panic。
  4. int32 + int 类型不同,编译错误,需显式转换。

工程思考题

  1. 拆成按序语句,避免未指定求值顺序与难测副作用。
  2. 封装位运算可集中校验、命名语义、避免魔数,并减少优先级误用。

延伸阅读与资料来源

资料类型支撑内容
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 文档线)官方规范总入口全文交叉核对

未把第三方博客结论当作规范。实现级优化(例如字符串拼接是否被编译器改写)未写成语言承诺。


笔记元信息

  • 建议文件名:go-operators-and-expressions.md
  • 所属阶段:阶段一 · 建立 Go 的基本认知
  • 学习顺序:第 1.8 篇(变量/基本类型之后,控制流之前)
  • 建议下一篇:深化 Go 流程控制,或继续路线中的下一缺口;若按“生成下一篇”主线,也可进入阶段二缺口 go-type-assertions-and-type-switch / 深化 go-slices
  • 本篇状态:初稿(已按官方规范核验;本地 go run 实验待环境可用时复跑)
创建于 2026/7/14 更新于 2026/7/15