第一章:Go语言核心语法与编程范式
Go 语言以简洁、明确和可组合性为核心设计哲学,摒弃隐式类型转换、继承与异常机制,转而强调显式声明、接口组合与错误即值的处理范式。其语法结构高度统一,所有变量声明均采用 var name type 或短变量声明 name := value 形式,且类型始终位于标识符右侧,强化“所见即所得”的可读性。
变量与类型系统
Go 是静态强类型语言,但支持类型推导。基础类型包括 int、float64、bool、string 和复合类型如 slice、map、struct。声明时若未初始化,变量将获得零值(如 、false、"" 或 nil):
var count int // 零值为 0
name := "Alice" // 推导为 string
scores := []float64{89.5, 92.0, 78.3} // slice 字面量
接口与组合式编程
Go 不支持类继承,而是通过接口(interface)实现行为抽象。接口仅定义方法签名,任何类型只要实现了全部方法即自动满足该接口——无需显式声明 implements。这鼓励“小接口、高复用”原则:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return d.Name + " says: Woof!" }
// Dog 类型自动实现 Speaker 接口,可直接传入接受 Speaker 的函数
错误处理与并发模型
Go 将错误视为普通返回值,惯用 func() (result T, err error) 模式。开发者必须显式检查 err != nil,避免被忽略。并发则依托轻量级 goroutine 与通道(channel)构建 CSP 模型:
ch := make(chan int, 1)
go func() { ch <- 42 }() // 启动 goroutine 发送数据
val := <-ch // 主协程阻塞接收,确保同步
| 特性 | Go 实现方式 | 设计意图 |
|---|---|---|
| 内存管理 | 自动垃圾回收(GC) | 降低手动管理风险 |
| 包依赖 | go mod init + go get |
显式版本控制与模块化 |
| 并发同步 | sync.Mutex、sync.WaitGroup |
替代锁竞争,强调通信 |
| 代码格式化 | gofmt 强制统一风格 |
消除风格争议,提升协作 |
第二章:内存模型与并发原语的底层实现
2.1 Go内存布局与逃逸分析实战:从变量分配到栈帧优化
Go 编译器在编译期通过逃逸分析(Escape Analysis)决定变量分配位置:栈上(高效、自动回收)或堆上(需 GC 管理)。理解其决策逻辑,是性能调优的关键起点。
什么触发逃逸?
以下常见模式会导致变量逃逸至堆:
- 变量地址被返回(如函数返回局部变量指针)
- 赋值给全局变量或
interface{}类型 - 作为 goroutine 参数传入(生命周期超出当前栈帧)
实战对比:栈 vs 堆分配
func stackAlloc() *int {
x := 42 // x 在栈上分配
return &x // ❌ 逃逸:返回局部变量地址 → 编译器强制移至堆
}
func noEscape() int {
y := 100 // y 在栈上分配,且不逃逸
return y // ✅ 返回值拷贝,无需地址传递
}
逻辑分析:
stackAlloc中&x使x的生命周期必须超越函数作用域,Go 编译器(go build -gcflags="-m")会报告&x escapes to heap;而noEscape的y完全驻留栈中,无指针泄漏,零GC压力。
逃逸分析决策流程
graph TD
A[变量声明] --> B{是否取地址?}
B -->|是| C{地址是否逃出当前函数?}
B -->|否| D[栈分配]
C -->|是| E[堆分配]
C -->|否| D
关键编译参数速查表
| 参数 | 说明 | 示例 |
|---|---|---|
-gcflags="-m" |
输出基础逃逸信息 | go build -gcflags="-m" main.go |
-gcflags="-m -m" |
显示详细分析过程 | 逐行标注“moved to heap”原因 |
-gcflags="-m -l" |
禁用内联以观察原始逃逸行为 | 排除内联干扰判断 |
优化本质是减少堆分配频次,从而降低 GC STW 时间与内存碎片。
2.2 goroutine调度器GMP模型源码级剖析:runtime/proc.go关键路径解读
GMP核心结构体关系
g(goroutine)、m(OS线程)、p(processor)三者通过指针双向绑定。关键字段包括:
g.m→ 所属M,g.status标识运行状态(_Grunnable/_Grunning等)m.p→ 绑定的P,m.g0为系统栈goroutinep.runq→ 本地运行队列(环形数组),p.runqhead/runqtail管理索引
主调度循环入口
// runtime/proc.go: schedule()
func schedule() {
gp := acquireg() // 获取当前G
if gp == nil {
execute(gp, false) // 执行G,false表示非handoff
}
}
acquireg() 从当前M获取绑定的G;execute() 切换至G的栈并调用其函数指针g.sched.pc,完成用户栈与系统栈切换。
P本地队列与全局队列协同
| 队列类型 | 容量 | 获取策略 | 触发时机 |
|---|---|---|---|
p.runq |
256 | FIFO(无锁原子操作) | runqget() 优先尝试 |
sched.runq |
无界 | 加锁竞争 | runqget() 本地空时窃取 |
graph TD
A[findrunnable] --> B{本地runq非空?}
B -->|是| C[runqget]
B -->|否| D[steal from other P]
D --> E[全局runq]
2.3 channel底层机制:hchan结构体、锁策略与阻塞队列状态迁移
Go 的 channel 核心由运行时 hchan 结构体承载,其内存布局决定并发行为:
type hchan struct {
qcount uint // 当前队列中元素数量
dataqsiz uint // 环形缓冲区容量(0 表示无缓冲)
buf unsafe.Pointer // 指向 dataqsiz * elemsize 字节数组
elemsize uint16
closed uint32
sendx uint // send index in circular queue
recvx uint // receive index in circular queue
recvq waitq // 等待接收的 goroutine 链表
sendq waitq // 等待发送的 goroutine 链表
lock mutex
}
该结构体通过 mutex 实现细粒度同步:所有读写操作均需 lock.lock(),避免 sendx/recvx 竞态与 qcount 不一致。recvq 和 sendq 是双向链表,存储 sudog 节点,封装 goroutine、待传值指针及唤醒逻辑。
阻塞队列状态迁移
当 ch <- v 遇到满缓冲或无接收者时:
- 若有等待接收者(
recvq非空),直接唤醒并传递值(跳过缓冲区); - 否则,当前 goroutine 封装为
sudog入sendq并挂起。
对应地,<-ch 触发对称迁移:从 sendq 唤醒或从 buf 复制。
| 状态迁移条件 | recvq 动作 | sendq 动作 |
|---|---|---|
| 缓冲非空且无 recvq | 从 buf 取值 | 无 |
| recvq 非空 | 唤醒首个 goroutine | 无 |
| 缓冲满且 sendq 非空 | 无 | 唤醒首个 goroutine |
graph TD
A[goroutine send] -->|buf 未满| B[copy to buf]
A -->|buf 满 & recvq 空| C[enqueue to sendq + gopark]
D[goroutine recv] -->|buf 非空| E[copy from buf]
D -->|buf 空 & sendq 非空| F[dequeue from sendq + goready]
2.4 sync.Mutex与RWMutex的原子指令实现:CompareAndSwap与自旋优化实测
数据同步机制
sync.Mutex 底层依赖 atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) 实现快速非阻塞加锁;RWMutex 则通过分离读计数(readerCount)与写状态位,配合 atomic.AddInt32 和 CAS 协同控制。
自旋优化路径
当锁竞争轻微时,Mutex.lock() 会执行最多 4 次自旋(active_spin),每次调用 PAUSE 指令降低功耗并避免流水线冲刷:
// runtime/sema.go 中简化逻辑
for i := 0; i < active_spin; i++ {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return // 成功获取
}
procyield(1) // x86 PAUSE / arm64 YIELD
}
procyield(1)触发 CPU 级轻量等待,延迟约 10–100ns,显著优于系统调用阻塞。
性能对比(16线程争用下)
| 锁类型 | 平均延迟(ns) | 吞吐(ops/s) | 自旋命中率 |
|---|---|---|---|
Mutex |
82 | 12.1M | 63% |
RWMutex(读) |
14 | 70.5M | 91% |
graph TD
A[尝试 CAS 加锁] --> B{成功?}
B -->|是| C[进入临界区]
B -->|否| D[判断是否可自旋]
D -->|是| E[执行 procyield]
D -->|否| F[挂起至 sema]
2.5 defer机制的编译器介入与延迟调用链构造:_defer结构体与open-coded defer对比
Go 1.14 引入 open-coded defer,大幅优化常见场景下的 defer 开销。编译器在 SSA 阶段识别简单 defer(无闭包、参数为栈变量),跳过 _defer 结构体分配与链表管理。
延迟调用链的两种实现路径
- 传统
_defer模式:运行时动态分配堆内存,通过g._defer单链表串联,runtime.deferproc/runtime.deferreturn管理; - open-coded 模式:编译器静态生成跳转标签与 cleanup 代码块,延迟调用直接内联到函数末尾(含 panic/recover 路径)。
关键结构对比
| 特性 | _defer 结构体 |
open-coded defer |
|---|---|---|
| 内存分配 | 堆上动态分配 | 零堆分配,纯栈操作 |
| 调用链维护 | 链表插入/遍历(O(n)) | 编译期确定执行顺序(O(1)) |
| 支持场景 | 所有 defer | 仅限“简单 defer”(如 defer f(x)) |
func example() {
defer fmt.Println("first") // open-coded(x 是栈变量)
defer fmt.Println("second") // open-coded
if true {
defer fmt.Println("third") // 仍可 open-coded,无闭包捕获
}
}
上述
defer均被编译器转为 open-coded 形式:example函数结尾自动插入三段fmt.Println调用,并按 LIFO 逆序排列;panic 时通过编译生成的deferreturn标签跳转执行。
graph TD
A[函数入口] --> B[执行主体]
B --> C{是否 panic?}
C -->|否| D[顺序执行 open-coded defer]
C -->|是| E[查找最近 defer 标签]
E --> D
第三章:GC机制与性能调优的硬核实践
3.1 三色标记-清除算法演进:从Go 1.5到1.22 GC停顿控制原理与Write Barrier类型分析
Go 的三色标记算法自 1.5 版本起成为并发 GC 的基石,核心目标是在标记过程中允许用户 Goroutine 与标记器并行执行,从而压缩 STW(Stop-The-World)时间。
Write Barrier 演进路径
- Go 1.5–1.7:Dijkstra-style barrier(插入屏障),保守但写放大高
- Go 1.8–1.11:Yuasa-style barrier(删除屏障),降低写开销,需额外“灰色化”快照
- Go 1.12+:混合屏障(hybrid barrier),融合两者优势,支持无栈扫描与增量式标记
关键屏障代码示意(Go 1.22 runtime)
// hybrid barrier 核心逻辑(简化示意)
func gcWriteBarrier(ptr *uintptr, newobj unsafe.Pointer) {
if gcphase == _GCmark && !isMarked(newobj) {
markroot(newobj) // 立即标记或入队
atomic.Or8(&newobj.gcmarkbits[0], 1) // 原子置灰
}
}
此屏障在指针写入时检查目标对象是否已标记;若未标记且 GC 处于标记阶段,则立即触发标记动作。
gcphase控制屏障激活时机,isMarked()基于 bitmap 快速判断,避免重复工作。
各版本 Write Barrier 对比
| 版本范围 | 类型 | STW 影响 | 内存开销 | 并发安全性 |
|---|---|---|---|---|
| 1.5–1.7 | Dijkstra | 中 | 高 | 强 |
| 1.8–1.11 | Yuasa | 低 | 中 | 依赖快照 |
| 1.12+ | Hybrid | 极低 | 低 | 自适应强 |
graph TD
A[GC Start] --> B[STW: root scan]
B --> C[Concurrent Marking]
C --> D{Write Barrier Active?}
D -->|Yes| E[Hybrid Barrier intercepts ptr write]
D -->|No| F[Mark as usual]
E --> G[Mark newobj or enqueue]
G --> H[Concurrent sweep]
3.2 pprof全链路诊断实战:CPU/Memory/Block/Mutex profile采集与火焰图精读
启动带 profiling 的 Go 服务
go run -gcflags="-l" main.go &
# 启用所有标准 profile 端点(需在代码中注册)
import _ "net/http/pprof"
-gcflags="-l" 禁用内联,提升调用栈可读性;net/http/pprof 自动注册 /debug/pprof/* 路由,无需额外 handler。
四类核心 profile 用途对比
| Profile | 采集方式 | 关键指标 | 典型场景 |
|---|---|---|---|
cpu |
?seconds=30 |
CPU 时间占比 | 热点函数定位 |
heap |
?gc=1 |
实时堆分配 | 内存泄漏分析 |
block |
?seconds=10 |
goroutine 阻塞时长 | 锁竞争/IO 瓶颈 |
mutex |
?debug=1 |
互斥锁持有分布 | 死锁/争用热点 |
火焰图生成流程
curl -o cpu.pb.gz "http://localhost:6060/debug/pprof/profile?seconds=30"
gunzip cpu.pb.gz && go tool pprof -http=:8080 cpu.pb
-http=:8080 启动交互式火焰图服务;.pb 是二进制 profile 格式,压缩后体积更小、传输更快。
graph TD
A[HTTP 请求] –> B[/debug/pprof/cpu]
B –> C[采样器启动]
C –> D[内核级周期中断捕获栈帧]
D –> E[聚合生成 profile.pb]
3.3 内存泄漏定位四步法:pprof+gdb+runtime.ReadMemStats+对象生命周期追踪
内存泄漏排查需协同多工具形成闭环验证:
四步协同流程
graph TD
A[pprof heap profile] --> B[识别高增长对象类型]
B --> C[gdb attach + runtime.ReadMemStats 对比]
C --> D[追踪对象创建栈与逃逸分析]
D --> E[结合代码审查生命周期管理]
关键诊断代码示例
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB\n", bToMb(m.Alloc)) // 当前堆分配量(字节→MiB)
m.Alloc 表示当前仍在使用的堆内存字节数,持续增长且不回落是泄漏强信号;bToMb 为辅助转换函数,提升可读性。
工具能力对比表
| 工具 | 定位粒度 | 实时性 | 是否需重启 |
|---|---|---|---|
pprof |
类型+调用栈 | 高(采样) | 否 |
gdb |
指针级对象 | 中(需attach) | 否 |
ReadMemStats |
全局统计 | 极高(纳秒级) | 否 |
- pprof 发现可疑类型后,用 gdb 查看具体实例地址与引用链;
ReadMemStats提供基线数据,支撑趋势判断。
第四章:高可用服务工程化落地关键路径
4.1 Context取消传播与Deadline穿透:源码级解读cancelCtx与timerCtx状态机
cancelCtx 的原子状态机
cancelCtx 通过 atomic.Value 封装 done channel 与 mu 互斥锁,其取消传播依赖 children map 的深度遍历:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已取消,短路退出
}
c.err = err
close(c.done)
for child := range c.children {
child.cancel(false, err) // 递归取消,不从父节点移除自身
}
c.children = nil
c.mu.Unlock()
}
该函数确保幂等性与拓扑有序传播:每个子 Context 仅被取消一次,且父节点在清空 children 前完成全部递归调用。
timerCtx 的 Deadline 穿透机制
timerCtx 在 cancel() 中额外触发 stopTimer(),避免 goroutine 泄漏:
| 字段 | 类型 | 作用 |
|---|---|---|
timer |
*time.Timer | 定时触发 deadline 超时 |
deadline |
time.Time | 截止时间(不可变) |
cancelCtx |
embed | 复用取消传播逻辑 |
graph TD
A[NewTimerCtx] --> B[启动 timer]
B --> C{timer.Fired?}
C -->|是| D[调用 cancelCtx.cancel]
C -->|否| E[手动 cancel()]
E --> D
Deadline 不可重置——timerCtx.Deadline() 返回原始值,确保下游感知一致截止点。
4.2 HTTP/2与gRPC流控机制:流控窗口计算、BDP探测与transport.Stream实现细节
gRPC底层依赖HTTP/2的多路复用与流控能力,其transport.Stream封装了逻辑流与底层帧传输的映射关系。
流控窗口动态调整
初始窗口默认为65,535字节,通过WINDOW_UPDATE帧实时更新:
// transport/http2_client.go 中窗口更新逻辑
stream.sendWindow = atomic.LoadInt32(&t.initialWindowSize) // 初始值来自SETTINGS帧
atomic.AddInt32(&stream.sendWindow, int32(delta)) // delta可正可负
delta由接收方根据缓冲水位与应用消费速率反馈,确保不超peer's flow-control window。
BDP探测原理
gRPC通过周期性发送PING帧+测量RTT与最大吞吐,估算带宽时延积(BDP):
- 探测间隔:
min(10s, 2×RTT) - 窗口上限:
BDP × 2(避免过度保守)
transport.Stream核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
sendQuota |
int32 |
当前可发字节数(受流控窗口与并发流共享) |
buf |
*bytes.Buffer |
应用写入缓冲区,受writeQuota保护 |
resetChan |
chan struct{} |
流终止通知通道 |
graph TD
A[Client Write] --> B{sendQuota > 0?}
B -->|Yes| C[Encode & Queue DATA frame]
B -->|No| D[Block until WINDOW_UPDATE]
C --> E[HTTP/2 Framing Layer]
D --> F[Recv WINDOW_UPDATE from Server]
F --> B
4.3 Go Module依赖治理与proxy安全审计:go.sum验证流程、replace指令陷阱与私有仓库鉴权集成
go.sum 的信任链验证机制
go.sum 文件记录每个模块的哈希值,构建不可篡改的依赖指纹链。每次 go build 或 go get 均自动校验:
# 示例:go.sum 条目结构
golang.org/x/text v0.14.0 h1:ScX5w+dcPKY6XQF8pE9JbOqy2NcGqTlCfzLZd7iUv4=
golang.org/x/text v0.14.0/go.mod h1:u+2+/hLmBbKt1RjRq9gD/6Aq2rWbVHn2Ia2k42o=
- 每行含模块路径、版本、算法前缀(
h1:表示 SHA-256)、Base64 编码哈希值; go mod verify可手动触发全量校验,失败则中止构建。
replace 指令的隐式绕过风险
使用 replace 会跳过 go.sum 校验和 proxy 重定向,常见于本地调试:
// go.mod
replace github.com/example/lib => ./local-fork
⚠️ 风险:该路径不参与 checksum 计算,且无法被 GOPROXY 审计——等同于关闭供应链签名验证。
私有仓库鉴权集成方案
| 组件 | 要求 | 示例配置 |
|---|---|---|
| GOPROXY | 支持 Basic Auth 或 token 注入 | https://goproxy.example.com |
| GIT_AUTH_TOKEN | 用于 git+ssh/git+https 场景 |
export GIT_AUTH_TOKEN=xxx |
| netrc | 存储凭证(推荐) | machine private.repo.com login user password token |
安全审计流程图
graph TD
A[go get github.com/org/pkg] --> B{GOPROXY?}
B -->|Yes| C[Proxy 请求 + token 注入]
B -->|No| D[直接 Git 克隆]
C --> E[校验 go.sum 哈希]
D --> F[跳过校验 → 风险提升]
E --> G[构建通过]
F --> H[告警:disable sumdb]
4.4 零信任服务网格Sidecar通信:基于net/http/httputil与x/net/http2的透明代理拦截点剖析
零信任模型下,Sidecar需在应用无感知前提下完成mTLS终止、策略校验与流量审计。核心拦截发生在HTTP/1.x与HTTP/2双协议栈交汇处。
关键拦截层定位
net/http/httputil.ReverseProxy提供HTTP/1.x请求重写与转发钩子(RoundTrip,Director)golang.org/x/net/http2的Transport与Server暴露ConfigureTransport及NewServerConn回调点- HTTP/2流级控制需通过
http2.Server.Handler劫持*http.Request原始上下文
双协议透明代理示例(含TLS握手透传)
// 构建支持ALPN协商的双向代理Transport
tr := &http.Transport{
TLSClientConfig: &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
},
}
// httputil.ReverseProxy会自动适配底层协议
proxy := httputil.NewSingleHostReverseProxy(targetURL)
proxy.Transport = tr
该代码启用ALPN协商,使Sidecar可动态选择HTTP/1.1或HTTP/2通道;
NextProtos顺序决定客户端优先级,httputil.ReverseProxy内部通过http2.ConfigureTransport(tr)自动注入HTTP/2支持,无需修改业务逻辑。
| 协议层 | 拦截点 | 可控粒度 | 零信任能力 |
|---|---|---|---|
| HTTP/1.x | Director, ModifyResponse | 请求/响应头 | 身份断言注入、JWT校验 |
| HTTP/2 | http2.Server.Handler | 单Stream | 流级mTLS证书绑定、RBAC决策 |
graph TD
A[Inbound Request] --> B{ALPN Negotiation}
B -->|h2| C[http2.Server.Handler]
B -->|http/1.1| D[httputil.ReverseProxy.Director]
C --> E[Per-Stream Context Enrichment]
D --> F[Header-Based Identity Injection]
E & F --> G[Policy Engine Evaluation]
第五章:Go语言演进趋势与架构决策边界
Go 1.22 的并发模型强化实践
Go 1.22 引入 runtime/trace 的结构化事件流与 sync/atomic 的新原子操作(如 AddInt64, LoadUintptr),显著提升高吞吐微服务的可观测性与无锁编程安全性。某支付网关在升级后将订单状态更新路径中的 sync.RWMutex 替换为 atomic.Value + unsafe.Pointer 双重检查锁定模式,P99 延迟从 83ms 降至 21ms,GC STW 时间减少 67%。关键代码片段如下:
type OrderState struct {
status uint32
version int64
}
var state atomic.Value // 存储 *OrderState 指针
eBPF 驱动的 Go 运行时热观测落地
某云原生日志平台基于 libbpf-go 构建运行时探针,在不修改业务代码前提下捕获 goroutine 阻塞点、调度延迟及内存分配热点。通过 perf_event_open 绑定 sched:sched_blocked_reason 和 golang:gc_start tracepoint,生成实时火焰图。部署后定位到 http.Server.Serve 中未关闭的 io.Copy 导致 1200+ goroutine 积压,修复后连接复用率提升至 98.4%。
模块化架构下的依赖收敛策略
大型单体向模块化演进时,Go 的 go.mod 语义版本控制暴露兼容性断层。某证券行情系统采用“接口先行 + 仓库分治”模式:核心 marketdata 接口定义于 github.com/org/api/v3,各业务模块通过 replace 指向本地 internal/marketdata/mock 进行契约测试。版本迁移表显示:
| 模块名称 | 当前版本 | 目标版本 | 兼容性验证方式 |
|---|---|---|---|
| quote-service | v3.1.2 | v3.2.0 | 接口契约测试 + 流量镜像 |
| risk-engine | v3.0.5 | v3.3.0 | gomock 单元测试覆盖 |
WASM 边缘计算场景的 Go 编译链重构
某 CDN 厂商将 Go 编写的规则引擎(含正则匹配、JSON Schema 校验)交叉编译为 WASM,通过 tinygo build -o rules.wasm -target wasm 生成 142KB 二进制。在 Cloudflare Workers 环境中,相比 JavaScript 实现,CPU 使用率下降 41%,冷启动时间从 120ms 缩短至 28ms。关键约束在于禁用 net/http 和 os 包,改用 syscall/js 调用宿主环境 API。
泛型深度使用的边界案例
电商搜索服务引入泛型 func Filter[T any](slice []T, f func(T) bool) []T 后,编译产物体积增长 17%,且 go vet 报告 generic type parameter T is unused in method signature。经分析发现 T 仅用于类型占位而未参与逻辑,最终重构为具体类型 FilterProducts([]Product, func(Product) bool),二进制体积回落至基准线,同时启用 -gcflags="-m=2" 确认内联成功率提升至 92%。
内存模型演进对分布式锁的影响
Go 1.21 起强化 sync/atomic 的内存序语义,atomic.LoadAcquire 与 atomic.StoreRelease 成为跨节点锁实现基石。某分布式任务调度器将 Redis Lua 锁替换为基于 etcd Lease + atomic.Bool 的本地乐观锁,利用 atomic.LoadAcquire(&leaseActive) 确保 goroutine 看到最新租约状态,集群锁争用失败率从 14.3% 降至 0.8%。
该架构决策需严格遵循 Go 内存模型文档中关于 happens-before 关系的定义,尤其注意 atomic 操作与 channel 发送/接收的同步边界。
