编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
        • 目录介绍
        • 4.1 本章学习目标
        • 4.2 算术运算符
          • 4.2.1 + - * / % 五件套
          • 4.2.2 整数除法与求余
          • 4.2.3 浮点除法与 Inf / NaN
          • 4.2.4 溢出与 math/bits 安全运算
        • 4.3 自增 / 自减:为什么是语句而不是表达式
        • 4.4 比较运算符
          • 4.4.1 == 与 != 的可比较性规则
          • 4.4.2 < <= > >= 的有序约束
          • 4.4.3 接口值的相等比较
        • 4.5 逻辑运算符
          • 4.5.1 短路求值
          • 4.5.2 ! 一元否定
        • 4.6 位运算符
          • 4.6.1 & | ^ << >> 与 &^(清位)
          • 4.6.2 算术右移 vs 逻辑右移
          • 4.6.3 实战:bitmap 标志位
        • 4.7 地址 & 与解引用 *
        • 4.8 赋值与多重赋值
        • 4.9 为什么 Go 没有指针算术与运算符重载
          • 没有指针算术
          • 没有运算符重载
        • 4.10 运算符优先级表
        • 4.11 综合示例:bitmap 权限系统
        • 4.12 Go 新手陷阱 Top 5
          • ❌ 陷阱 1:i++ 当表达式
          • ❌ 陷阱 2:整数除法丢小数
          • ❌ 陷阱 3:浮点 ==
          • ❌ 陷阱 4:位运算优先级低于比较
          • ❌ 陷阱 5:把 slice 塞进 interface 后比较 panic
        • 4.13 思考题
        • 4.14 推荐阅读
          • 卷内
          • 跨卷
          • 外部资料
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
      • 错误处理
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

运算符

# 第 4 章 运算符

