← [ BACK_TO_NOTES ]

Go 运算符与表达式

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

#concept #growing #dev #go

[!info] Related notes

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

乘法先于加法结合,括号改变结合方式;比较结果用于 &&,字符串 + 表示拼接。

核心概念与准确模型

表达式是什么

根据 Go 语言规范 · Expressions:

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

操作数可以是:

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

规则:

  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

最小有符号整数除以 -1 是溢出特例:运行时 var x int8 = -128,x / -1 仍为 -128,x % -1 为 0;若写成常量运算,结果 128 不能表示为 int8,编译失败。

除零

运算行为
常量表达式除以 0(包括浮点)编译错误
运行时整数 / 或 %,除数为 0panic
运行时浮点除以 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)
<<左移
>>右移;有符号数为算术右移(符号扩展)

移位要点(规范):

  1. 左操作数是整数(或可表示为整数的无类型常量)
  2. 右操作数可以是任意整数类型(Go 1.13 起允许有符号类型),或可表示为 uint 的无类型常量;负常量计数编译失败,运行时负计数 panic
  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 的相等性不要靠 == 判断内容。元素/值可比较时可用 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++

关键限制:

  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. 普通变量读取等非调用操作数与调用之间的相对顺序,在某些情形下未指定;移位也没有额外的“先左后右”通用保证
// 第一阶段确定左侧下标和右侧值;第二阶段从左到右赋值
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(变量整数)
    这是语言规定的定宽整数行为,不等于业务计算安全;需要检测溢出时应显式检查。

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

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

工程实践

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

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

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

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

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

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

  7. 把副作用请出表达式
    需要调用并检查错误时,用短变量声明 + 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。
  • 下一步:用这些表达式写出清晰的 控制流,并在 函数 中处理多返回值与错误。

自测题

概念题

  1. 为什么说 ++ 在 Go 里不是运算符表达式?
  2. 变量 int8 从 127 再加 1 会怎样?常量 int8(128) 呢?
  3. 哪些类型可以用 < 比较?slice 能否 == 另一个 slice?
  4. <-ch 与 ch <- 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 函数
  • 核验范围:主要语义边界及可运行示例已在 Go 1.26.8 验证;实现优化不作为语言承诺。
创建于 2026/7/14 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本