在 Go 1.27.1(arm64 macOS,8 个线程)里量原子操作与 Mutex 的边界:无竞争时 atomic.Add 4.1 ns、Mutex 7.7 ns;8 个 goroutine 争同一个计数器时 atomic 53.7 ns、Mutex 157.2 ns、CAS 循环 254.9 ns;互不共享的原子计数器相邻放置比隔开 128 字节慢约 70 倍(57.1 ns 对 0.8 ns);并看 arm64 上编译出的 LDADDALD 与 CASALD。
在 Go 1.27.1(arm64 macOS)里验证无锁读配置的写法:8 个 goroutine 读时 atomic.Pointer.Load 0.1 ns、RWMutex.RLock 78.8 ns;原地覆盖共享结构体会让读者读到 A≠B 106250 次(-race 报竞争),而“新建再 Store 指针”为 0 次;atomic.Value 的三种 panic 原文;值装箱会多一次分配;go vet 能发现复制含 atomic 值的结构体。
在 Go 1.27.1 里验证 sync.Pool:放入 100 个对象后,一次 GC 还能取回 99 个,两次 GC 全部丢失(对应 poolCleanup 里“主缓存→victim→丢弃”);Put 切片本身比 Put 指针多一次分配;复用 bytes.Buffer 从 714 ns/6 次分配降到 192 ns/0 次分配,4 核并行 987 ns 降到 71 ns;200 万次借用 4 KiB 缓冲区,GC 次数从 2611 降到 0。
在 Go 1.27.1(arm64 macOS)里量 sync.Map:8 个 goroutine 读多写少时 5.2–6.1 ns,RWMutex+map 要 98–132 ns;50% 写时 32 ns 对 86–95 ns;但单 goroutine 下反而更慢(29.6 对 16.8 ns),100 万个键的内存是普通 map 的 3.2 倍;并确认它在 1.27 里建立在内部的 HashTrieMap 上。
在 Go 1.27.1 里实测 RWMutex:持读锁时递归 RLock 遇上排队的写者会死锁(fatal error);写者排队后新读者要等(R2 在 20 ms 请求,122 ms 才拿到);8 个连续读的 goroutine 下写者每次只等 0.60–1.01 ms;8 个只读 goroutine 下,临界区 100 µs 时 RWMutex 比 Mutex 快约 8.1 倍,而几乎不持有时只快 1.4 倍。
在 Go 1.27.1 里验证 sync.Cond.Wait 为什么要放在循环里:8 个消费者、Broadcast 时,Wait 放 if 里 3000 个元素中有 2998 次醒来后队列是空却照常取,放 for 里是 0 次;Signal 唤醒 1 个、Broadcast 唤醒 8 个;Signal 先于 Wait 会丢(300 ms 后仍卡住);Cond 来回传递 308 ns,比无缓冲 channel 的 261 ns 慢。
用 golang.org/x/sync v0.24.0 实测 errgroup:一个任务出错 51 ms 后其余 4 个任务都因 ctx 取消结束(WaitGroup 要等 1001 ms);多个错误只返回第一个;SetLimit(3) 时最大并发恰好是 3、g.Go 会阻塞而 TryGo 返回 false;任务里 panic 会让整个进程崩溃(Wait 处 recover 不到);WithContext 的 ctx 在 Wait 返回后一定被取消;并读源码里的信号量与 errOnce。
用 golang.org/x/sync v0.24.0 的 singleflight 实测:100 个并发请求同一个键,加载函数从 100 次降到 1 次;缓存刚过期的 200 个请求,后端从 200 次降到 1 次、总用时 150 ms 降到 101 ms;但它不是缓存(顺序调用 3 次就执行 3 次);错误被所有等待者共享;第一个调用方的 ctx 超时会连累第二个调用方;加载函数 panic 会让所有调用方 panic。
用 golang.org/x/time v0.16.0 的 rate 实测令牌桶:桶满时 Allow 连放 5 个再拒绝,空闲 1.2 秒也只能一口气放 5 个;Wait 以 20/s 放行,间隔 49.9–51.1 ms;n 超过 burst 或等待会超过 ctx 期限时立即返回错误;Reserve 的预约依次排到 100/200/300 ms;1、8、64 个 goroutine 共用一个限流器,吞吐都是约 1003 次/秒。
在 Go 1.27.1 里实测 channel 流水线:消费者提前走掉而没有取消机制时泄漏 100 个 goroutine,加 cancel 后为 0;4 个 worker fan-out 后输出有 794 处逆序;第二级 worker 从 1 个增加到 8 个,2 万个元素从 596 ms 降到 114 ms(约 5.2 倍);级间 channel 容量从 0 加到 256,每个元素的耗时从 594 ns 降到 129 ns。
在 Go 1.27.1 里比较固定并发数的五种写法:2000 个 5 ms 任务、上限 50 时,固定 worker、先占位的信号量、errgroup 峰值只有 52 个 goroutine,先起 goroutine 再占位的写法峰值 2002;100 万个极短任务、上限 8 时,固定 8 个 worker 414 ms、累计分配 27 KiB,每任务一个 goroutine 的限并发写法 521–649 ms、分配 31–39 MB。
在 Go 1.27.1 里实测 select:两个 channel 都就绪时 100 万次选择 50.0%/50.0%;普通 select 会让低优先级被选中约一半,嵌套 select 为 0;结果和超时同时就绪时 10 万次里结果 50017 次、超时 49983 次;已关闭的 channel 不置 nil 会空转,置为 nil 后只循环 12 次;select+default 忙等待占满 CPU。