Go 的运算符大体来自 C,但故意去掉了指针算术、++i / --i 表达式形式、三元 ?:、运算符重载。 关键词:算术、比较、逻辑、位运算、&/*、运算符优先级、短路求值、&^ 清位


# 目录介绍

  • 4.1 本章学习目标
  • 4.2 算术运算符
    • 4.2.1 + - * / % 五件套
    • 4.2.2 整数除法与求余
    • 4.2.3 浮点除法与 Inf / NaN
    • 4.2.4 溢出与 math/bits 安全运算
  • 4.3 自增 / 自减:为什么是语句而不是表达式
  • 4.4 比较运算符
    • 4.4.1 == 与 != 的可比较性规则
    • 4.4.2 < <= > >= 的有序约束
    • 4.4.3 接口值的相等比较
  • 4.5 逻辑运算符
    • 4.5.1 短路求值
    • 4.5.2 ! 一元否定
  • 4.6 位运算符
    • 4.6.1 & | ^ << >> 与 &^(清位)
    • 4.6.2 算术右移 vs 逻辑右移
    • 4.6.3 实战:bitmap 标志位
  • 4.7 地址 & 与解引用 *
  • 4.8 赋值与多重赋值
  • 4.9 为什么 Go 没有指针算术与运算符重载
  • 4.10 运算符优先级表
  • 4.11 综合示例:bitmap 权限系统
  • 4.12 Go 新手陷阱 Top 5
  • 4.13 思考题
  • 4.14 推荐阅读

# 4.1 本章学习目标

  • ✅ 默写 Go 的运算符优先级表(共 5 级)
  • ✅ 理解为什么 i++ 是语句而不是表达式
  • ✅ 区分 ==「可比较」与 <「可排序」的类型约束
  • ✅ 解释短路求值,并能用它写防御性代码(如 p != nil && p.Foo())
  • ✅ 默写 6 个位运算符及 &^(清位)的语义
  • ✅ 写一个 bitmap 权限系统:增删查权限位
  • ✅ 解释 Go 为何去掉指针算术与运算符重载

# 4.2 算术运算符

# 4.2.1 + - * / % 五件套

运算符 名称 适用类型
+ 加 / 字符串拼接 数值、string
- 减 / 一元负号 数值
* 乘 数值
/ 除 数值
% 求余 仅整数(浮点用 math.Mod)
fmt.Println(7 + 3)        // 10
fmt.Println("Go" + "lang") // Golang —— + 唯一被"重载"的位置
fmt.Println(-5)           // -5(一元负号)
fmt.Println(7 % 3)        // 1
// fmt.Println(7.0 % 3.0) // ❌ 编译错:浮点不支持 %
1
2
3
4
5

+ 在字符串上的"重载"是 Go 唯一的运算符多态。语言层不开放运算符重载——这是设计取舍(详见 4.9)。

# 4.2.2 整数除法与求余

整数除法向零截断(不是向下取整):

fmt.Println(7 / 2)   //  3
fmt.Println(-7 / 2)  // -3,不是 -4!
fmt.Println(7 % 2)   //  1
fmt.Println(-7 % 2)  // -1
1
2
3
4

💡 求余的符号取决于被除数:-7 % 2 = -1、7 % -2 = 1。 想要"数学意义上的非负模"(mod),自己写:

func mod(a, n int) int { return ((a % n) + n) % n }
mod(-7, 2) // 1
1
2

整数除以 0 直接 panic:

defer func() { fmt.Println(recover()) }()
fmt.Println(1 / 0) // 编译错(常量除零)
var b int = 0
fmt.Println(1 / b) // runtime panic: integer divide by zero
1
2
3
4

# 4.2.3 浮点除法与 Inf / NaN

浮点除以 0 不 panic,按 IEEE 754 返回特殊值:

var z float64 = 0
fmt.Println(1.0 / z)   // +Inf
fmt.Println(-1.0 / z)  // -Inf
fmt.Println(z / z)     // NaN
1
2
3
4

详见第 3 章 3.3.3。

# 4.2.4 溢出与 math/bits 安全运算

整数溢出静默环绕(与 C 一致):

var x int8 = 127
x++
fmt.Println(x) // -128
1
2
3

需要"溢出报错"时用 math/bits (opens new window):

import "math/bits"

func safeAdd64(a, b uint64) (uint64, error) {
    sum, carry := bits.Add64(a, b, 0)
    if carry != 0 {
        return 0, fmt.Errorf("overflow: %d + %d", a, b)
    }
    return sum, nil
}
1
2
3
4
5
6
7
8
9

math/bits 还提供 Mul64、Sub64、Div64、LeadingZeros、TrailingZeros、OnesCount(popcount)等硬件指令级原语,是写 hash/crypto/压缩算法的基础工具。


# 4.3 自增 / 自减:为什么是语句而不是表达式

Go 把 i++ 与 i-- 设计成语句(statement),不是表达式(expression):

i := 0
i++           // ✅ 语句
// j := i++   // ❌ 编译错:i++ 不是表达式
// ++i        // ❌ 编译错:没有前置形式
// i--; j--   // ❌ 编译错:一行只能一个语句
for i := 0; i < 10; i++ { } // ✅ for 的 post 语句位置可以放 i++
1
2
3
4
5
6

设计动机(来自 Go FAQ (opens new window)):

  1. 消除歧义:C 里 f(i++, i++) 行为未定义,Go 直接语法上禁掉。
  2. 去掉前置 / 后置之争:只保留后置,且不返回值。
  3. 可读性 > 简洁性:a[i++] = b[j++] + c[k--] 在 Go 里只能拆三行写——多敲几个字符换来零歧义。

这是 Go "显式胜于隐式" 哲学的一个典型体现。


# 4.4 比较运算符

# 4.4.1 == 与 != 的可比较性规则

并非所有类型都能用 == 比较。可比较类型包括:

类型 可比较? 比较规则
布尔 / 数值 / 字符串 ✅ 按值
指针 ✅ 比较地址(nil 也可比)
channel ✅ 同一个 channel?
接口 ✅ 动态类型 + 动态值都相等
数组(如 [3]int) ✅ 逐元素比较
结构体 ✅ 当所有字段都可比较时
slice ❌ 只能与 nil 比
map ❌ 只能与 nil 比
函数 ❌ 只能与 nil 比
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // true,数组逐元素比

s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// fmt.Println(s1 == s2) // ❌ 编译错
fmt.Println(s1 == nil)   // false(仅能与 nil 比)
1
2
3
4
5
6
7
8

slice / map 为什么不能 ==? 因为它们是引用类型,没有"明确的相等语义"——是比较底层指针、还是逐元素?标准库没替你决定。要"逐元素比"用 slices.Equal、maps.Equal(Go 1.21+):

import "slices"
fmt.Println(slices.Equal(s1, s2)) // true
1
2

结构体的"传染性":

type A struct{ X int }       // 可比较
type B struct{ S []int }     // 不可比较(含 slice 字段)
type C struct{ A; B }        // C 也不可比较

var c1, c2 C
// fmt.Println(c1 == c2) // ❌ 编译错
1
2
3
4
5
6

# 4.4.2 < <= > >= 的有序约束

只有"有序类型"才能用 <:

类型 有序?
数值(整数 / 浮点) ✅
string(按字节字典序) ✅
其他(bool、指针、channel、接口、结构体、数组) ❌
fmt.Println("apple" < "banana") // true
fmt.Println("Z" < "a")          // true,'Z'=0x5A < 'a'=0x61

type P struct{ X, Y int }
p1, p2 := P{1, 2}, P{1, 3}
// fmt.Println(p1 < p2) // ❌ 编译错:结构体不可排序
1
2
3
4
5
6

要给结构体定义排序,用 sort.Slice 或 slices.SortFunc(Go 1.21+),自己写比较函数。

# 4.4.3 接口值的相等比较

接口比较有三个判断:

var a, b interface{}
a = 10
b = 10
fmt.Println(a == b) // true:动态类型 int + 值 10 都相等

a = 10        // int
b = int64(10) // int64
fmt.Println(a == b) // false:动态类型不同!

// 把"不可比较类型"塞进接口再比,会 panic
a = []int{1, 2}
b = []int{1, 2}
// fmt.Println(a == b) // runtime panic: comparing uncomparable type []int
1
2
3
4
5
6
7
8
9
10
11
12
13

经验:把 slice/map 塞进 interface{} 后再比 == 是常见的隐藏 panic,写库时要用 reflect.DeepEqual 或先做类型断言。


# 4.5 逻辑运算符

只有三个:&&(与)、||(或)、!(非)。只能用于 bool——和 C 不同,整数不能当条件。

# 4.5.1 短路求值

&& 和 || 都是短路的——这一点和 C/Java 一致,但比 C 更重要,因为 Go 没有三元运算符,短路是写防御代码的主要手段:

// 经典:先判 nil 再访问字段
if p != nil && p.Name != "" {
    fmt.Println(p.Name)
}

// 经典:先判 ok 再用 value
if v, ok := m["key"]; ok && v > 0 {
    fmt.Println(v)
}

// || 的短路:找到 cache 就不查 DB
if v := getFromCache(k); v != nil || queryDB(k) != nil {
    handle(v)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

短路的副作用——副作用函数顺序敏感:

counter := 0
add := func() bool { counter++; return true }

_ = false && add()  // counter 仍为 0(短路了)
_ = true && add()   // counter = 1
_ = true || add()   // counter 仍为 1(短路了)
1
2
3
4
5
6

# 4.5.2 ! 一元否定

isEmpty := len(s) == 0
isNonEmpty := !isEmpty
1
2

不要写 if !!x——Go 不允许(! 不能连写两个,因为 ! 后面只允许 bool 表达式,!x 是 bool,所以 !!x 其实是合法的,但 gofmt 会保留,linter 会提示"双重否定")。


# 4.6 位运算符

# 4.6.1 & | ^ << >> 与 &^(清位)

运算符 名称 例子
& 位与(AND) 0b1100 & 0b1010 = 0b1000
| 位或(OR) 0b1100 \| 0b1010 = 0b1110
^ 位异或(XOR)/ 一元按位取反 0b1100 ^ 0b1010 = 0b0110;^x = ~x
<< 左移 1 << 3 = 8
>> 右移 8 >> 2 = 2
&^ 位清除(AND NOT) 0b1100 &^ 0b1010 = 0b0100,相当于 a & (^b)

⚠️ ^ 一符两用:

  • 二元位置 a ^ b 是异或
  • 一元位置 ^x 是按位取反(C 里的 ~x)

Go 没有专门的 ~,所有"取反"都用 ^。

&^ 是 Go 独有的"清位"运算符,简化常见的"清掉某些位"操作:

const (
    FlagRead  = 1 << 0  // 0b001
    FlagWrite = 1 << 1  // 0b010
    FlagExec  = 1 << 2  // 0b100
)

perm := FlagRead | FlagWrite | FlagExec  // 0b111

// 清掉 Write 位
perm = perm &^ FlagWrite                  // 0b101
// 等价于 perm = perm & ^FlagWrite,但少打一个空格
1
2
3
4
5
6
7
8
9
10
11

# 4.6.2 算术右移 vs 逻辑右移

>> 的行为取决于左操作数的符号性:

var s int8 = -8     // 0b1111_1000(补码)
fmt.Println(s >> 1) // -4(算术右移,高位补 1)

var u uint8 = 248   // 0b1111_1000
fmt.Println(u >> 1) // 124(逻辑右移,高位补 0)
1
2
3
4
5

记住一句话:有符号 >> 是算术右移,无符号 >> 是逻辑右移。

移位计数为负或 ≥ 类型宽度:

var x int32 = 1
// _ = x << -1       // ❌ 编译错(常量负数)
_ = x << uint(-1)    // ❌ 运行期 panic(Go 1.13+ 直接编译错,因移位计数必须非负整型)
_ = x << 33          // 结果为 0(不像 C 是 UB,Go 规范明确:超过宽度则结果归零)
1
2
3
4

# 4.6.3 实战:bitmap 标志位

位运算的核心套路 4 招:

// 1. 设置位
flags |= FlagX

// 2. 清除位
flags &^= FlagX            // 推荐用 &^=
// flags &= ^FlagX         // 等价

// 3. 检查位
if flags & FlagX != 0 { }

// 4. 切换位(toggle)
flags ^= FlagX
1
2
3
4
5
6
7
8
9
10
11
12

完整示例见 4.11。


# 4.7 地址 & 与解引用 *

x := 42
p := &x          // 取地址:p 的类型是 *int
fmt.Println(*p)  // 解引用:42
*p = 100         // 通过指针修改原变量
fmt.Println(x)   // 100
1
2
3
4
5
符号 上下文 含义
&x 表达式 取 x 的地址,结果是 *T
*p 表达式 解引用 p,结果是 T
*T 类型声明 指向 T 的指针类型
&T{...} 复合字面量前 创建结构体并取地址
type Point struct{ X, Y int }
p := &Point{1, 2}     // *Point,等价于 p := new(Point); *p = Point{1, 2}
fmt.Println(p.X)       // 1(Go 自动 (*p).X)
1
2
3

💡 Go 的指针没有指针算术:p++、p+1、p[1] 全部编译错。要遍历内存,用 slice。

第 8 章会专门讲指针、逃逸分析和"为什么 Go 的指针不像 C 那么危险"。


# 4.8 赋值与多重赋值

= 是赋值,:= 是"声明+赋值"(仅在函数内):

x := 10              // 声明并初始化
x = 20               // 重新赋值
// x := 30           // ❌ 同作用域不能重复 :=
1
2
3

多重赋值——Go 的标志性特性之一:

a, b := 1, 2
a, b = b, a              // 交换,无需中间变量

// 用于函数多返回值
v, ok := m["key"]
n, err := strconv.Atoi(s)
1
2
3
4
5
6

复合赋值:+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=、&^=,全部在赋值前先求值右侧:

flags &^= FlagWrite  // 等价 flags = flags &^ FlagWrite
1

# 4.9 为什么 Go 没有指针算术与运算符重载

# 没有指针算术

// C 里
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *(p + 2)); // 3
1
2
3
4
// Go 里
arr := [5]int{1, 2, 3, 4, 5}
p := &arr[0]
// fmt.Println(*(p + 2)) // ❌ 编译错
fmt.Println(arr[2])       // ✅ 用 slice/array 索引
1
2
3
4
5

理由:

  1. GC 友好:指针算术让 GC 难以判断哪些内存"还在用"。Go 想做精准 GC,禁掉指针算术是前提。
  2. 内存安全:消灭 90% 的越界 / 段错误。Go 的 slice 替代了"指针 + 长度"模式,且自带边界检查。
  3. 真要做底层操作?用 unsafe.Pointer + unsafe.Add(Go 1.17+)显式声明"我知道我在干什么"。

# 没有运算符重载

// C++ 里
class Vec { Vec operator+(const Vec& other) { ... } };
Vec a, b; Vec c = a + b;  // 调用 operator+
1
2
3
// Go 里
type Vec struct{ X, Y float64 }
// Go 不允许定义 (v Vec) operator+,只能写方法
func (v Vec) Add(o Vec) Vec { return Vec{v.X + o.X, v.Y + o.Y} }
c := a.Add(b)
1
2
3
4
5

理由(Go FAQ (opens new window)):

  1. 可读性:看到 a + b 就是基本类型加法,看到 a.Add(b) 就知道有自定义逻辑。不需要跳转去看是不是被重载。
  2. 简单性:实现简单(编译器无需符号表查找)、调试简单(栈帧名字就是 Add)。
  3. 取舍:放弃数学领域 DSL 的"美感",换工程代码的可预期性——这是 Go 全程贯彻的取舍。

唯一例外:+ 在 string 上是拼接(已经是历史遗留的内置规则)。


# 4.10 运算符优先级表

Go 的优先级表只有 5 级,远比 C(15 级)简单:

优先级 运算符
5(最高) * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1(最低) ||

记忆口诀:"乘除位与高、加减位或中、比较再次、逻辑最低"。一元运算符(+x、-x、!x、^x、*p、&x、<-ch)优先级最高,永远先于二元。

⚠️ 位运算优先级低于比较——这是 C 留下来的祖坟坑:

if flag & MASK == 0 { ... }
// 实际是 flag & (MASK == 0) ❌ 编译错(MASK == 0 是 bool)
// 你想要的是:(flag & MASK) == 0
if (flag & MASK) == 0 { ... } // ✅
1
2
3
4

Go 把它"显化"成了编译错(C 里则是悄悄算错)——感谢 Go 的强类型。但习惯加括号仍是好习惯。


# 4.11 综合示例:bitmap 权限系统

把本章知识串起来——一个 Linux 风格的 rwx 权限系统:

// permission/main.go
package main

import (
    "fmt"
    "strings"
)

// Permission 用 uint8 的低 3 位表示 r/w/x
type Permission uint8

const (
    PermRead    Permission = 1 << iota // 0b001
    PermWrite                          // 0b010
    PermExecute                        // 0b100

    PermAll  = PermRead | PermWrite | PermExecute
    PermNone Permission = 0
)

// Has 检查是否包含某权限
func (p Permission) Has(q Permission) bool { return p&q == q }

// Grant 授权
func (p Permission) Grant(q Permission) Permission { return p | q }

// Revoke 撤权(用 &^ 清位)
func (p Permission) Revoke(q Permission) Permission { return p &^ q }

// Toggle 切换(XOR)
func (p Permission) Toggle(q Permission) Permission { return p ^ q }

// String 自定义格式(Linux 风格 "rwx")
func (p Permission) String() string {
    var b strings.Builder
    b.Grow(3)
    if p.Has(PermRead) {
        b.WriteByte('r')
    } else {
        b.WriteByte('-')
    }
    if p.Has(PermWrite) {
        b.WriteByte('w')
    } else {
        b.WriteByte('-')
    }
    if p.Has(PermExecute) {
        b.WriteByte('x')
    } else {
        b.WriteByte('-')
    }
    return b.String()
}

func main() {
    p := PermRead | PermWrite          // rw-
    fmt.Println("init     :", p)        // rw-

    p = p.Grant(PermExecute)            // rwx
    fmt.Println("grant x  :", p)

    p = p.Revoke(PermWrite)             // r-x
    fmt.Println("revoke w :", p)

    p = p.Toggle(PermRead)              // --x
    fmt.Println("toggle r :", p)

    fmt.Println("has exec :", p.Has(PermExecute)) // true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

输出:

init     : rw-
grant x  : rwx
revoke w : r-x
toggle r : --x
has exec : true
1
2
3
4
5

涵盖的知识点:

知识点 体现位置
自定义类型 + 方法 type Permission uint8 + func (p Permission) ...
iota 与左移定义标志位 1 << iota
位或 / 位清除 / 异或 / 位与 Grant / Revoke / Toggle / Has
Stringer 接口(fmt 自动调) String() 方法
strings.Builder 拼接 b.Grow(3) + WriteByte

# 4.12 Go 新手陷阱 Top 5

# ❌ 陷阱 1:i++ 当表达式

i := 0
// j := i++       // 编译错:i++ 不是表达式
// f(i++, i--)    // 编译错
1
2
3

修复:拆成两行——i++; j := i。

# ❌ 陷阱 2:整数除法丢小数

fmt.Println(5 / 2)         // 2,不是 2.5
fmt.Println(float64(5)/2)  // 2.5 ✅
fmt.Println(5 / 2.0)       // 2.5 ✅(无类型常量 2.0 让结果变 float)
1
2
3

修复:至少一边显式转 float,或者写浮点字面量 2.0。

# ❌ 陷阱 3:浮点 ==

if 0.1+0.2 == 0.3 { ... } // 永远 false
1

修复:用容差 math.Abs(a-b) < 1e-9。详见第 3 章 3.3.2。

# ❌ 陷阱 4:位运算优先级低于比较

if flag & MASK == 0 { ... }
// 实际想法:(flag & MASK) == 0
// Go 编译器会因类型不匹配报错救你一命,但要养成加括号的习惯
if (flag & MASK) == 0 { ... } // ✅
1
2
3
4

# ❌ 陷阱 5:把 slice 塞进 interface 后比较 panic

var a, b interface{} = []int{1, 2}, []int{1, 2}
fmt.Println(a == b) // runtime panic: comparing uncomparable type []int
1
2

修复:用 reflect.DeepEqual(a, b),或先类型断言再用 slices.Equal。


# 4.13 思考题

  1. 为什么 Go 设计 i++ 是语句而不是表达式?这种设计阻止了 C 里的哪些 bug?请举两个例子。
  2. &^ 与 & ^ 在语法上的区别?写一段代码:用 &^ 实现"清掉低 4 位",再用 &^= 简写。
  3. 为什么 slice 不能用 == 比较,而数组可以?请从"语义不明确"和"性能可预测"两个角度分析。
  4. 写一个泛型函数 Equal[T comparable](a, b T) bool——为什么参数必须是 comparable 而不能是任意 T?
  5. 短路求值在并发场景的"惊喜":思考 if a.Load() && b.Load(),如果两次 Load 之间有别的 goroutine 修改了状态,会怎样?这是 bug 还是 feature?
  6. Go 没有运算符重载。如果你想给自定义的 BigDecimal 加法体验"最像 +"的 API,怎么设计?请给出函数签名与示例调用。

# 4.14 推荐阅读

# 卷内

  • 第 3 章 数据类型(整数、浮点的取值范围与零值)
  • 第 5 章 复合类型(slice / map 为什么不可比较)
  • 第 8 章 指针与逃逸(& / * 的完整语义与逃逸分析)

# 跨卷

  • 卷三第 4 章 字符串与切片底层(== 在底层如何对比 stringStruct)
  • 卷四第 9 章 性能优化(math/bits 在 hash / 压缩里的实战)

# 外部资料

  • Go 官方规范:Operators (opens new window)
  • Go FAQ: Why no operator overloading? (opens new window)
  • Go FAQ: Why is ++ a statement? (opens new window)
  • math/bits 包文档 (opens new window)(溢出安全运算与位计数)
上次更新: 2026/06/10, 11:13:41
数据类型
复合类型

← 数据类型 复合类型→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式