阅读视图

发现新文章,点击刷新页面。
🔲 ☆

一天重写 JSONata,我用 400 美元干掉了公司 50 万美元的 K8s 集群

本文永久链接 – https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai

大家好,我是Tony Bai。

过去的几年,我们见证了 AI 编程工具从“玩具”到“神器”的进化。无数开发者都在分享自己效率翻倍的喜悦。

你有没有想过,用 AI 来完成一次“外科手术式”的精准重构,一天之内,就能帮你把公司每年烧掉的 50 万美元(约 360 万人民币)的服务器成本,直接砍到零?

这听起来像天方夜谭,但它真实地发生了。

就在前几天,以色列安全公司 Reco 的工程师 Nir Barak 发表了一篇极其硬核的博客。他详细复盘了自己是如何在一天之内,花费了仅仅 400 美元的 Token 费用,利用 AI 将一个用 JavaScript 编写的核心组件 JSONata,完美地重写为了纯 Go 版本,最终为公司节省了每年 50 万美元的开销,并带来了 1000 倍的性能提升。

这不仅仅是一个“AI 真牛逼”的简单故事。它背后揭示的,是一套足以改变我们未来架构选型和技术债偿还方式的“AI 驱动重构(AI-Driven Refactoring)”实用方法。

跨语言 RPC,微服务架构中最昂贵的“性能税”

要理解这次重构的意义有多么重大,首先得看看 Nir Barak 的团队曾经陷入了多深的泥潭。

他们的核心业务是一个用 Go 编写的高性能数据管道,每天处理数十亿的事件。但其中有一个环节,需要用到一个名为 JSONata 的查询语言(你可以把它想象成带 Lambda 函数的 jq)来执行动态策略。

尴尬的是,JSONata 的官方实现是 JavaScript 写的。

这就导致了一个极其痛苦的架构:他们的主业务 Go 服务,为了执行这些规则,不得不去远程调用(RPC)一个专门部署在 Kubernetes 上的庞大的 Node.js 服务集群。

这个“小小的”跨语言调用,给他们带来了三大噩梦:

  1. 恐怖的成本:为了扛住流量,这个 jsonata-js 集群常年需要维持 300 多个 Pod 副本,光是这部分,每年就要烧掉 30 万美元的计算资源。
  2. 惊人的延迟:一次最简单的字段查找,比如 email = “admin@co.com”,在 Node.js 内部执行可能只需要几纳秒。但算上序列化、跨进程网络往返的开销,一次 RPC 调用在啥也没干之前,150 微秒的延迟就先进来了。对于一个每天处理几十亿事件的系统来说,这简直是灾难。
  3. 意想不到的运维黑洞:随着业务增长,Pod 数量一度多到耗尽了 Kubernetes 集群的 IP 地址分配上限!

Nir Barak 的团队当然也尝试过各种小修小补:优化表达式、加缓存、甚至用 CGO 把 V8 引擎直接嵌进 Go 里……但这些都只是“头痛医头”,无法根治“跨语言”这颗毒瘤。

Cloudflare 的“抄作业”哲学

转机发生在前几周。Nir Barak 看到了 Cloudflare 那篇刷爆全网的文章《我们如何用 AI 在一周内重构 Next.js》。

Cloudflare 的做法极其“暴力”且有效:他们没有让 AI 去创造新东西,而是把 Next.js 现成的spec,以及包含几千个 case 的官方测试套件(Test Suite)直接扔给大模型,然后对 AI 下达了一个简单粗暴的指令:

“我不管你怎么实现,给我写一个能在 Vite 上跑通所有这些测试的 API 就行!”

Nir Barak 看到这里,瞬间被点醒了:“我们面临的问题一模一样!我们也有 jsonata-js 官方那套包含 1778 个测试用例的完整套件啊!”

与其让 AI 去搞创新,不如把它变成一个任劳任怨、24 小时待命的“代码翻译工”!

于是,他花了一个周末,用 AI 制定了一个极其清晰的“三步走”作战计划:

  1. 第一步(人类智慧):用 Go 语言把 jsonata-js 的测试套件先“翻译”过来。
  2. 第二步(AI 体力):把 JSONata 2.x 的官方文档和规范全部喂给 AI。
  3. 第三步(测试驱动):对 AI 下达指令:“开始写 Go 代码,目标是跑通第一步的所有测试用例。”

第二天,他按下了“开始键”。

7 小时,400 美元,13000 行 Go 代码

接下来的故事,充满了令人肾上腺素飙升的极客快感。

Nir Barak 坐在电脑前,看着 AI Agent 像一台失控的缝纫机一样,疯狂地生成 Go 代码、运行测试、读取报错、然后自我修正。

整个过程被划分成了几个“波次(Waves)”:先实现核心解析器,再实现内置函数,最后处理各种边缘 case。

在 AI 与测试用例的左右互搏之下,仅仅 7 个小时 后,奇迹发生了:

一个包含 13,000 多行纯 Go 代码的、名为 gnata 的全新 JSONata 实现诞生了。它完美通过了官方所有的 1778 个测试用例。

而这整个过程的成本呢?

400 美元的 Token 费用。

Nir Barak 在博客中晒出了一张截图,数据显示,在重构 gnata 的那一天,AI 生成的代码占比高达 91.7%

当他把这个 PR 提交到公司内部时,立刻有人质疑 ROI(投资回报率)。而他的回答简单粗暴:

“上个月,jsonata-js 集群的成本是 2.5 万美元。现在,是 0。”

百倍性能与意外之喜:“手术刀式”重构的深远影响

成本降为零已经足够震撼,但性能上的收益更是堪称“恐怖”。

这还只是开始。由于 gnata 是纯 Go 实现,Nir Barak 团队得以进行更深度的“魔改”:他们设计了一套两层评估架构。对于简单的字段查找,gnata 直接在原始的 JSON 字节流上操作,实现了 零堆内存分配(Zero Heap Allocations)!只有遇到复杂表达式时,才会启动完整的解析器。

在接下来的两周内,他们乘胜追击,用 gnata 的批量处理能力,替换掉了主数据管道中另一个极其臃肿、靠启动上万个 Goroutine 来并发处理规则的旧引擎。 结果:又省下了每年 20 万美元。

短短两周,两次“外科手术式”的重构,总共为公司节省了每年 50 万美元的开销。

最让人意想不到的是,这次重构还带来了组织层面的“意外之喜”:

gnata 是公司内部第一个完全由 AI Agent 大规模参与生成的 PR。在 Code Review 的过程中,团队成员被迫去学习如何分辨“AI 真正发现的并发 Bug”和“AI 瞎操心的代码格式问题”。这次经历,为他们后续制定全公司的 AI Code Review 规范积累了宝贵的实战经验。

小结:我们不再只是“氛围感编码”

在文章的结尾,Nir Barak 提到了 AI 大神 Andrej Karpathy 最近的观点,大意是:

“编程正在变得面目全非。在底层,深厚的技术专长正成为比以往任何时候都更强大的‘乘数效应放大器’。”

Nir Barak 感慨道,直到最近,他自己都对那种完全由 AI Agent 生成代码的“氛围编码(Vibe coding)”持怀疑态度。但 2026 年 2 月,成为了一个连他这样固执的开发者都无法忽视的“拐点”

gnata 的诞生,标志着我们不再只是用 AI 去写一些无关紧要的玩具项目。在拥有明确测试用例和边界规范的前提下,AI 已经具备了对生产环境核心组件进行“手术刀式重构”的惊人能力。

你准备好拿起这把名为“AI”的手术刀,去切掉你系统里那些最昂贵、最臃肿的“技术肿瘤”了吗?

资料链接:https://www.reco.ai/blog/we-rewrote-jsonata-with-ai


今日互动探讨:

在你的公司里,是否存在类似的“异构技术栈”调用导致的性能瓶颈或成本黑洞?你有没有想过,可以用 AI + 测试用例的方式,对某个核心组件进行“代码翻译”式的重构?

欢迎在评论区分享你的架构痛点与大胆构想!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

CSAPP Cache Lab II: Optimizing Matrix Transposition

In this part of the Cache Lab, the mission is simple yet devious: optimize matrix transposition for three specific sizes: 32x32, 64x64, and 61x67. Our primary enemy? Cache misses.

Matrix Transposition

A standard transposition swaps rows and columns directly:

1
2
3
4
5
6
7
8
9
10
11
12
void trans(int M, int N, int A[N][M], int B[M][N])
{
int i, j, tmp;

for (i = 0; i < N; i++) {
for (j = 0; j < M; j++) {
tmp = A[i][j];
B[j][i] = tmp;
}
}

}

While correct, this approach is a cache-miss nightmare because it ignores how data is actually stored in memory.

Cache Overview

To optimize effectively, we first have to understand our hardware constraints. The lab specifies a directly mapped cache with the following parameters:

ParameterValue
Sets (S)32
Block Size (B)32 bytes
Associativity (E)1 (Direct-mapped)
Integer Size4 bytes
Capacity per line8 integers

We will use Matrix Tiling and Loop Unrolling to optimize the codes.

32x32 Case

In this case, a row of the matrix needs 32/8 = 4 sets of cache to store. And cache conflicts occur every 32/4 = 8 rows. This makes 8x8 tiling the sweet spot.

By processing the matrix in 8×88 \times 8 blocks, we ensure that once a line of A is loaded, we use all 8 integers before it gets evicted. We also use loop unrolling with 8 local variables to minimize the overhead of accessing B.

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
int i,j,k;
int tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8;
for(i = 0; i<N; i+=8){
for(j = 0; j<M; j+=8){
for(k = i; k<N && k<i+8; k++) {
// Read row from A
tmp1 = A[k][j];
tmp2 = A[k][j+1];
tmp3 = A[k][j+2];
tmp4 = A[k][j+3];
tmp5 = A[k][j+4];
tmp6 = A[k][j+5];
tmp7 = A[k][j+6];
tmp8 = A[k][j+7];

// Write to columns of B
B[j][k] = tmp1;
B[j+1][k] = tmp2;
B[j+2][k] = tmp3;
B[j+3][k] = tmp4;
B[j+4][k] = tmp5;
B[j+5][k] = tmp6;
B[j+6][k] = tmp7;
B[j+7][k] = tmp8;
}
}
}

61x67 Case

Since 61 and 67 are not powers of two, the conflict misses don’t occur in a regular pattern like they do in the square matrices. This “irregularity” is actually a blessing. We can get away with simple tiling. A 16x16 block size typically yields enough performance to pass the miss-count threshold.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int BLOCK_SIZE = 16;
int i,j,k,l,tmp;
int a,b;
for(i = 0; i<N; i+=BLOCK_SIZE){
for(j = 0; j<M; j+=BLOCK_SIZE){
a = i+BLOCK_SIZE;
b = j+BLOCK_SIZE;
for(k = i; k<N && k<a; k++) {
for(l = j; l<M && l<b; l++){
tmp = A[k][l];
B[l][k] = tmp;
}
}
}
}

64x64 Case

This is the hardest part. In a 64x64 matrix, a row needs 8 sets, but conflict misses occur every 32/8=432/8 = 4 rows. If we use 8x8 tiling, the bottom half of the block will evict the top half.

We can try a 4x4 matrix tiling first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int BLOCK_SIZE = 4;
int i,j,k,l,tmp;
int a,b;
for(i = 0; i<N; i+=BLOCK_SIZE){
for(j = 0; j<M; j+=BLOCK_SIZE){
a = i+BLOCK_SIZE;
b = j+BLOCK_SIZE;
for(k = i; k<N && k<a; k++) {
for(l = j; l<M && l<b; l++){
tmp = A[k][l];
B[l][k] = tmp;
}
}
}
}

But this isn’t enough to pass the miss-count threshold.

We try a 8x8 matrix tiling. We solve this by partitioning the 8×88 \times 8 block into four 4×44 \times 4 sub-blocks and using the upper-right corner of B as a “buffer” to store data temporarily.

Block A=(ATLATRABLABR)TransposeBlock B=(ATLTABLTATRTABRT)\text{Block } A = \begin{pmatrix} A_{TL} & A_{TR} \\ A_{BL} & A_{BR} \end{pmatrix} \quad \xrightarrow{\text{Transpose}} \quad \text{Block } B = \begin{pmatrix} A_{TL}^T & A_{BL}^T \\ A_{TR}^T & A_{BR}^T \end{pmatrix}

Here are the steps:

  1. Transpose ATLA_{TL} into BTLB_{TL} while simultaneously moving ATRA_{TR} into BTRB_{TR} (as a temp storage).
  2. Move the stored ATRA_{TR} from BTRB_{TR} to its final position, while moving ABLA_{BL} into its spot.
  3. Transpose ABRA_{BR} into BBRB_{BR}.
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
int i, j, k;
int tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8;

// Iterate through the matrix in 8x8 blocks to improve spatial locality
for (i = 0; i < N; i += 8) {
for (j = 0; j < M; j += 8) {

/**
* STEP 1: Handle the top half of the 8x8 block (rows i to i+3)
*/
for (k = 0; k < 4; k++) {
// Read 8 elements from row i+k of matrix A into registers
tmp1 = A[i + k][j]; tmp2 = A[i + k][j + 1];
tmp3 = A[i + k][j + 2]; tmp4 = A[i + k][j + 3]; // Top-left 4x4
tmp5 = A[i + k][j + 4]; tmp6 = A[i + k][j + 5];
tmp7 = A[i + k][j + 6]; tmp8 = A[i + k][j + 7]; // Top-right 4x4

// Transpose top-left 4x4 from A directly into top-left of B
B[j][i + k] = tmp1;
B[j + 1][i + k] = tmp2;
B[j + 2][i + k] = tmp3;
B[j + 3][i + k] = tmp4;

// Temporarily store top-right 4x4 of A in the top-right of B
// This avoids cache misses by using the already-loaded cache line in B
B[j][i + k + 4] = tmp5;
B[j + 1][i + k + 4] = tmp6;
B[j + 2][i + k + 4] = tmp7;
B[j + 3][i + k + 4] = tmp8;
}

/**
* STEP 2: Handle the bottom half and fix the temporary placement
*/
for (k = 0; k < 4; k++) {
// Read bottom-left 4x4 column-wise from A
tmp1 = A[i + 4][j + k]; tmp2 = A[i + 5][j + k];
tmp3 = A[i + 6][j + k]; tmp4 = A[i + 7][j + k];

// Read bottom-right 4x4 column-wise from A
tmp5 = A[i + 4][j + k + 4]; tmp6 = A[i + 5][j + k + 4];
tmp7 = A[i + 6][j + k + 4]; tmp8 = A[i + 7][j + k + 4];

// Retrieve the top-right elements we temporarily stored in B in Step 1
int t1 = B[j + k][i + 4];
int t2 = B[j + k][i + 5];
int t3 = B[j + k][i + 6];
int t4 = B[j + k][i + 7];

// Move bottom-left of A into the top-right of B
B[j + k][i + 4] = tmp1;
B[j + k][i + 5] = tmp2;
B[j + k][i + 6] = tmp3;
B[j + k][i + 7] = tmp4;

// Move the retrieved temporary values into the bottom-left of B
B[j + k + 4][i] = t1;
B[j + k + 4][i + 1] = t2;
B[j + k + 4][i + 2] = t3;
B[j + k + 4][i + 3] = t4;

// Place bottom-right of A into the bottom-right of B
B[j + k + 4][i + 4] = tmp5;
B[j + k + 4][i + 5] = tmp6;
B[j + k + 4][i + 6] = tmp7;
B[j + k + 4][i + 7] = tmp8;
}
}
}

Note: The key trick here is traversing B by columns where possible (so B stays right in the cache) and utilizing local registers (temporary variables) to bridge the gap between conflicting cache lines.

Conclusion

Optimizing matrix transposition is less about the math and more about mechanical sympathy—understanding the underlying hardware to write code that plays nice with the CPU’s cache.

The jump from the naive version to these optimized versions isn’t just a marginal gain; it’s often a 10x reduction in cache misses. It serves as a stark reminder that in systems programming, how you access your data is just as important as the algorithm itself.

🔲 ☆

真相调查:Go 语言真的消灭了 Undefined Behavior 吗?

本文永久链接 – https://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation

大家好,我是Tony Bai。

在系统编程的古老传说中,流传着一个关于“鼻恶魔”(Nasal Demons)的笑话。

这个梗源自 comp.std.c 新闻组,它是对 C/C++ 语言中“未定义行为”(Undefined Behavior,以下简称 UB)最生动也最恐怖的诠释。根据 ISO C++ 标准,如果你的代码触犯了 UB(例如数组越界、有符号整数溢出、空指针解引用),编译器可以“为所欲为”。

这种“为所欲为”不仅包括程序崩溃,还包括产生错误的结果、损坏数据,甚至——虽然只是笑话——让恶魔从你的鼻孔里飞出来。换句话说,一旦触碰 UB,程序的所有保证瞬间失效。

2009 年,Go 语言横空出世,高举“云原生时代系统语言”的旗帜,承诺提供比 C++ 更高的安全性、更快的编译速度和更简单的并发模型。Go 的拥趸们津津乐道于它的内存安全特性,仿佛 Go 已经彻底终结了 UB 的噩梦。

但真相果真如此吗?

近日,我翻阅了一份珍贵的历史资料——2013 年发生在 golang-nuts 邮件组的一场深度辩论。对话的一方是 Go 语言曾经的顶级贡献者 Dave Cheney,另一方是 Go 核心团队成员、gccgo 的作者 Ian Lance Taylor。

这场发生在这个语言童年时期的对话,揭示了一个令人背脊发凉又引人深思的事实:Go 并没有完全消灭未定义行为,它只是将 UB 赶进了一个更隐秘、更危险的角落——并发。

本文将带你层层剥开 Go 语言规范的表皮,调查“未定义行为”在 Go 中的真实生存状态,并探讨这对我们编写高质量代码意味着什么。

用“定义”换取“安全”——Go 的显式哲学

要理解 Go 做了什么,我们首先得明白 C/C++ 为什么保留 UB。Ian Lance Taylor 指出,C/C++ 保留 UB 本质上是为了性能——允许编译器假设“坏事永远不会发生”,从而进行激进的优化。

Dave Cheney 的疑问直击灵魂:“Go 规范中几乎看不到‘undefined’这个词,这种设计如何影响了 Go 的安全性与性能?”

答案是:Go 选择了一条确定性(Determinism)优先的道路。Go 语言规范以一种近乎偏执的态度,将绝大多数在 C/C++ 中属于 UB 的行为,都进行了严格的“定义”。即便是在错误场景下,Go 也要保证行为是可预测的

整数溢出的“确定性”承诺

在 C 语言中,有符号整数(Signed Integer)的溢出是经典的 UB。编译器有权假设溢出永远不会发生,从而将 x + 1 > x 优化为恒真(Always True),这曾导致过无数的安全漏洞。

但在 Go 语言规范中,对此有着截然不同的定义:

无符号整数:运算结果严格按照 2^n 取模。这意味着高位被丢弃,程序可以依赖这种“回绕(Wrap-around)”行为。

有符号整数:运算可以合法地溢出(legally overflow)。结果由有符号整数的表示方式(通常是补码)、运算类型和操作数确定性地定义。溢出不会导致运行时 Panic。

最关键的是,Go 规范明确禁止编译器进行危险的假设:“编译器不得假设溢出不会发生。例如,它不得假设 x < x + 1 总是为真。”

代码实证:

// https://go.dev/play/p/5CZVVU-SITX
package main

import "fmt"

func main() {
    // 1. 有符号整数溢出 (Signed Overflow)
    var a int8 = 127
    // 在 C 语言中这是 UB,但在 Go 中这是明确定义的
    b := a + 1
    fmt.Printf("int8: %d + 1 = %d\n", a, b)
    // 输出: 127 + 1 = -128 (确定性的回绕)

    // 2. 编译器禁止做的优化
    // 如果编译器假设溢出不发生,它会把这个判断优化掉
    if b < a {
        fmt.Println("发生溢出:b 确实小于 a")
    } else {
        fmt.Println("未发生溢出逻辑(Go 中不会走到这里)")
    }

    // 3. 无符号整数溢出 (Unsigned Overflow)
    var c uint8 = 255
    d := c + 1
    fmt.Printf("uint8: %d + 1 = %d\n", c, d)
    // 输出: 255 + 1 = 0 (严格的 Modulo 2^n)
}

Go这么做的代价是Go 编译器失去了一些数学优化机会(例如不能简单地消除某些循环边界检查)。但也消除了因编译器“自作聪明”而导致的逻辑崩塌,保证了不同平台下的行为一致性。

数组越界的“必杀令”

缓冲区溢出(Buffer Overflow)是网络安全史上最大的杀手。C/C++ 将越界访问视为 UB,允许攻击者通过越界读取敏感内存或覆盖返回地址,进而控制系统。

Go 对此零容忍:越界必须触发 Panic。

无论是在栈上分配的数组,还是在堆上分配的切片,Go 编译器都会在每一次访问操作前(除非能静态证明安全)插入一段 Bounds Check(边界检查)指令。一旦越界,程序立即停止,绝不含糊。

代码实证:

// https://go.dev/play/p/-CqDpIDr0BC
package main

import "fmt"

func main() {
    // 定义一个长度为 3 的切片
    s := []int{1, 2, 3}

    // 模拟一个动态索引(避免编译器在编译期直接报错)
    index := getIndex() 

    fmt.Println("尝试访问索引:", index)

    // 这里会触发 Runtime Panic
    // 错误信息明确:runtime error: index out of range [3] with length 3
    val := s[index] 

    fmt.Println("这行代码永远不会执行", val)
}

func getIndex() int {
    return 3
}

这种边界检查是在运行时(Runtime)介入,抛出 Panic,打印堆栈信息。因此会带来运行时性能损耗。虽然现代 Go 编译器引入了 BCA(边界检查消除)技术,但在无法静态分析的场景下,这就是必须缴纳的“安全税”。

空指针的“硬着陆”

在 C 语言中,解引用一个空指针是 UB。编译器有时会优化掉判空逻辑,因为它认为“既然你解引用了,那指针肯定不为空”,导致后续的安全检查失效。

Go 规定:解引用 nil 指针必须触发 Panic。

这通常是通过 CPU 的硬件异常(SIGSEGV)来捕获的。Go 运行时会接管这个硬件信号,并将其转化为一个可恢复的 Go Panic,而不是让进程直接 Core Dump 或进入不可预测的僵死状态。

代码实证:

// https://go.dev/play/p/hlyZks1dGRf
package main

import "fmt"

type User struct {
    Name string
}

func main() {
    var u *User // u 默认为 nil

    fmt.Println("准备访问 nil 指针...")

    // 在 C 中这是 UB,可能导致程序崩溃或更糟的情况
    // 在 Go 中,这不仅会 Panic,还可以被 Recover 捕获
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到恐慌:", r)
            // 输出: runtime error: invalid memory address or nil pointer dereference
        }
    }()

    // 触发 Panic
    fmt.Println(u.Name)
}

综上,我们可知:在单线程维度,Go 确实几乎消灭了 Undefined Behavior。它通过强制规定行为(Wrapping, Panicking),将“未定义”变成了“定义明确的错误”。即使程序写错了,它的错误方式也是确定的,而非随机的。

房间里的大象——数据竞争

如果文章到这里结束,那么 Go 就是一个完美的、绝对安全的语言。

但 Ian Lance Taylor 随后抛出了一个重磅炸弹:

“However, Go does have undefined behavior: if your program has a race condition, the behaviour is undefined.”
(然而,Go 确实存在未定义行为:如果你的程序存在数据竞争,那么行为就是未定义的。)

这就是 Go 语言安全神话中最大的裂痕。

在 Rust 中,编译器借用检查器(Borrow Checker)会在编译期阻止数据竞争,因此 Rust 可以自豪地宣称“无数据竞争”。但 Go 选择了更简单的并发模型,允许 Goroutine 共享内存。

一旦发生数据竞争(Data Race),即多个 Goroutine 同时访问同一块内存且至少有一个是写操作,Go 就不再提供任何保证。

为什么数据竞争是真正的 UB?

很多 Gopher 认为数据竞争只是“读到了旧数据”或者“计数器少加了 1”。这是一种极其危险的误解。在多核 CPU 和现代编译器优化的加持下,数据竞争在 Go 中可能导致内存安全破坏

这主要源于 Go 的多字数据结构(Multi-word Data Structures)

接口(Interface)的“撕裂”

Go 的 interface 在底层是由两个机器字组成的:{type_ptr, data_ptr}。

  • type_ptr 指向具体类型的元数据(如方法表)。
  • data_ptr 指向具体的数据值。

假设我们有一个全局接口变量 var i interface{},以及两个实现类型 type A 和 type B。

  • Goroutine 1 试图将 i 赋值为 A{}。
  • Goroutine 2 试图将 i 赋值为 B{}。

如果没有加锁,Goroutine 3 可能会读到一个“弗兰肯斯坦”般的怪物接口:它的 type_ptr 来自 A,但 data_ptr 却指向 B 的数据!

当你调用这个接口的方法时,程序会尝试用 A 的方法表去操作 B 的内存布局。这会导致什么?

如果运气好,你会得到Panic(类型断言失败或非法内存访问)。

反之,如果运气不好,那远程代码执行(RCE)的攻击者可以精心构造内存布局,利用这种类型混淆(Type Confusion)来劫持控制流。

切片(Slice)的“越界”

切片由 {ptr, len, cap} 三个字组成。数据竞争可能导致你读到了新的 len(变得很大),但 ptr 还是旧的(指向一个小数组)。结果是你拥有了一个长度远超底层数组容量的切片,这让你能够读取甚至修改不属于该切片的任意内存——这正是 C 语言缓冲区溢出的翻版。

这,就是 Go 中的 Undefined Behavior。 它不是“鼻恶魔”,但它是真实存在的安全黑洞。

那些“未指明”的灰色地带

除了致命的 UB,讨论中还涉及了 Go 语言规范中的另一种存在:未指明行为(Unspecified Behavior)实现定义行为(Implementation-Defined Behavior)

这些行为虽然不会导致内存破坏,但同样破坏了程序的“确定性”。

Map 的迭代顺序

在 Go 中,for k, v := range m 的顺序是故意未定义的。

Ian 解释说,这是为了防止开发者依赖某种特定的哈希实现顺序。Go 运行时甚至在每次迭代开始时引入了随机种子(迭代器会在map bucket 数组中随机选取一个起始位置向后遍历),强制让顺序变得不可预测。

这是一个非常有智慧的设计:通过强制随机化,逼迫开发者编写不依赖顺序的健壮代码。

表达式求值顺序:在“确定”与“未指明”之间

在 C/C++ 中,f(g(), h()) 中 g() 和 h() 谁先执行是未定义的(Undefined Behavior 或 Unspecified Behavior),这取决于编译器实现。

Go 语言规范对此做了更严格的规定,但依然保留了一块微妙的“灰色地带”。

确定的部分(Defined):

Go 规定,在求值表达式的操作数、赋值语句或返回语句时,所有的函数调用、方法调用和通信操作(Channel receive)都必须按照词法上从左到右的顺序执行。

例如,在赋值语句 y[f()], ok = g(h(), i()+x[j()], <-c), k() 中,函数调用和通信的发生顺序被严格锁定为:

f() -> h() -> i() -> j() -> <-c -> g() -> k()。

未指明的部分(Unspecified):

然而,规范同时也指出:并没有规定上述事件与表达式求值、索引操作、以及变量 y 的求值之间的顺序。

这意味着,虽然函数调用的相对顺序是固定的,但涉及副作用(Side Effects)的变量读写顺序可能是不确定的。来看 Spec 中的经典反例:

a := 1
f := func() int { a++; return a }

// x 可能是 [1, 2] 也可能是 [2, 2]
// 因为 a 的求值与 f() 的执行顺序未定义
x := []int{a, f()}
println(a, x)

// --- 示例:map 字面量中 key/value 的求值顺序未定义 ---
b := 1
g := func() int { b++; return b } // g() 会修改 b

// 若 b 先被求值:key=1, value=2  → m = {1: 2}
// 若 g() 先被执行:key=2, value=2 → m = {2: 2}
// Go 规范不保证 key 表达式与 value 表达式谁先求值
m2 := map[int]int{b: g()}
println(b, m2[b])

虽然 Go 比 C/C++ 确定得多,但在编写依赖于求值顺序的副作用代码(例如在参数列表中修改全局变量)时,依然可能会掉进“未指明行为”的陷阱。因此,最好不要在单行表达式中依赖复杂的副作用顺序。

浮点数转换的幽灵

讨论中有开发者 提到了 float64 转换为 uint8 的行为。在早期的 Go 版本中,对于溢出值的处理可能依赖于底层硬件指令(x86 vs ARM),从而表现出不一致。

虽然 Go 正在逐步收紧这些规范,例如 #76264 提案(尚未落地)正试图统一浮点转整数的饱和行为,但这提醒我们:即使是强类型语言,在跨平台移植时也可能遇到底层架构带来的“方言”差异。

如何在充满 UB 的世界里生存?

既然 Go 没有彻底消灭 UB,作为开发者,我们该如何自保?

视 -race 为生命线

Ian Lance Taylor 的警告应该被打印在每个 Go 开发者的工位上。

建议

  • 单元测试必须开启 -race 标志运行。
  • 在 CI/CD 流水线中,竞态检测是不可跳过的阻断性步骤。
  • 不要相信“我的并发逻辑很简单,不会出错”,人脑无法模拟现代 CPU 的乱序执行。

敬畏 unsafe

Go 的 unsafe 包是通往 C 语言 UB 世界的后门。使用 unsafe.Pointer 进行类型转换时,你实际上是在对编译器说:“我知道我在做什么,出了事我负责。”

除非你是编写底层运行时或极致性能库的专家,否则在业务代码中绝对禁止使用 unsafe。一旦使用,你必须熟读《Go 内存模型》和《垃圾回收器写屏障规则》。

理解“实现定义”与“未定义”的区别

  • 未定义(UB):可能导致 Crash、数据损坏、安全漏洞(如数据竞争)。零容忍。
  • 未指明/实现定义:不同版本或平台可能表现不同(如 Map 顺序)。不要依赖它。
  • 已定义:Go 承诺的行为(如整数回绕)。可以依赖,但需知晓代价。

小结:完美的幻象与工程的现实

通过这次“真相调查”,我们得出的结论可能有些令人沮丧,但也足够清醒:

Go 语言并没有彻底消灭 Undefined Behavior。它只是通过牺牲一部分性能和增加运行时检查,将 UB 的“攻击范围”从 C/C++ 的“随处可见”缩小到了“并发数据竞争”和“不安全代码”这两个特定的领域。

这是一种极其成功的工程权衡。它让 Go 在保持高性能的同时,为 99% 的日常编码提供了坚实的安全保障。

然而,作为 Gopher,我们不能沉浸在“绝对安全”的幻象中。我们必须意识到,当我们敲下 go func() 的那一刻,当我们试图共享一个指针的那一刻,我们正行走在悬崖的边缘。

Go 给了我们围栏(定义明确的行为),但也给了我们梯子(并发与 Unsafe)。能否不跌入 UB 的深渊,最终取决于我们是否遵守工程的纪律。

资料链接:https://groups.google.com/g/golang-nuts/c/MB1QmhDd_Rk


你遇到过“鼻恶魔”吗?

哪怕是 Go 这样严谨的语言,在并发面前也会露出锋利的牙齿。在你的开发生涯中,是否遇到过那种因为没开 -race 而在生产环境产生的“灵异事件”?你对 Go 这种“用性能换确定性”的哲学怎么看?

欢迎在评论区分享你的“探案”心得!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

别再滥用 ClickHouse 了!单机每秒狂刷 1800 万条数据,拆解 Go+DuckDB 的“微型数仓”降维打击

本文永久链接 – https://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction

大家好,我是Tony Bai。

设想这样一个极其普遍的日常工作场景:

产品经理找到你,希望能给业务后台加一个“简单”的数据看板,用来实时统计用户的 PV/UV 漏斗、Nginx 日志的慢查询分析,或者是 IoT 设备的近期时序数据。

面对每天几百万到上千万条的数据量,你陷入了沉思。

如果直接用 MySQL/PostgreSQL 跑 GROUP BY 和 COUNT(DISTINCT),数据库的 CPU 瞬间飙到 100%,不仅查询要等上十几秒,甚至可能把核心交易业务一起拖死。

如果为了这个需求,去大动干戈地部署一套 ClickHouse、Elasticsearch 、Spark 集群或某个大型时序数据库……不仅运维成本上天,对于这点数据量来说,简直是用高射炮打蚊子。

在“传统关系型数据库跑不动”和“大数据集群太沉重”之间,难道就没有一个恰到好处的方案吗?

今天,我想给你介绍一个在海外工程界使用较多的方案。它不仅能把你从沉重的大数据组件中解救出来,还能在你的 Go 语言单二进制文件中,塞进一个性能恐怖的 OLAP(在线分析处理)引擎。

它就是 DuckDB。结合 Go 语言,它能在普通服务器上跑出每秒 1800 万条记录的写入速度,和毫秒级的亿级数据分析延迟。

这绝对是一场对传统数据架构的降维打击。

为什么 MySQL/PG 做不好数据分析?

很多开发者在职业生涯早期都会踩这个坑:试图用 MySQL 解决一切问题。

当你在 PostgreSQL 或 MySQL 中执行一个跨度为 30 天的聚合分析时,为什么会慢得让人绝望?因为它们的底层是“行式存储(Row-oriented)”

在行式存储中,即使你只需要 user_id 和 timestamp 这两列,数据库也必须把每一行的所有字段(包括那些庞大的 JSON 或 Text 字段)全部从磁盘加载到内存中。大量无用的 I/O 消耗,让分析查询变成了灾难。

为了解决这个问题,我们被迫引入了 ClickHouse 等“列式存储(Column-oriented)”数据库。列式存储让分析查询的速度提升了上百倍,但代价是:你需要额外部署和维护分布式集群、学习复杂的表引擎配置等。

DuckDB——OLAP 界的 SQLite

难道列式存储就必须伴随着复杂的集群部署吗?

DuckDB 给出了一个极其优雅的答案:做 OLAP 领域的 SQLite。

DuckDB 是一个纯粹的嵌入式列式数据库。它没有独立的服务器进程,而是内嵌在你的应用进程中,不需要你配置任何网络端口。它有很多语言的binding,包括Go。

在 Go 项目中,你只需要简单地 import “github.com/duckdb/duckdb-go/v2″,它就会作为动态/静态链接库,直接融入你的 Go Application 进程中。

但千万别因为“嵌入式”三个字就觉得它是玩具。社区的一款开源高性能数据库 Arc(基于 Go + DuckDB)给出了一份令人毛骨悚然的实测数据(基于MacBook Pro M3 Max (14 cores, 36GB RAM, 1TB NVMe)):

  • 写入性能:高达 18.6M+(1860万)记录/秒
  • 写入延迟:P50 < 0.5ms,P99 < 4ms
  • 查询性能:6M+(600万)行/秒扫描 (Arrow格式)

它是怎么做到的?除了列式存储,它底层还偷偷藏着两个大杀器:向量化执行引擎(Vectorized Execution) 和对 Parquet 格式的原生支持

手把手拆解 1800 万/秒的极致写入

口说无凭,我们直接上硬核源码。

很多新手刚接入 DuckDB 时,会习惯性地用标准 SQL 的 INSERT INTO … VALUES 去循环写数据。你会发现速度并不快,一秒钟只能写几万条。

真正的降维打击,藏在 DuckDB 专门为 Go 语言暴露的 Appender API 中。

Appender 绕过了繁琐的 SQL 解析器和规划器,直接将 Go 的内存数据格式,以极低的开销批量“灌”入 DuckDB 的底层列存结构中。来看这段极致狂暴的写入代码:

// https://go.dev/play/p/mHXu-kAydDX
package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    "time"

    duckdb "github.com/duckdb/duckdb-go/v2"
)

func main() {
    // 1. 用 NewConnector 创建连接器(指定数据库文件)
    connector, err := duckdb.NewConnector("analytics.db", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer connector.Close()

    // 2. 用 sql.OpenDB 打开标准 db(用于建表等 SQL 操作)
    db := sql.OpenDB(connector)
    defer db.Close()

    _, err = db.Exec(CREATE TABLE IF NOT EXISTS metrics (id INTEGER, name VARCHAR, value DOUBLE, ts TIMESTAMP))
    if err != nil {
        log.Fatal(err)
    }

    // 3. 用 connector.Connect() 获取底层 driver.Conn(Appender 需要这个)
    conn, err := connector.Connect(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 4. 直接传 driver.Conn,无需 Raw()
    appender, err := duckdb.NewAppenderFromConn(conn, "", "metrics")
    if err != nil {
        log.Fatal(err)
    }
    defer appender.Close()

    startTime := time.Now()

    for i := 0; i < 100000; i++ {
        err := appender.AppendRow(
            int32(i),
            fmt.Sprintf("metric_%d", i%10),
            float64(i%100),
            time.Now(),
        )
        if err != nil {
            log.Fatal(err)
        }
    }

    elapsed := time.Since(startTime)
    fmt.Printf("插入 10 万条数据耗时: %v\n", elapsed)
    fmt.Printf("吞吐量: %.0f 记录/秒\n", 100000.0/elapsed.Seconds())
}

在我的一台2019款 普通 MBP 笔记本(Intel芯片)上,上述这段代码写入 10 万条数据仅需 69 毫秒

插入 10 万条数据耗时: 69.466586ms
吞吐量: 1439541 记录/秒

换算下来,吞吐量轻松突破 143 万条/秒。如果开启并发和更大批次,逼近千万级似乎也毫无压力。这比传统的 SQL INSERT 快了整整 100 倍

替代 ELK,只需一个 Go 二进制文件

掌握了这把利器,我们该如何在实际业务中发挥它的威力?

假设你有一个 10GB 的 Nginx 日志文件(或者 CSV 文件),老板让你马上查一下昨天的 PV、UV 和慢查询排行。

过去,你需要搭建 Logstash -> Elasticsearch -> Kibana 这一套全家桶。

现在,你只需要写几十行 Go 代码。DuckDB 支持直接查询 CSV 和 Parquet 文件,连数据导入都省了

你可以直接把底层的统计逻辑嵌在你的 Go REST API 里(仅作说明使用):

// 直接在 Go 代码中,把 DuckDB 当作微型分析网关
func (adb *AnalyticsDB) GetHourlyStats() (map[string]interface{}, error) {
    // 惊人特性:直接用 SQL 语法查询本地或 S3 上的 Parquet 压缩文件!
    rows, err := adb.db.Query(
        SELECT
            DATE_TRUNC('hour', timestamp) as hour,
            COUNT(*) as pv,
            COUNT(DISTINCT path) as uv
        FROM read_parquet('s3://my-bucket/nginx_logs/*.parquet')  -- 对 Parquet 格式的原生支持与深度优化(谓词下推、列裁剪),可跳过无关数据块,大幅减少实际 I/O
        WHERE timestamp > NOW() - INTERVAL '24 hours'
        GROUP BY hour
        ORDER BY hour DESC
    )
    // ... 解析并返回给前端
}

通过这种架构,你的 Go 语言 Web 服务瞬间拥有了媲美 ClickHouse 的 OLAP 分析能力。

最绝的是,整个系统的部署产物,仅仅是一个几十 MB 的 Go 二进制文件。没有额外的依赖,丢上服务器就能跑。

小结:它不是万能的银弹

虽然 DuckDB 强到离谱,但作为高级工程师,我们必须理智看待边界。

DuckDB 绝对不适合做高并发的 OLTP(在线事务处理)。

如果你用它来扛电商的下单扣库存、或者多用户的并发更新行数据,它会死得很惨。因为它是一头为了“大吞吐分析”而生的巨兽,并没有针对行级锁和高频短事务做优化。

所以,最完美的现代架构公式应该是:

PostgreSQL/MySQL(负责核心业务流) + Go 应用内嵌 DuckDB(负责旁路日志、报表聚合的简单轻量分析)。


** 今日互动探讨:**

你在公司里遇到过哪些“为了小数据杀鸡用牛刀,强行部署大集群”的奇葩架构?或者你平时处理百万级数据分析时,最爱用什么工具?

欢迎在评论区疯狂吐槽或分享!


认知跃迁:掌控架构降维打击的底层逻辑

看到这里,你是否对日常的业务开发有了全新的视角?

在过去,面对复杂的分析需求,CRUD 程序员的本能反应是“引入一个新的重量级中间件”。

但真正的高级架构师,懂得利用底层技术栈的差异性(如行存与列存、向量化与标量计算),用最轻量、最克制的方案完成“降维打击”。

如果你的 Go 技能依然停留在写写简单的增删改查 API,对更深层的并发控制、内存管理和系统级架构选型感到迷茫——

我的极客时间专栏《Go语言进阶课》正是为你量身打造!

在这 30+ 讲硬核内容中,我将带你剥开语法糖,深入理解 Go 的底层运行机制,不仅教你写代码,更教你像顶级大厂架构师一样思考:如何用最少的组件,设计出极高并发、极低延迟的优雅系统。

目标只有一个:助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变!

扫描下方二维码,加入专栏,让我们一起用技术实现“四两拨千斤”的震撼。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

你每天敲下的 go func(),藏着这位 92 岁老人的毕生心血

本文永久链接 – https://tonybai.com/2026/03/11/in-memory-of-tony-hoare

大家好,我是Tony Bai。

在这个由代码构建的现代世界里,有些名字如同星辰般指引着航向。但遗憾的是,2026 年 3 月 5 日,其中一颗最明亮的星辰熄灭了。

图灵奖得主、快速排序(Quicksort)发明者、CSP(通信顺序进程)理论之父 Tony Hoare(托尼·霍尔)与世长辞,享年 92 岁

也许你并不熟悉这个名字。但只要你是一个程序员,你就一定在面试时手写过他发明的快速排序;如果你是一个 Go 开发者,那你每天在键盘上敲下的每一个 go func() 和 make(chan int),都在调用着他留给这个世界的伟大的遗产。

今天,让我们暂时放下手头的 CRUD,跨越半个世纪的时间洪流,去看看这位非典型天才,是如何用他那近乎神迹的洞察力,赐予了 Go 语言制霸云原生时代的“并发灵魂”。

被“共享内存”支配的黑暗时代

在讲 Tony Hoare 有多伟大之前,我们必须先回忆一下,在他提出那套神级理论之前,程序员们在并发编程的泥潭里经历了怎样暗无天日的挣扎。

随着多核时代的到来,程序需要同时执行多个任务。传统的思路极其简单粗暴:共享内存(Shared Memory)。

一堆线程就像一群饿狼,死死盯着同一块内存区域。为了防止数据被写乱,程序员们被迫发明了互斥锁(Mutex)、信号量(Semaphore)。你必须极其小心地、以上帝视角去加锁、读写、释放锁。

只要你稍有不慎,忘记解锁,或者加锁顺序反了,死锁(Deadlock)和竞态条件(Race Condition) 就会像幽灵一样找上门来。程序在本地跑得好好的,一上生产环境就离奇崩溃,且极难复现、极难调试。

那是一个属于并发编程的“黑暗时代”。天下程序员苦“共享内存与锁”久矣,却找不到破局之法。

从古典哲学到“六便士的赌注”

就在整个计算机科学界在锁的泥潭里打滚时,Tony Hoare 站了出来。

有趣的是,Tony 并非科班出身。他在大学修读的竟然是古典学与哲学,后来又在皇家海军服役期间接受了高强度的俄语训练。这种看似“不务正业”的跨学科背景,赋予了他极其严密的逻辑思辨能力和哲学视角的解构能力。

他年轻时有个极其经典的轶事:在一家公司打工时,老板让他实现 Shellsort(希尔排序)。Tony 完成任务后,怯生生地对老板说:“我知道一种比这快得多的算法。” 老板不屑一顾:“我跟你赌六便士(大约几毛钱),你肯定不知道!”

于是,Tony 写出了那个后来被印在全世界每一本数据结构教材里的算法——快速排序(Quicksort)。他不仅赢走了那六便士,还顺手改变了世界。

而在面对并发编程的“绝症”时,Tony 再次展现了他哲学般的降维打击能力。

惊世骇俗的 CSP 理论

1978 年,Tony Hoare 发表了一篇名为《通信顺序进程》(Communicating Sequential Processes, 简称 CSP)的学术论文。

宛如一道闪电,这篇论文劈开了并发编程的混沌。

Tony 的哲学思维告诉他:既然共享内存那么容易出错,那我们干脆就不要共享内存了!

在 CSP 理论中,系统被划分为多个独立的、顺序执行的黑盒(进程)。它们之间没有任何共享状态。当它们需要协作时,唯一的交互方式是通过一条极其明确的管道(Channel)来“发送和接收消息”

这就像是现实生活中的流水线工人:每个人只管自己手头的活(顺序执行),做完了就通过传送带(Channel)递给下一个人。没人去抢同一个零件,自然就不需要打架(加锁)。

这种高度抽象的数学模型,完美地将复杂的并发控制,降维成了简单的数据流动。

Go 语言与云原生的基石

理论是伟大的,但在 1978 年,CSP 受限于当时的硬件架构,很难大规模工程化普及。它在学术界的象牙塔里,静静等待着一个能将它发扬光大的使者。

30 年后,谷歌的一间办公室里,Rob Pike、Ken Thompson 等几位大神正被 C++ 的并发折磨得痛不欲生。他们决定创造一门新的语言

由于 Rob Pike 早年深受 CSP 理论启发,他将 Tony Hoare 的毕生心血,直接刻进了这门新语言的基因里。这门语言,就是 Go。

Tony Hoare 论文里的晦涩数学模型,在 Go 语言里被具象化为了两个极其优雅的关键字:

  1. 顺序进程,演化成了轻量级的 Goroutine (go func())。
  2. 通信管道,演化成了强类型的 Channel (make(chan int))。

Rob Pike 更是将 CSP 的核心思想,提炼成了那句在 Go 圈子里无人不知的至理名言:

“Do not communicate by sharing memory; instead, share memory by communicating.”
(不要通过共享内存来通信,而应该通过通信来共享内存。)

让我们看一眼这被 CSP 灵魂洗礼过的代码,没有任何 sync.Mutex,没有复杂的死锁恐惧,数据的控制权随着流水的管道优雅地传递:

func main() {
    ch := make(chan int) // 创造一条 Tony Hoare 定义的通信管道

    go func() {          // 启动一个 Tony Hoare 定义的顺序进程
        ch <- 42         // 通过通信转移数据
    }()

    fmt.Println(<-ch)    // 完美接收,无需任何锁
}

Tony Hoare 也许没有预料到,他在半个世纪前写下的论文,会在今天成为支撑全球互联网的基石之一。

当我们谈论云原生时代的 Docker、Kubernetes、Prometheus 时,我们谈论的其实是 Go 语言;而当我们惊叹于 Go 语言能轻松扛起千万级的高并发调度时,我们真正应该感谢的,是底层那个名叫 CSP 的幽灵。

我们每一次扩容容器,底层的字节流都在以 Tony Hoare 所描绘的方式,有条不紊地穿梭于硅片与光纤之间。

致敬宗师:最好的纪念,是传承他的思想

Jim Miles 在追忆 Tony 的文章中提到,这位伟大的图灵奖得主极其谦逊。他曾笑着对别人说:“真正的天才不是一蹴而就的,而是在无数个日夜的深度思考中,为了一个单一问题苦苦挣扎的凡人。”

作为普通的开发者,我们无缘与这位伟人共饮下午茶,或听他亲口讲述那六便士的赌注。但作为工程师,我们对宗师最好的纪念,就是停止写那些糟糕的、充满死锁风险的并发代码,去真正理解并传承他的设计哲学。

今天,当你再次在 IDE 中敲下那个简短却充满魔力的 go func() 时,请在心底默默向这位智者致敬。

再见了,一代巨匠 Tony Hoare。

您的代码和算法已是不朽。您赐予计算世界的并发灵魂,将伴随着一代又一代的程序员,在无尽的服务器网络中,永不停止地运行下去。

参考资料

  • https://en.wikipedia.org/wiki/Communicating_sequential_processes
  • https://blog.computationalcomplexity.org/2026/03/tony-hoare-1934-2026.html

今日互动:

你在平时的 Go 开发中,是更喜欢用 Channel(CSP 模型)还是更习惯用 Mutex 锁(共享内存模型)?在并发编程中踩过哪些大坑?

欢迎在评论区分享你的心得!


认知跃迁:真正驾驭 Go 的并发灵魂

Tony Hoare 将复杂的并发问题,抽象成了极其优雅的 CSP 理论。但很多 Go 开发者,由于没有看透这层底层哲学,依然在用写 Java/C++(共享内存)的思维来写 Go,最终把 Channel 滥用得一塌糊涂,甚至引发严重的 Goroutine 泄漏。

想要真正吃透 Go 语言的并发灵魂,靠死背语法是绝对不够的。 你必须深入理解底层调度器(G-M-P 模型)是如何运作的,必须明白何时该用 Channel,何时该退回到 Mutex。

如果你渴望突破并发编程的认知瓶颈,不再只做一个“会调关键字”的熟练工,而是想成为能设计出高可用、极高并发架构的 Go 资深专家——

我的极客时间专栏 Go语言进阶课 正是为你量身定制。在这 30+ 讲硬核内容中,我将带你剥开语法糖,直击 Go 并发模型的底层骨架,重塑你的系统级架构审美。

扫描下方二维码,加入专栏。让我们用最扎实的工程实践,去向半个世纪前的伟大思想致敬!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

告别 google/uuid:Go 标准库拟新增 crypto/uuid 深度解析

本文永久链接 – https://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid

大家好,我是Tony Bai。

在 Go 的世界里,有几个第三方库的地位几乎等同于标准库,github.com/google/uuid 绝对是其中之一。无论是微服务架构、数据库主键,还是分布式追踪,UUID 的身影无处不在。

然而,尽管其他主流语言(如 Java, C#, Python)早已将 UUID 纳入标准库,Go 却迟迟未动。直到最近,一个长达近三年讨论的提案 #62026: proposal: crypto/uuid: add API to generate and parse UUID 终于迎来了突破性进展:Go 官方提案审查委员会已将其标记为 “likely accept”(极有可能接受)。

这意味着,在不久的将来(大概率是 Go 1.27 或后续版本),我们终于可以使用官方的 crypto/uuid 包了。

不仅如此,这个issue中的数百条留言也折射出的是 Go 团队对极简主义、安全性以及现代 UUID 标准的深刻思考。

UUID 极简史:从 V1 到 V8 的演进

在深入探讨 Go 的提案之前,我们有必要先补齐 UUID(通用唯一识别码,Universally Unique Identifier)的背景知识。

UUID 是一个 128 位(16 字节)的标识符,通常以 32 个十六进制数字和 4 个连字符表示,形如:f81d4fae-7dec-11d0-a765-00a0c91e6bf6。它的核心目标是:在无需中央协调机构的情况下,保证全球范围内的唯一性。

随着技术的演进,UUID 规范(主要是 RFC 4122 以及最新的 RFC 9562)定义了多种版本,它们在生成机制上各有千秋:

  • V1 & V2 (基于时间与 MAC 地址):早期的 UUID 依赖机器的物理网卡地址和当前时间。缺点:暴露了机器身份和生成时间,存在严重隐私风险,现已极少使用。
  • V3 & V5 (基于名称的哈希):根据特定的命名空间(如 URL)和名称生成。V3 使用 MD5,V5 使用 SHA-1。相同输入永远产生相同输出。缺点:MD5 和 SHA-1 已被认为在密码学上不够安全,使用场景受限。
  • V4 (纯随机):目前最广泛使用的版本。128 位中除了 6 位用于版本和变体标识外,其余 122 位全部由密码学安全的随机数生成。优点:完全匿名,冲突概率极低。缺点:完全无序,作为数据库主键时,会导致 B+ 树索引严重碎片化,影响写入性能。
  • V6 (重新排序的 V1):为了解决 V4 的无序问题,将 V1 的时间戳字段重新排列,使其具有时间上的单调递增性。
  • V7 (时间有序的随机):新一代的王者(RFC 9562 重点推荐)。它的前 48 位是 Unix 毫秒时间戳,后面跟着充足的随机数据。优点:兼顾了 V4 的隐私性/随机性,和时间上的粗略单调递增。作为数据库主键时,插入性能远超 V4。
  • V8 (自定义):为实验性或特定供应商的格式预留。

了解了这些,我们就能理解为什么 Go 团队在设计官方 API 时,会做出一些看似“保守”的选择了。

为什么现在才引入标准库?

既然 UUID 如此重要,为什么 Go 官方拖到现在?

Go 核心成员 neild 的回答非常坦诚:

  1. 没有迫切需求:github.com/google/uuid 这个第三方库工作得非常好,API 稳定,没有不可容忍的缺陷。
  2. API 设计的迷茫:UUID 标准一直在演进。如果在 2018 年将其纳入标准库,可能只会提供 V4;而今天来看,V7 显然是必需的。由于 Go 极其严苛的向后兼容性承诺,一旦将庞杂的 API 加入标准库,就永远无法删除。

那么,为什么现在又决定引入了呢?

  • 事实上的基础设施:UUID 已经成为现代软件开发的基石。
  • RFC 9562 的发布:新的标准确立了 V7 的地位,结束了长期的混乱,是时候一锤定音了。
  • 原第三方库的维护困境:github.com/google/uuid 包含了大量历史包袱(如已废弃的方法、不再需要的错误返回等),且维护状态堪忧。Go 团队希望提供一个更精简、更现代、与 Go 核心理念更契合的官方实现。

极简的艺术:crypto/uuid API 设计解析

经过社区数月的激烈辩论,官方最终拟定的 crypto/uuid API 极度精简,展现了 Go 语言一贯的克制。

这是目前被标记为 “likely accept” 的 API 概览:

package uuid // 位于 crypto/uuid

// UUID 的本质:16个不透明的字节
type UUID [16]byte

// 变量:极值
var Nil = UUID{}
var Max = UUID{0xff, 0xff, ...} // 16个 0xff

// 构造函数
func New() UUID { return NewV4() } // 默认提供 V4
func NewV4() UUID
func NewV7() UUID

// 解析函数
func Parse(s string) (UUID, error)
func MustParse(s string) UUID

// 序列化与格式化
func (u UUID) String() string
func (u UUID) MarshalText() ([]byte, error)
func (u UUID) AppendText(b []byte) ([]byte, error)
func (u *UUID) UnmarshalText(b []byte) error

// 比较
func (u UUID) Compare(v UUID) int

乍一看,这个 API 似乎比 google/uuid 少了很多东西。这些“缺失”正是设计的精髓所在。让我们逐一解析背后的考量。

为什么底层类型是 [16]byte?

有人提议用 struct 隐藏实现,有人提议用 string。官方最终坚持使用 [16]byte。

  • 兼容性:它与 google/uuid 的底层类型完全一致,这意味着两者之间的转换仅仅是一个零成本的类型强转(Type Cast),极大降低了生态迁移的成本。
  • 语义准确:UUID 本质上就是 16 个字节的数据,没有任何序列是“非法”的。

为什么 New 函数不再返回 error?

在使用 google/uuid 时,最让人烦躁的就是 uuid.NewRandom() 会返回 (UUID, error)。因为在底层,它调用的是 crypto/rand.Read。理论上,读取系统随机数可能会失败。

但现实中,现代操作系统的安全随机源(如 /dev/urandom 或 getrandom 系统调用)几乎不可能失败。如果它失败了,说明你的系统内核已经崩溃,此时程序 panic 才是最正确的选择。

Go 1.24 引入的 #66821 提案明确了 crypto/rand 会在失败时直接致命退出(Fatal)。因此,在新的 crypto/uuid 中,所有的 New 函数都去掉了冗余的 error 返回值,极大地净化了调用方的代码。

// 以前
id, err := uuid.NewRandom()
if err != nil { ... }

// 现在
id := uuid.New() // 爽!

为什么只提供 V4 和 V7?V1、V3、V5 呢?

Go 安全团队负责人 Roland Shoemaker 对开源生态进行了大规模的数据挖掘,发现:

  • 超过 90% 的调用是生成随机 UUID(V4)。
  • 生成 V5 的函数(NewSHA1)使用率不足 0.05%

基于“如无必要,勿增实体”的原则,官方决定只提供 V4 和 V7。

  • NewV4:当你只需要一个纯随机的唯一标识符时。
  • NewV7:当你的标识符会被用作数据库主键,且你希望获得更好的插入性能(时间局部性)时。

如果你真的需要 V5 这种基于 SHA-1 的弱哈希 UUID 怎么办?社区的回答是:自己写,或者继续用第三方库。标准库不应该为这种罕见且安全性存疑的场景买单。

V7 的争议:要不要提供时间偏移量(Offset)?

这是提案中最激烈的交锋之一。

一些数据库专家强烈要求提供类似 NewV7WithOffset(offset) 的方法。他们认为,在极高并发的分布式数据库中,完全连续的时间戳会导致 B 树索引的写入热点(Hotspot)。通过稍微偏移时间戳,可以打散写入压力。同时,偏移也能隐藏真实的创建时间,保护隐私。

然而,Go 核心团队(neild)坚决拒绝了这个提议:

  • 偏离规范:RFC 9562 的初衷就是利用时间局部性。如果你故意打乱时间,那为什么要用 V7?不如直接用 V4。
  • 隐私悖论:如果你担心泄露创建时间,V7 本身就不是正确的选择。
  • 增加复杂性:这属于极少数高级数据库引擎才会考虑的特性,不应该污染基础库的通用 API。

为什么没有 Version() 和 Time() 等解析方法?

在 google/uuid 中,你可以通过方法提取 UUID 的版本号或时间戳。但在新标准库中,这些被全部砍掉。

原因:遵循 RFC 9562 的“不透明性”(Opacity)原则。规范明确指出:“建议尽可能将 UUID 视为不透明(Opaque)的值,除非绝对必要,应避免解析 UUID。”

UUID 是用来比较和标识的,不是用来承载业务逻辑的。如果你试图从 UUID 中提取时间,并依此执行业务判断,那么你的架构设计大概率出了问题。

数据库集成与生态迁移

对于 Gopher 来说,UUID 最常见的作用就是存入数据库。

google/uuid 之所以流行,很大程度上是因为它实现了 database/sql/driver.Valuer 和 sql.Scanner 接口,可以无缝与各种 ORM(如 GORM)和数据库驱动配合。

令人惊讶的是,新的 crypto/uuid 并没有实现这些接口。

这是因为 Go 团队认为方向搞反了。 不应该是底层的 crypto 库去依赖 database/sql,而应该是 database/sql 原生认识 UUID 这种基础类型。

目前的计划是,与 crypto/uuid 同步,修改 database/sql 和底层驱动框架,使其在遇到 uuid.UUID 类型时,自动完成与字符串(或字节)的转换。这种解耦设计更加优雅。

小结

crypto/uuid 的提案,表面上只是增加了一个小小的包,实则又是一场关于 Go 工程哲学的集中展示:

  1. 极度克制:砍掉 99% 开发者不需要的 80% 的功能(V1-V3, V5, 提取内部信息),只保留最核心的骨架(V4, V7, 解析, 格式化)。
  2. 安全性优先:放在 crypto 目录下,强调其依赖密码学安全的随机数;拒绝支持已被攻破的弱哈希算法(MD5/SHA1)。
  3. 零冗余处理:借助语言底层的进化(crypto/rand 必定成功),去掉了无意义的 error 返回。

对于我们普通的 Go 开发者来说,未来的迁移路径将非常简单:

当 Go 版本更新后,我们只需要将 import 路径从 github.com/google/uuid 替换为 crypto/uuid。由于底层类型都是 [16]byte,甚至不用担心性能下降。

告别那些臃肿的、历史包袱沉重的第三方库,拥抱一个清爽、安全、原生的 crypto/uuid,Gopher 们,准备好了吗?

资料链接:https://github.com/golang/go/issues/62026


你会第一时间切换吗?

面对即将到来的原生 crypto/uuid,你是支持“极简主义”的官方版本,还是离不开功能丰富的 google/uuid?在你的项目中,UUID V7 的单调递增特性是否真的解决了数据库索引碎片的问题?

欢迎在评论区分享你的看法,我们一起坐等 Go 1.27!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Go 1.26 发布在即,为何 json/v2 依然“难产”?七大技术路障全解析

本文永久链接 – https://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks

大家好,我是Tony Bai。

Go 1.26 预计将于本月(2026 年 2 月)正式发布。然而,在即将到来的 release notes 的欢呼声中,有一个备受瞩目的名字依然带着“实验性”的标签躲在 GOEXPERIMENT 背后——那就是 encoding/json/v2

作为 Go 生态中最核心的基础设施之一,JSON 库的每一次呼吸都牵动着数百万开发者的神经。从 v1 到 v2,不仅仅是性能的提升,更是一场关于API 设计哲学、向后兼容性与极致性能的艰难博弈。

很多人以为 v2 的延迟是因为“官方动作慢”或“设计理念之争”。但当我们深入 json/v2 工作组的看板,剥开表层的讨论,会发现横亘在稳定版之前的,是七个具体而微、却又关乎全局的技术“钉子”。这些问题并非宏大的路线图分歧,而是关乎浮点数精度、错误处理语义、API 封装性等实打实的工程细节。

本文将基于最新的 GitHub Issues 讨论(截至 2026 年 2 月),带你通过显微镜审视这七大阻塞问题,一窥 Go 标准库演进背后的严谨与妥协。

七大阻塞问题(Blockers)一览

深度解析:魔鬼藏在细节中

1. API 设计的“丑陋妥协”:jsontext.Internal (#73435)

在当前的 encoding/json/jsontext 包中,竟然存在一个导出的 Internal 类型。这在 Go 标准库的审美中,简直是“房间里的大象”。

jsontext 是 v2 引入的底层包,专注于 JSON 的语法解析(Tokenizing),而上层的 json 包负责语义绑定(Binding)。为了让上层包能够访问底层的缓冲区或状态机,当前的实现不得不导出一个 Internal 符号。

这违背了 Go 标准库的黄金法则之一:公共 API 必须是为用户设计的,而不是为实现者自己设计的。

Joe Tsai (dsnet) 提出了一种解决方案:将 jsontext 的核心逻辑移入 encoding/json/internal/jsontext,然后通过类型别名(Type Alias)在公共包中暴露 API。然而,这带来了一个新的难题:godoc 对类型别名的支持并不友好,生成的文档可能会让用户感到困惑,因为方法都挂载在内部类型上。

这个问题已经上升为工具链生态问题。如果这个问题不解决,v2 发布后将面临两个风险:要么用户依赖了这个“临时” API 导致未来无法修改,要么标准库留下了一个永久的“伤疤”。

2. 致命的递归:当 Unmarshaler 遇到指针 (#75361)

这是一个真实且诡异的 Bug。一位开发者在迁移旧代码时发现,以下模式在 v1 中正常工作,但在开启 GOEXPERIMENT=jsonv2 后会导致栈溢出(Stack Overflow):

type MyType string

// 自定义 Unmarshal 方法
func (m *MyType) UnmarshalJSON(b []byte) error {
    // 试图通过定义一个新类型来“剥离”当前类型的方法,以回退到默认行为
    type MyTypeNoMethods *MyType
    var derived MyTypeNoMethods = MyTypeNoMethods(m)

    // v2 在这里会错误地再次识别出 derived 拥有 UnmarshalJSON 方法
    // 从而导致无限递归调用自己
    return json.Unmarshal(b, derived)
}

在 v1 中,开发者习惯通过类型转换来“剥离”自定义方法。但在 v2 中,为了修复 v1 中某些指针方法无法被调用的 Bug(如 #22967),引入了更激进的方法集查找逻辑

v2 的逻辑是:只要这个值的地址(Addressable)能找到 UnmarshalJSON 方法,就调用它。在上面的例子中,derived 虽然是新类型,但它底层的指针指向的还是 MyType,v2 过于“聪明”地认为应该调用 (MyType).UnmarshalJSON,结果造成了死循环。

这是一个典型的“修复了一个 Bug,却引入了另一个 Bug”的案例。Go 团队目前倾向于保留 v2 的正确逻辑(即更一致的方法调用),但也必须为这种遗留代码提供一种检测机制。目前的计划是引入运行时检测或 go vet 检查,明确告知用户:请使用 type MyTypeNoMethods MyType(非指针别名)来剥离方法,而不是使用指针别名。

3. 浮点数的“薛定谔精度”:float32 (#76430)

下面是展示该问题的一段示例代码:

var f float32 = 3.1415927 // math.Pi 的 float32 近似值
json.Marshal(f)

输出应该是 3.1415927(保持 float32 精度),还是 3.1415927410125732(提升到 float64 精度以确保无损)?

Go v1 的 json 包为了兼容性,倾向于将所有浮点数视为 float64 处理。这导致 float32 在序列化时经常会出现“精度噪音”——那些用户并不想要的、只有在 float64 精度下才有意义的尾数。

然而,v2 的 jsontext 包默认使用 64 位精度。这导致了 json.Marshal(上层)和 jsontext.Encoder(底层)在行为上的不一致。

  • 用户期望:float32 就该像 float32,短小精悍。
  • 技术现实:JSON 标准(RFC 8259)并没有区分浮点精度。
  • 性能视角:处理 32 位浮点数理论上更快,但需要专门的算法路径。

Go 团队正在考虑引入 Float32 构造器和访问器到 jsontext 包中,并修改底层的 AppendFloat 逻辑,以支持显式的 32 位浮点数格式化。这不仅是为了“好看”,更是为了数值正确性——避免“双重舍入”(Double Rounding)带来的微小误差。

4. 选项系统的“任督二脉”:透传难题 (#76440)

你调用 json.Marshal(v, json.WithIndent(” “)) 很爽,但如果你想控制底层的 jsontext 行为(比如“允许非法 UTF-8”或“允许重复键名”),你发现:顶层函数把路堵死了。目前的 MarshalEncode 只接受 json.Option,不接受 jsontext.Option。

v2 将 json(语义层)和 jsontext(语法层)拆分是架构的一大进步。但这也带来了配置穿透的问题。

如果为了保持 API 纯洁,强迫用户必须先创建一个 jsontext.Encoder 并在那里配置选项,再传给 json.MarshalEncode,那么 99% 的简单用例都会变得无比繁琐。

Go团队给出的提案是打破层级隔离,允许 json.Marshal 等顶层函数直接接受 jsontext.Option。这是一个实用主义战胜洁癖的胜利。

5. 功能做减法:unknown 标签的存废 (#77271)

v2 曾引入了一个 unknown 结构体标签,用于指示某个字段专门用来捕获所有未知的 JSON 字段。同时,还有一个 DiscardUnknownMembers 选项用于丢弃未知字段。

dsnet(Joe Tsai)发起提案,建议删除两个功能。理由如下:

  1. 功能重叠:v2 已经引入了 inline 标签,它与 unknown 的行为非常相似,仅仅是语义上的微小差别(是否包含“已知”字段)。这种微小的差别会让用户感到困惑。
  2. API 极简主义:如果用户真的需要处理未知字段,可以通过自定义 Unmarshaler 来实现,或者利用 inline 标签配合后期处理。
  3. 向后兼容的智慧:添加功能永远比删除功能容易。现在删除,未来如果真有需求还可以加回来;但如果现在保留,未来想删就难了。

6. 控制流的缺失:SkipFunc (#74324)

json.SkipFunc 是 v2 引入的一个 Sentinel Error,用于告诉编码器“跳过当前字段/值”。目前它只能在 MarshalToFunc(用户自定义函数)中使用。但如果我在类型的方法 MarshalJSONTo 中想跳过自己怎么办?目前是不支持的。

这是一个典型的“二等公民”问题。用户自定义的函数拥有比类型方法更高的权限。这导致在迁移旧代码时,如果要实现“条件性跳过”,必须写出非常丑陋的 hack 代码(比如定义一个空结构体来占位)。

允许 MarshalJSONTo 返回 SkipFunc 看似简单,但它要求调用者必须处理这个错误。这意味着不能直接调用 v.MarshalJSONTo,而必须通过 json.Marshal 来调用,否则你会收到一个未处理的错误。这需要文档和工具链的配合。

7. 文档真空:新接口的最佳实践 (#76712)

v2 引入了 MarshalerTo 和 UnmarshalerFrom 两个高性能接口,它们直接操作 jsontext.Encoder/Decoder,避免了内存分配。但是,到底该什么时候用它们?

目前缺乏明确的文档指导。如果用户在任何时候都直接调用 v.MarshalJSONTo(enc),可能会绕过 json.Marshal 中处理的许多全局选项(如大小写敏感、省略零值等)。

Go 团队计划在文档中明确:这属于“高级 API”,普通用户应始终使用 json.Marshal,除非你在编写极其底层的库。

路线图:我们何时能用上“真v2”?

根据最新的工作组纪要和 Issue 状态,我们可以画出一条清晰的时间线:

  • 当前 (Go 1.26, 2026.02):GOEXPERIMENT=jsonv2 继续存在。v2 代码库已进入主仓库,但 API 仍未冻结。此时适合库作者进行集成测试,但不建议在生产环境核心业务中大规模铺开。
  • 决战期 (2026 H1):必须彻底解决上述 7 个 Blocker。特别是 API 签名相关的修改(如 float32 支持和 SkipFunc),一旦定型就是 10 年承诺。
  • 目标 (Go 1.27, 2026.08):如果一切顺利,我们有望在今年 8 月发布的 Go 1.27 中,看到移除实验标签、正式可用的 encoding/json/v2。这意味着 Go 语言将迎来其历史上最大规模的标准库升级之一。

小结:给 Gopher 的建议

  1. 别急着重构:现有的 encoding/json (v1) 依然稳健。除非你有极端的性能需求(v2 性能提升显著)或需要 v2 独有的某些特性,否则请按兵不动。
  2. 关注 jsontext:即使不用 v2 的序列化,新独立的 jsontext 包也是一个处理 JSON Token 流的神器,非常适合写高性能的底层解析工具。它的 API 设计比 v1 的 Scanner 更加现代化和高效。
  3. 参与反馈:现在是影响 Go 未来 10 年 JSON 处理方式的最后窗口期。如果你对上述 Issue 有独到见解,去 GitHub 上发声吧!

Go 团队的“慢”,是对生态的“敬”。这七个拦路虎,每一个都是为了让未来的十年里,我们能写出更少 Bug、更快速度的 Go 代码。好事多磨,让我们静候佳音。

参考资料

  • json/v2 工作组的看板 – https://github.com/orgs/golang/projects/50
  • encoding/json/v2: working group meeting minutes – https://github.com/golang/go/issues/76406

你更在意什么?

Go 团队为了 API 的洁癖和严谨,宁愿让 json/v2 多飞一会儿。在你的开发实践中,你更倾向于“尽快用上新特性”,还是“哪怕慢一点也要保证接口设计的绝对完美”?你对 float32 的精度噪音有切肤之痛吗?

欢迎在评论区分享你的看法,我们一起坐等 Go 1.26 官宣!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

CSAPP Cache Lab II: Optimizing Matrix Transposition

In this part of the Cache Lab, the mission is simple yet devious: optimize matrix transposition for three specific sizes: 32x32, 64x64, and 61x67. Our primary enemy? Cache misses.

Matrix Transposition

A standard transposition swaps rows and columns directly:

1
2
3
4
5
6
7
8
9
10
11
12
void trans(int M, int N, int A[N][M], int B[M][N])
{
int i, j, tmp;

for (i = 0; i < N; i++) {
for (j = 0; j < M; j++) {
tmp = A[i][j];
B[j][i] = tmp;
}
}

}

While correct, this approach is a cache-miss nightmare because it ignores how data is actually stored in memory.

Cache Overview

To optimize effectively, we first have to understand our hardware constraints. The lab specifies a directly mapped cache with the following parameters:

ParameterValue
Sets (S)32
Block Size (B)32 bytes
Associativity (E)1 (Direct-mapped)
Integer Size4 bytes
Capacity per line8 integers

We will use Matrix Tiling and Loop Unrolling to optimize the codes.

32x32 Case

In this case, a row of the matrix needs 32/8 = 4 sets of cache to store. And cache conflicts occur every 32/4 = 8 rows. This makes 8x8 tiling the sweet spot.

By processing the matrix in 8×88 \times 8 blocks, we ensure that once a line of A is loaded, we use all 8 integers before it gets evicted. We also use loop unrolling with 8 local variables to minimize the overhead of accessing B.

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
int i,j,k;
int tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8;
for(i = 0; i<N; i+=8){
for(j = 0; j<M; j+=8){
for(k = i; k<N && k<i+8; k++) {
// Read row from A
tmp1 = A[k][j];
tmp2 = A[k][j+1];
tmp3 = A[k][j+2];
tmp4 = A[k][j+3];
tmp5 = A[k][j+4];
tmp6 = A[k][j+5];
tmp7 = A[k][j+6];
tmp8 = A[k][j+7];

// Write to columns of B
B[j][k] = tmp1;
B[j+1][k] = tmp2;
B[j+2][k] = tmp3;
B[j+3][k] = tmp4;
B[j+4][k] = tmp5;
B[j+5][k] = tmp6;
B[j+6][k] = tmp7;
B[j+7][k] = tmp8;
}
}
}

61x67 Case

Since 61 and 67 are not powers of two, the conflict misses don’t occur in a regular pattern like they do in the square matrices. This “irregularity” is actually a blessing. We can get away with simple tiling. A 16x16 block size typically yields enough performance to pass the miss-count threshold.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int BLOCK_SIZE = 16;
int i,j,k,l,tmp;
int a,b;
for(i = 0; i<N; i+=BLOCK_SIZE){
for(j = 0; j<M; j+=BLOCK_SIZE){
a = i+BLOCK_SIZE;
b = j+BLOCK_SIZE;
for(k = i; k<N && k<a; k++) {
for(l = j; l<M && l<b; l++){
tmp = A[k][l];
B[l][k] = tmp;
}
}
}
}

64x64 Case

This is the hardest part. In a 64x64 matrix, a row needs 8 sets, but conflict misses occur every 32/8=432/8 = 4 rows. If we use 8x8 tiling, the bottom half of the block will evict the top half.

We can try a 4x4 matrix tiling first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int BLOCK_SIZE = 4;
int i,j,k,l,tmp;
int a,b;
for(i = 0; i<N; i+=BLOCK_SIZE){
for(j = 0; j<M; j+=BLOCK_SIZE){
a = i+BLOCK_SIZE;
b = j+BLOCK_SIZE;
for(k = i; k<N && k<a; k++) {
for(l = j; l<M && l<b; l++){
tmp = A[k][l];
B[l][k] = tmp;
}
}
}
}

But this isn’t enough to pass the miss-count threshold.

We try a 8x8 matrix tiling. We solve this by partitioning the 8×88 \times 8 block into four 4×44 \times 4 sub-blocks and using the upper-right corner of B as a “buffer” to store data temporarily.

Block A=(ATLATRABLABR)TransposeBlock B=(ATLTABLTATRTABRT)\text{Block } A = \begin{pmatrix} A_{TL} & A_{TR} \\ A_{BL} & A_{BR} \end{pmatrix} \quad \xrightarrow{\text{Transpose}} \quad \text{Block } B = \begin{pmatrix} A_{TL}^T & A_{BL}^T \\ A_{TR}^T & A_{BR}^T \end{pmatrix}

Here are the steps:

  1. Transpose ATLA_{TL} into BTLB_{TL} while simultaneously moving ATRA_{TR} into BTRB_{TR} (as a temp storage).
  2. Move the stored ATRA_{TR} from BTRB_{TR} to its final position, while moving ABLA_{BL} into its spot.
  3. Transpose ABRA_{BR} into BBRB_{BR}.
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
int i, j, k;
int tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8;

// Iterate through the matrix in 8x8 blocks to improve spatial locality
for (i = 0; i < N; i += 8) {
for (j = 0; j < M; j += 8) {

/**
* STEP 1: Handle the top half of the 8x8 block (rows i to i+3)
*/
for (k = 0; k < 4; k++) {
// Read 8 elements from row i+k of matrix A into registers
tmp1 = A[i + k][j]; tmp2 = A[i + k][j + 1];
tmp3 = A[i + k][j + 2]; tmp4 = A[i + k][j + 3]; // Top-left 4x4
tmp5 = A[i + k][j + 4]; tmp6 = A[i + k][j + 5];
tmp7 = A[i + k][j + 6]; tmp8 = A[i + k][j + 7]; // Top-right 4x4

// Transpose top-left 4x4 from A directly into top-left of B
B[j][i + k] = tmp1;
B[j + 1][i + k] = tmp2;
B[j + 2][i + k] = tmp3;
B[j + 3][i + k] = tmp4;

// Temporarily store top-right 4x4 of A in the top-right of B
// This avoids cache misses by using the already-loaded cache line in B
B[j][i + k + 4] = tmp5;
B[j + 1][i + k + 4] = tmp6;
B[j + 2][i + k + 4] = tmp7;
B[j + 3][i + k + 4] = tmp8;
}

/**
* STEP 2: Handle the bottom half and fix the temporary placement
*/
for (k = 0; k < 4; k++) {
// Read bottom-left 4x4 column-wise from A
tmp1 = A[i + 4][j + k]; tmp2 = A[i + 5][j + k];
tmp3 = A[i + 6][j + k]; tmp4 = A[i + 7][j + k];

// Read bottom-right 4x4 column-wise from A
tmp5 = A[i + 4][j + k + 4]; tmp6 = A[i + 5][j + k + 4];
tmp7 = A[i + 6][j + k + 4]; tmp8 = A[i + 7][j + k + 4];

// Retrieve the top-right elements we temporarily stored in B in Step 1
int t1 = B[j + k][i + 4];
int t2 = B[j + k][i + 5];
int t3 = B[j + k][i + 6];
int t4 = B[j + k][i + 7];

// Move bottom-left of A into the top-right of B
B[j + k][i + 4] = tmp1;
B[j + k][i + 5] = tmp2;
B[j + k][i + 6] = tmp3;
B[j + k][i + 7] = tmp4;

// Move the retrieved temporary values into the bottom-left of B
B[j + k + 4][i] = t1;
B[j + k + 4][i + 1] = t2;
B[j + k + 4][i + 2] = t3;
B[j + k + 4][i + 3] = t4;

// Place bottom-right of A into the bottom-right of B
B[j + k + 4][i + 4] = tmp5;
B[j + k + 4][i + 5] = tmp6;
B[j + k + 4][i + 6] = tmp7;
B[j + k + 4][i + 7] = tmp8;
}
}
}

Note: The key trick here is traversing B by columns where possible (so B stays right in the cache) and utilizing local registers (temporary variables) to bridge the gap between conflicting cache lines.

Conclusion

Optimizing matrix transposition is less about the math and more about mechanical sympathy—understanding the underlying hardware to write code that plays nice with the CPU’s cache.

The jump from the naive version to these optimized versions isn’t just a marginal gain; it’s often a 10x reduction in cache misses. It serves as a stark reminder that in systems programming, how you access your data is just as important as the algorithm itself.

🔲 ☆

大项目构建太慢?Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间

本文永久链接 – https://tonybai.com/2026/02/05/brad-fitzpatrick-cachelink-reduce-go-test-wait-time

大家好,我是Tony Bai。

在维护大型 Go 单体仓库(Monorepo)时,你是否遇到过这样的场景:明明只是修改了测试的运行参数(比如 -run 的正则),或者在不同的 CI 节点上运行同一个包的测试,却发现 go test 依然在缓慢地执行“链接(Linking)”步骤?

对于代码量巨大的项目,链接过程往往是构建链条中最耗时的一环。为了解决这一痛点,Go 社区领袖、Tailscale 核心开发者 Brad Fitzpatrick 近日提交了 #77349 提案,建议引入 -cachelink 标志。这一看似微小的改动,有望在分布式测试和重复执行场景下,显著“挤出”原本被浪费的等待时间。

被忽视的瓶颈:重复链接的代价

Go 的构建缓存(GOCACHE)机制已经非常高效,它能很好地缓存编译阶段的中间产物(.a 文件)。但是,当你运行 go test 时,工具链的最后一步——将所有依赖链接成一个可执行的测试二进制文件——通常是“一次性”的。

这意味着,即使你的代码没有任何变动,只要测试指令稍有变化(例如多次运行 go test 但指定不同的测试用例),Go 工具链往往会重新触发链接器。

# 第一次运行:链接 + 执行
$ go test -run=^TestFoo$ ./pkg/

# 第二次运行(代码未变):依然触发重新链接 + 执行
$ go test -run=^TestBar$ ./pkg/

对于依赖项数以千计的大型项目,链接过程可能长达数秒甚至更久。在本地频繁调试或 CI 流水线中,这些重复的秒数累积起来就是巨大的时间浪费。

Brad 的解法:-cachelink

Brad Fitzpatrick 的提案非常直接:允许将链接器输出的最终测试二进制文件,也写入 GOCACHE。

通过显式开启 -cachelink,go test 的行为将发生变化:

  1. 它会基于构建输入(代码、依赖、环境变量等)计算哈希。
  2. 如果发现 GOCACHE 中已经存在已链接好的测试二进制文件。
  3. 直接跳过链接步骤,复用该文件进行测试。

这样,上述例子中的第二次调用将瞬间启动,因为最耗时的构建步骤被完全省去了。

为什么不做成默认行为?

既然能提速,为什么不默认开启?Brad 在提案讨论中给出了专业的权衡分析:

空间 vs. 时间

测试二进制文件通常包含完整的符号表和调试信息,体积比普通的中间对象文件大得多。如果默认缓存所有测试二进制文件,开发者的磁盘空间(GOCACHE)会迅速膨胀。因此,这是一个以空间换时间的策略,更适合由开发者根据项目规模手动开启,或者在 CI 环境中配置。

分布式 CI 的“加速器”

该提案真正的杀手级应用场景是 分布式 CI 系统。

许多大厂使用 GOCACHEPROG 来在构建集群间共享缓存。在典型的 CI 流程中,测试任务往往会被分片(Sharding)到数十台机器上并发执行。

  • 现状:每一台机器拉取源码后,都需要各自进行一次链接操作,浪费计算资源。
  • 引入 -cachelink 后:第一台完成构建的机器会将二进制文件上传到共享缓存。后续几十台机器直接下载该文件并运行,全集群的链接成本降为“1”。

不仅是 go test -c

有经验的开发者可能会问:“我为什么不直接用 go test -c 手动编译成二进制文件,然后分发运行呢?”

Brad 指出,手动管理二进制文件会绕过 Go 原生的测试结果缓存。而 -cachelink 的精妙之处在于,它既复用了二进制文件,又保留了 go test 完整的缓存与输出管理体验。你不需要编写复杂的脚本来管理这些文件,一切依然由 go 命令自动处理。

小结

目前,该提案已进入活跃评审阶段,并有了初步的代码实现。对于深受“构建慢”和“测试慢”困扰的大型项目维护者来说,这无疑是一个值得期待的性能优化利器。我们有望在 Go 1.27 或后续版本中见证它的落地。

资料链接:https://github.com/golang/go/issues/77349


聊聊你的构建之苦

链接时间正在成为你的“带薪摸鱼”理由吗?在你的项目中,go test 运行一次通常需要多久?你为了缩短测试反馈周期,还尝试过哪些黑科技(比如 GOCACHEPROG)?

欢迎在评论区分享你的实战经验或吐槽!让我们一起期待 -cachelink 的落地。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

算法神话的祛魅:Russ Cox 与浮点数转换的 15 年求索之路

本文永久链接 – https://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion

大家好,我是Tony Bai。

“浮点数到十进制的转换一直被认为很难。但本质上,它们非常简单直接。” —— Russ Cox (2011)

“我错了。快速的转换器也可以很简单,这篇文章将展示如何做到。” —— Russ Cox (2026)

在计算机科学的深处,潜伏着一条名为“浮点数转换”的恶龙。将一个二进制浮点数(如 float64)转换为人类可读的十进制字符串(如 “0.1″),看似简单,实则是一个困扰了业界半个世纪的难题。

2011 年,Go 语言的核心人物 Russ Cox 写下了一篇博文,试图用一种简单的算法来“驯服”这条龙。然而,在随后的十几年里,学术界和工业界爆发了一场军备竞赛:Dragon4, Grisu3, Ryū, Schubfach, Dragonbox… 每一个新算法都试图在速度上压倒前一个,但也让代码变得越来越复杂,数学证明越来越晦涩。

2026 年初,Russ Cox 带着他的新系列文章强势回归。这一次,他不仅带来了一套比所有已知算法都更快的全新算法,而且证明了:极致的性能不需要极致的复杂性。

这套算法已被确定将在 Go 1.27 (2026年8月) 中发布。今天,我们就来深度解析这项可能改写浮点数处理历史的技术突破。

历史的迷宫与“不可能三角”

要理解 Russ Cox 的成就,我们首先要理解这个问题的难度。一个完美的浮点数打印算法,必须同时满足三个苛刻的条件(“不可能三角”):

  1. 正确性 (Correctness):转换必须是双射的。Parse(Print(f)) == f 必须恒成立。这意味着你不能随意丢弃精度。
  2. 最短性 (Shortest):输出的字符串必须是所有能转回原值的字符串中最短的。例如,0.3 在二进制中无法精确表示,打印时应该是 “0.3″ 而不是 “0.2999999999999999889″。
  3. 速度 (Speed):在大规模数据处理(如 JSON 序列化)中,转换速度直接决定了系统的吞吐量。

历史的演进:
* Dragon4 (1990):实现了正确性和最短性,但依赖大整数(BigInt)运算,慢如蜗牛。
* Grisu3 (2010):Google 的 V8 引擎引入。速度极快,但不保证最短性,约 0.5% 的情况会失败并回退到慢速算法。
* Ryū (2018) & Dragonbox (2020):通过复杂的数学技巧(查表法),终于在不使用 BigInt 的情况下实现了正确且最短。这是性能的巅峰,但代码极其复杂,充满魔术数字。

Russ Cox 的目标,就是打破这个迷宫:能不能既像 Ryū 一样快且正确,又像 2011 年的那个算法一样简单?

核心技术——“未舍入缩放” (Unrounded Scaling)

Russ Cox 的新算法核心,源于一个极其精妙的数学原语:快速未舍入缩放 (Fast Unrounded Scaling)

什么是“未舍入数”?

在传统算法中,我们总是纠结于“何时舍入”。Russ Cox 引入了 “未舍入数” (Unrounded Number) 的概念 ⟨x⟩。它由三部分组成:

  • 整数部分: floor(x)
  • ½ bit: 标记 x – floor(x) >= 0.5
  • sticky bit (粘滞位): 标记 x 是否有非零的小数残余。

这种表示法不仅保留了用于正确舍入(Round half to even)的所有必要信息,而且可以通过极其廉价的位运算(| 和 &)来维护。这就像是在计算过程中保留了一个“高精度的尾巴”,直到最后一步才决定如何截断。

缩放的魔法

浮点数打印本质上是计算 f = m * 2^e 对应的十进制 d * 10^p。核心步骤是将 m * 2^e 乘以 10^p。

Russ Cox 使用查表法(预计算 10^p 的 128 位近似值)来实现这一缩放。但他最惊人的发现是:在 64 位浮点数转换的场景下,我们甚至不需要完整的 128 位乘法!

他证明了:只需计算 64 位 x 64 位的高位结果,并利用低位的“粘滞位”来修正,就能得到完全正确的结果。这意味着,曾经需要几十次乘法或大整数运算的转换过程,现在被缩减为极少数几次 CPU 原生乘法

这一发现被称为 “Omit Needless Multiplications”(省略不必要的乘法),它是新算法性能超越 Ryū 的关键。

从理论到 Go 1.27

基于这个核心原语,Russ Cox 构建了一整套算法家族:

  • FixedWidth: 定点打印(如 %.2f)。
  • Shortest: 最短表示打印(如 %g)。
  • Parse: 字符串转浮点数。

性能碾压

Russ Cox 在 Apple M4 和 AMD Ryzen 9 上进行了详尽的基准测试:

  • 定点打印:新算法 (uscale) 显著快于 glibc 和 double-conversion,甚至快于 Ryū。
  • 最短打印:在纯算法层面,新算法与业界最快的 Dragonbox 持平或更快,但代码逻辑要简单得多。
  • 解析:同样基于该原理的解析算法,性能超越了目前业界标杆 fast_float (Eisel-Lemire 算法)。

更令人兴奋的是,Go 1.27 将直接集成这套算法或算法的一部分。对于 Gopher 来说,这意味着你的 fmt.Sprintf、json.Marshal 和 strconv.ParseFloat 将在下个版本中自动获得显著的性能提升,而无需修改一行代码。

证明的艺术

除了代码,Russ Cox 还做了一件很“极客”的事:他用 Ivy(一种 APL 风格的语言)编写了完整的数学证明。

他没有选择形式化验证工具(如 Coq),而是通过编写可执行的代码来验证算法在每一个可能的 float64 输入下都是正确的。这种“通过计算来证明” (Proof by Computation) 的方法,不仅验证了算法的正确性,也为后来者留下了一份可交互的、活生生的文档。

小结:简单是终极的复杂

从 2011 年的初次尝试,到 2026 年的最终突破,Russ Cox 用 15 年的时间完成了一个完美的闭环。

这一系列文章是一种工程哲学的胜利。它告诉我们:当我们面对复杂的遗留问题时,不要只是盲目地堆砌优化技巧。回到数学的源头,重新审视问题的本质,或许能找到那条既简单又快的“捷径”。

现在的 Go 标准库中,即将拥有一颗比以往任何时候都更强大、更轻盈的“心脏”。

资料链接:https://research.swtch.com/fp-all


你更看重哪一点?

在算法的世界里,正确性、最短表示、运行速度,这“不可能三角”总是让我们反复权衡。在你平时的开发中,有哪些场景曾让你被浮点
能或精度困扰?或者,你对 Russ Cox 这种“死磕 15 年”的工程精神有何感触?

欢迎在评论区分享你的看法!如果这篇文章让你对浮点数实现算法方面有了新的认识,别忘了点个【赞】和【在看】,并转发给你的Go开发朋友们!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Git 即数据库:Beads (bd) —— 专为 AI Agent 打造的分布式任务追踪引擎

本文永久链接 – https://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent

大家好,我是Tony Bai。

在 AI 编码智能体(如 Claude CodeGemini CLI 等)日益普及的今天,我们面临着一个棘手的工程难题:AI Agent 虽然极其聪明,但它们通常是”健忘”的。

它们在处理一个长期、复杂的重构任务时,往往会在海量的上下文切换中迷失方向。传统的 Issue Tracker(如 Jira)对 AI 来说太重、太慢且难以集成;而简单的 Markdown 文件又缺乏结构化和版本控制。

于是,Beads(命令行工具 bd)应运而生。它是 Gas Town —— 那个被誉为 AI Coding 领域”Kubernetes”的宏大愿景 —— 的底层记忆基石。它巧妙地利用 Git 作为分布式数据库,为 AI Agent 提供了一个持久化、可协作、依赖感知的任务追踪系统。

为什么 AI Agent 需要 Beads?

传统的软件工程工具是为人类设计的,而 Beads 是为AI 智能体设计的。

上下文的持久化

AI 模型的上下文窗口(Context Window)虽然越来越大,但依然昂贵且有限。当一个 Agent 需要处理跨越数周、涉及数百个文件的任务时,它不能一直把所有信息都塞进 Prompt 里。

Beads 提供了一个外部的、结构化的存储,让 Agent 可以随时”卸载”和”重载”任务状态,就像人类使用笔记本一样。

原生的依赖管理

复杂的编码任务往往是一张有向无环图(DAG):“先重构数据库 Schema,再更新 API,最后修复前端。”

Beads 原生支持任务依赖(Dependency Graph)。它能自动计算出当前的 Ready Work(就绪工作) ,告诉 Agent:”别瞎忙,现在你只能做这个,其他的都还被阻塞着。”

分布式协作

如果是多个 Agent(或者人类与 Agent)同时工作怎么办?

Beads 将任务数据存储为 .beads/ 目录下的 JSONL 文件。 这意味着:任务即代码。你可以像合并代码一样,通过 Git 分支、合并、解决冲突来管理任务。

核心架构:Git as a Database

Beads 的设计哲学极其硬核:它不想引入任何外部的中心化服务,它只想利用你现有的 Git 仓库。

三层分层架构:清晰的职责边界

Beads 采用了经典的三层架构,但在每一层都做了针对性的优化:

  • CLI Layer:基于 spf13/cobra 构建,负责命令解析和用户交互。 它不直接操作数据,而是优先通过 RPC 与守护进程通信,失败时才降级到直接数据库访问。
  • Daemon Process:每个工作区运行独立的后台守护进程,处理 RPC 请求、协调自动同步时机,并持有数据库连接以加速查询。通过 Unix domain socket(Windows 上为 named pipes)进行通信。
  • Storage Layer:这是核心。它通过接口隔离原则定义了 Storage 接口,支持 SQLite、Dolt 甚至内存等多种后端。这种设计使得底层的存储实现可以被轻松替换,而不影响上层逻辑。

更为完整的架构参考下图:


来自deepwiki.com对beads源码的分析结果

双存储写屏障:SQLite 与 JSONL 的完美同步

Beads 最精妙的设计之一是它的双存储写屏障 (Dual-Storage Write Barrier)。它是如何解决 SQLite(高性能查询)与 JSONL(版本控制)之间的数据一致性的呢?

  • 写入路径:当用户创建任务时,数据首先写入 SQLite,保证了毫秒级的操作反馈。
  • 防抖刷新 (Debounced Flush):为了避免频繁的磁盘 I/O 和 Git 操作,Beads 实现了一个基于 Go Channel 的事件驱动 FlushManager。所有 flush 状态(isDirty、needsFullExport、debounceTimer)由单个后台 goroutine 拥有,通过 channel 通信消除了竞态条件。

系统默认配置 30 秒的防抖窗口(可通过 flush-debounce 配置调整),这为批量操作提供了”事务窗口”——30 秒内的多次修改会被合并成一次 JSONL 导出,避免了频繁的 Git commit。

这种机制确保了在高频操作下(如批量导入),系统不会因为频繁的 Git Commit 而卡顿。

并发安全与锁机制

在分布式和多进程环境下,数据竞争是最大的敌人。Beads 采取了多重防御:

  • 进程级互斥:使用文件锁(Unix 上为 flock,Windows 上为 LockFileEx)对守护进程(daemon.lock)和同步操作(.sync.lock)加锁,确保同一时间只有一个守护进程在运行,且不会有并发的 sync 操作导致数据损坏。

  • 数据库连接池优化:SQLite 连接池根据数据库类型进行智能配置:

    • 内存数据库:SetMaxOpenConns(1) 强制单连接,因为 SQLite 的内存数据库在连接间是隔离的。
    • 文件数据库:SetMaxOpenConns(runtime.NumCPU() + 1) 利用 SQLite WAL 模式的”1 writer + N readers”特性,同时防止写锁竞争导致的 goroutine 堆积。
  • Context 传播:所有的存储操作都强制要求传递 context.Context,确保了超时控制和优雅退出的能力,这对于一个长期运行的后台守护进程至关重要。

自适应哈希 ID:算法的胜利

为了在”简短易读”和”全局唯一”之间取得平衡,Beads 没有使用 UUID,而是设计了一套自适应哈希算法

  • 综合哈希源:ID 并非简单的标题哈希,而是综合了 title、description、creator、timestamp 和 nonce 的 SHA256 哈希,确保了即使标题相同,不同时间创建的 issue 也有不同的 ID。

  • Base36 编码:使用 base36(0-9, a-z)而非 hex 编码,提供了更高的信息密度,让 ID 更短。

  • 动态长度:系统根据当前数据库的规模,使用生日悖论数学计算碰撞概率,自动调整 ID 的长度:

    • 小型数据库:bd-a1b2 (4 字符)
    • 中型数据库:bd-a1b2c3 (6 字符)
    • 大型数据库:最多 8 字符
  • 碰撞处理:在生成 ID 时,如果检测到碰撞,系统会尝试最多 10 个 nonce 值,如果仍然碰撞,则增加哈希长度。这是一种典型的用计算换取协作体验的策略。

Beads Issue 状态

Beads 定义了 8 个核心状态

  • open – 可开始的工作
  • in_progress – 正在进行中
  • blocked – 被依赖阻塞
  • deferred – 暂时延期
  • closed – 已完成
  • tombstone – 软删除(30天后清理)
  • pinned – 永久保持开放
  • hooked – AI智能体钩子工作

它们的状态机转换流程如下图所示:

这个状态机设计确保了数据一致性、合并安全性和自动化工作流。

依赖管理的多样性

Beads 支持多种依赖类型,不同类型有不同的语义:

  • blocks:阻塞依赖,Issue X 必须关闭后 Y 才能开始,影响 bd ready 计算
  • parent-child:层级关系,用于 Epic 和子任务,父节点被阻塞时子节点也被阻塞
  • related:软链接,仅用于引用,不影响执行顺序
  • discovered-from:记录在执行某任务时发现的新问题

系统使用递归 CTE(Common Table Expression) 检测循环依赖,确保依赖图始终是一个有向无环图(DAG)。

Blocked Issues Cache:性能的飞跃

在大型项目中,实时计算哪些 issue 被阻塞可能非常耗时。Beads 引入了 Blocked Issues Cache 机制,这是一个关键的性能优化:

  • 问题:在 10K issue 的数据库上,使用递归 CTE 实时计算阻塞状态需要约 752ms。
  • 解决方案:使用 blocked_issues_cache 表物化阻塞计算结果。
  • 效果:查询时间降至约 29ms,性能提升 25 倍

缓存在每次依赖变更或状态变更时完全重建(DELETE + INSERT),虽然重建需要 <50ms,但由于依赖变更相对读取操作非常罕见,这个权衡是值得的。

实战:Agent 的工作流

让我们看看在一个典型的 AI 编码场景中,Beads 是如何工作的。

场景:你需要重构一个遗留系统的用户认证模块。

  1. 初始化与规划
    Agent 首先通过 bd create 创建主任务(Epic)。 注意,Beads 支持层级 ID,这对于 AI 拆解任务非常有帮助。

    # 创建 Epic
    $ bd create "重构用户认证模块"
    Created: bd-auth01
    
    # 拆分子任务(注意:Beads 支持层级结构,或者我们可以手动关联)
    $ bd create "设计新 User 表结构"
    Created: bd-db002
    
    $ bd create "迁移旧数据"
    Created: bd-migr03
    
    $ bd create "切换 API 逻辑"
    Created: bd-api004
    
  2. 建立依赖
    Agent 知道事情有轻重缓急,它会建立依赖关系。根据 bd dep add 格式(child 依赖 parent,即 parent blocks child):

    # bd-migr03 (Child) 依赖于 bd-db002 (Parent)
    # 意味着:必须先设计完表结构,才能迁移数据
    $ bd dep add bd-migr03 bd-db002
    
    # bd-api004 (Child) 依赖于 bd-migr03 (Parent)
    # 意味着:必须先迁移完数据,才能切换 API
    $ bd dep add bd-api004 bd-migr03
    
  3. 获取就绪工作
    Agent 不再迷茫,它只需要问 Beads:”我现在能做什么?”

    $ bd ready --json
    {
      "issues": [
        {
          "id": "bd-db002",
          "title": "设计新 User 表结构",
          "status": "pending",
          "blocks": ["bd-migr03"]
          ...
        }
      ]
    }
    

    Beads 会告诉它,只有”设计表结构”是 Ready 的,其他的都被阻塞了。

  4. 执行与更新
    Agent 完成任务后,关闭 Issue。

    $ bd close bd-db002
    

    此时,后台的 blocked cache 自动重建,”迁移旧数据” (bd-migr03) 的任务状态瞬间变为 Ready。

高阶实战:Claude Code 与 Beads 的”双人舞”

如果说上面的命令是基本舞步,那么当 Claude Code 遇上 Beads,它们能跳出怎样的双人舞?让我们看一个“任务中断与恢复”的真实场景。

0. 前置配置:教会 Claude 使用工具

要让 Claude Code 懂得使用 Beads,我们首先需要在项目的根目录下创建一个CLAUDE.md 文件(其它Coding agent一般支持AGENTS.md)。这是 Claude Code 的”行动指南”。

创建beads-demo目录,使用git init初始化该目录:

$mkdir beads-demo
$cd beads-demo
$git init .

执行bd init初始化该目录:

bd init
  Repository ID: 3f0bbad4
  Clone ID: c15058f43e4678f2
  ✓ Created AGENTS.md with landing-the-plane instructions

✓ bd initialized successfully!

  Backend: sqlite
  Database: .beads/beads.db
  Issue prefix: beads-demo
  Issues will be named: beads-demo-<hash> (e.g., beads-demo-a3f2dd)

Run bd quickstart to get started.

⚠ Setup incomplete. Some issues were detected:
  • Git Hooks: Missing 1 recommended hook(s)
  • Sync Divergence: 1 sync divergence issue(s) detected
  • Claude Integration: Not configured
  • Git Working Tree: Uncommitted changes present
  • Git Upstream: No upstream configured for master
  • Version Tracking: Version tracking not initialized

Run bd doctor --fix to see details and fix these issues.

beads创建了AGENTS.md文件,我们将之改名为CLAUDE.md,并写入下面这行文字:

$mv AGENTS.md CLAUDE.md
$echo "Use 'bd' for task tracking" >> CLAUDE.md

有了CLAUDE.md 这份”说明书”,Claude Code 就会在启动时自动扫描并理解这套规则。

1. 记忆的建立

场景:你让 Claude Code (在plan模式下,disable一些不必要的插件或skill)使用 Go 开发一个”简单的并发下载器”。它会创建了任务,并建立任务间的依赖关系:

● Bash(bd create --type feature --title "项目初始化" --description "初始化Go项目结构和基础配置…)
  ⎿  ✓ Created issue: beads-demo-dee
       Title: 项目初始化
       Priority: P2
       Status: open                                                                                                               

● Bash(bd create --type feature --title "核心下载逻辑实现" --description "实现下载器的核心功能…)
  ⎿  ✓ Created issue: beads-demo-9uq
       Title: 核心下载逻辑实现
       Priority: P2
       Status: open                                                                                                               

● Bash(bd create --type feature --title "进度显示实现" --description "实现终端进度条功能…)
  ⎿  ✓ Created issue: beads-demo-8pf
       Title: 进度显示实现
       Priority: P2
       Status: open                                                                                                               

● Bash(bd create --type feature --title "断点续传实现" --description "实现状态持久化和恢复功能…)
  ⎿  ✓ Created issue: beads-demo-fhv
       Title: 断点续传实现
       Priority: P2
       Status: open                                                                                                               

● Bash(bd create --type feature --title "CLI入口实现" --description "实现命令行工具入口…)
  ⎿  ✓ Created issue: beads-demo-4bb
       Title: CLI入口实现
       Priority: P2
       Status: open 

... ...

● 现在设置依赖关系:                                                                                     09:24 AM  glm-4.7        

● Bash(bd dep add beads-demo-9uq --blocked-by beads-demo-dee)
  ⎿  ✓ Added dependency: beads-demo-9uq depends on beads-demo-dee (blocks)                                                        

● Bash(bd dep add beads-demo-8pf --blocked-by beads-demo-9uq)
  ⎿  ✓ Added dependency: beads-demo-8pf depends on beads-demo-9uq (blocks)                                                        

● Bash(bd dep add beads-demo-fhv --blocked-by beads-demo-9uq)
  ⎿  ✓ Added dependency: beads-demo-fhv depends on beads-demo-9uq (blocks)                                                        

● Bash(bd dep add beads-demo-4bb --blocked-by beads-demo-9uq --blocked-by beads-demo-8pf --blocked-by beads-demo-fhv)
  ⎿  ✓ Added dependency: beads-demo-4bb depends on beads-demo-fhv (blocks)                                                        

● Bash(bd dep add beads-demo-4id --blocked-by beads-demo-4bb)
  ⎿  ✓ Added dependency: beads-demo-4id depends on beads-demo-4bb (blocks)                                                        

● Bash(bd list)
  ⎿  ○ beads-demo-4id [● P2] [task] - 编写测试
     ○ beads-demo-4bb [● P2] [feature] - CLI入口实现
     ○ beads-demo-fhv [● P2] [feature] - 断点续传实现
     ○ beads-demo-8pf [● P2] [feature] - 进度显示实现
     ○ beads-demo-9uq [● P2] [feature] - 核心下载逻辑实现
     ○ beads-demo-dee [● P2] [feature] - 项目初始化                                                                               

● Bash(bd graph --all)
  ⎿  Dependency graph for beads-demo-4id:                                                                                      

       Status: ○ open  ◐ in_progress  ● blocked  ✓ closed                                                                         

       Layer 0 (ready)
       ┌────────────────────────────┐
       │ ○ 项目初始化                    │
       │ beads-demo-dee             │
       │ blocks:1                   │
       └────────────────────────────┘
           │
           ▼                                                                                                                      

... ...
∴ Thinking…                                                                                                                       

  现在bd issues已经创建完成,依赖关系也设置好了。我可以看到:- Layer 0: 项目初始化 (ready)
  - Layer 1: 核心下载逻辑实现
  - Layer 2: 进度显示实现、断点续传实现
  - Layer 3: CLI入口实现
  - Layer 4: 编写测试                                                                                                             

  现在我需要调用ExitPlanMode来请求用户批准计划。                                                                                  

● 完美!bd issues已创建完成,依赖关系如下:                                                              09:25 AM  glm-4.7        

  Layer 0 (ready):     项目初始化
             │
             ▼
  Layer 1:            核心下载逻辑实现
             │
             ├──────────────┐
             ▼              ▼
  Layer 2:      进度显示实现    断点续传实现
             │              │
             └──────┬───────┘
                    ▼
  Layer 3:            CLI入口实现
                    │
                    ▼
  Layer 4:            编写测试                                                                                                    

  现在退出计划模式,准备开始实现:

之后让Claude Code基于bd中的任务逐个进行实现即可。

❯ 使用bd获取要完成的任务并实现任务。                                                                                              

● Bash(bd ready)
  ⎿  Ready work (1 issues with no blockers):                                                                                   

     1. [● P2] [feature] beads-demo-dee: 项目初始化                                                                               

● Bash(bd list)
  ⎿  ○ beads-demo-4id [● P2] [task] - 编写测试
     ○ beads-demo-4bb [● P2] [feature] - CLI入口实现
     ○ beads-demo-fhv [● P2] [feature] - 断点续传实现
     … +3 lines (ctrl+o to expand)                                                                                                

● Bash(bd show beads-demo-dee)
  ⎿  ○ beads-demo-dee · 项目初始化   [● P2 · OPEN]
     Owner: Tony Bai · Type: feature
     Created: 2026-01-27 · Updated: 2026-01-27
     … +11 lines (ctrl+o to expand)                                                                                               

● Bash(bd update beads-demo-dee --status in_progress)
  ⎿  ✓ Updated issue: beads-demo-dee                                                                                              

● 开始执行项目初始化:                                                                                                            

● Bash(mkdir -p cmd/downloader pkg/downloader)
  ⎿  (No content)                                                                                                                 

● Bash(go mod init github.com/tonybai/beads-demo)
  ⎿  go: creating new go.mod: module github.com/tonybai/beads-demo
     go: to add module requirements and sums:
        go mod tidy                                                                                                               

● Write(README.md)
  ⎿  Wrote 43 lines to README.md
       1 # beads-downloader
       2
       3 A simple concurrent download tool written in Go.
       4
       5 ## Features
       6
       7 - Concurrent chunked download with automatic HTTP Range detection
       8 - Resumable downloads (breakpoint resume)
       9 - Progress bar display
      10 - Graceful exit on Ctrl-C
     … +33 lines (ctrl+o to expand)                                                                                               

● Bash(ls -la && find . -type f -name "*.md" -o -name "go.mod" | head -10)
  ⎿  total 44
     drwxr-xr-x 7 root root 4096 Jan 27 09:34 .
     drwxr-xr-x 3 root root 4096 Jan 27 09:10 ..
     … +14 lines (ctrl+o to expand)                                                                                               

● Bash(bd close beads-demo-dee)
  ⎿  ✓ Closed beads-demo-dee: Closed                                                                                              

如果中途退出再继续会话,可以告诉Claude Code基于bd中的任务继续进行实现即可:

❯ 使用bd获取未完成的任务,继续完成                                                                                                

● 我来使用 bd 获取未完成的任务。                                                                                                  

● Bash(bd ready)
  ⎿  Ready work (2 issues with no blockers):                                                                                   

     1. [● P2] [feature] beads-demo-8pf: 进度显示实现
     2. [● P2] [feature] beads-demo-fhv: 断点续传实现                                                                             

● 有2个未完成的任务。让我查看详细信息:                                                                                           

● Bash(bd show beads-demo-8pf)
  ⎿  ○ beads-demo-8pf · 进度显示实现   [● P2 · OPEN]
     Owner: Tony Bai · Type: feature
     Created: 2026-01-27 · Updated: 2026-01-27
     … +11 lines (ctrl+o to expand)                                                                                               

● Bash(bd show beads-demo-fhv)
  ⎿  Running…

这就是 Beads 的魔力:它将 AI 的短期记忆(Context Window)转化为项目的长期记忆(Git Database),让 AI 能够像人类一样,跨越时间,有条不紊地推进复杂工程。

小结与展望

Beads 不仅仅是一个工具,它代表了一种“任务即代码” (Tasks as Code) 的新范式。

在 Gas Town 的宏大构想中,未来的软件开发将是由无数个 AI Agent 协作完成的。而 Beads,正是连接这些 Agent 的神经网络。它让任务的状态、依赖和历史,像代码一样被版本控制、被分发、被协同。

对于正在构建 AI Coding Agent 的开发者来说,集成 Beads 或许是让你的 Agent 拥有”长期记忆”和”战略规划能力”的最短路径。

项目地址github.com/steveyegge/beads

附录

为了便于开发者查看当前beads中的issue状态,社区开源了多款图形化的Beads viewer工具,包括网页版的beads-ui、终端TUI版的beads_viewer等。

这里以TUI版的beads_viewer为例,简单看看这些viewer的用法。

安装beads_viewer:

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/main/install.sh?$(date +%s)" | bash
==> Installing bv...
==> Detected platform: linux_amd64
==> Checking for pre-built binary...
==> Latest release: v0.13.0
==> Selected asset: bv_0.13.0_linux_amd64.tar.gz
==> Downloading https://github.com/Dicklesworthstone/beads_viewer/releases/download/v0.13.0/bv_0.13.0_linux_amd64.tar.gz...
==> Extracting...
==> Installed bv v0.13.0 to /root/.local/bin/bv
==> Run 'bv' in any beads project to view issues.

Tip: You can also install via Homebrew:
  brew install dicklesworthstone/tap/bv

使用beads_viewer查看beads中的issue列表和状态:

进入beads-demo目录,执行bv命令即可,你将看到类似下面的输出:

从图中,我们可以看到issue列表、优先级、状态,以及处于选择状态下的issue详情(包括依赖图)。


你的 Agent 记忆法

Beads 用 Git 解决了 Agent 的“长期记忆”问题。你在使用 AI 编程时,是如何管理任务上下文的?是靠手动复制粘贴,还是有什么独门秘籍?你
觉得“任务即代码”这种理念会成为未来的主流吗?

欢迎在评论区分享你的工作流或对 Beads 的看法!让我们一起探索 AI 协作的最佳实践。

如果这篇文章为你打开了 AI 任务管理的新视界,别忘了点个【赞】和【在看】,并转发给你的极客朋友,邀请他们一起体验 Beads!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Go 性能诊断工具大变天?Race 检测有望进生产,Trace 秒开不是梦!

本文永久链接 – https://tonybai.com/2026/01/31/go-official-updates-race-detector-trace-ui-pprof

大家好,我是Tony Bai。

近期,Go Runtime 团队公开了一系列关于性能与诊断工具链的内部会议记录(2025年末至2026年初)。从中,我们可以看到从轻量级竞态检测的探索,到 Trace 工具的交互式革命,再到 pprof 接口的现代化重构,Go 团队正在酝酿一系列深远的变革。

今天,我们就来深度解码这些前沿动向,看看 Go 1.27 及未来版本可能带给我们什么惊喜。

竞态检测 (Race Detection):寻找“轻量级”圣杯

Go 的 Race Detector (-race) 虽然强大,但其昂贵的运行时开销(通常 10x 内存和 CPU)使其难以在生产环境中常驻。Go 团队正在探索打破这一僵局的两种新路径:

  1. 纯软件方案的突围:社区贡献者 (thepudds) 提出了一种新的软件检测思路,试图将开销降低到足以在某些生产场景下运行的程度。虽然目前还处于“推测”阶段,但这种无需重新编译、动态挂载的可能性极其诱人。
  2. 硬件辅助的回归:利用现代 CPU 的硬件特性(如 Intel PT 或 AMD LBR)来实现低开销的竞态检测。虽然这需要特定硬件支持,但其“内置于每个二进制文件”的潜力不容忽视。

未来的 Go 可能会提供分级的竞态检测能力——在 CI 中使用全量 -race,在生产中使用采样的轻量级检测。

Execution Trace:交互体验与可编程性的大升级

Go 的执行追踪 (Execution Trace) 是诊断复杂并发问题的神器,但其庞大的数据量和难以解析的格式一直是痛点。会议记录透露了几个令人振奋的改进:

新一代 Trace UI:即时响应

Michael Knyszek 展示了一个全新的 cmd/trace UI 实验。通过引入索引 API (Index API),新工具可以:

  • 瞬间打开:不再需要预先解析整个 GB 级别的 Trace 文件。
  • 按需切片:用户可以选择一个时间窗口(例如 1秒),工具只解析并加载该窗口内的数据。
  • 无损切片:除了跨越边界的任务和区域状态会有语义上的微调,数据几乎是无损的。

这意味着,Gopher 们终于可以告别打开 Trace 文件时漫长的等待进度条了。

Trace 的读写 API 化

社区正在推进 x/exp/trace 包的演进,不仅支持解析(Read),更要支持生成(Write)

  • 测试场景:可以手动构造 Trace 事件来测试分析工具。
  • 脱敏与过滤:可以编写工具读取 Trace,过滤掉敏感数据,然后写回一个新的 Trace 文件。

这将为构建第三方的 Trace 分析和可视化生态打开大门。

pprof 的现代化:告别全局变量

当前的 runtime/pprof 严重依赖全局变量(如 MemProfileRate),这在多租户或库代码中是一个巨大的痛点。

Nick 提出的 pprof.Recorder 提案旨在解决这个问题。它允许创建独立的 Recorder 实例来控制采样。会议中甚至讨论了一个激进的想法:
* 废弃全局配置:在未来的 Go 版本(如 1.27)中,通过编译器检查或 go vet,禁止直接修改 runtime.MemProfileRate,强制迁移到新的 API。
* 多采样率支持:虽然 pprof 格式本身不支持变采样率,但团队正在讨论如何优雅地处理多个 Recorder 设置不同采样率的冲突(通常是“最细粒度者胜出”)。

性能优化的深水区:NUMA 与分片计数器

除了工具链,Runtime 本身的性能优化也在向深水区迈进:

  • NUMA 优化:Michael Pratt 和 Michael Knyszek 正在致力于消除 GC 中的最后一批主要缓存未命中 (Cache Misses),这通常是跨 NUMA 节点内存访问造成的。这将显著提升 Go 在超大核心数服务器上的表现。
  • Sharded Counter (分片计数器):Carlos 正在开发一种高性能的分片计数器。在高并发场景下,单一的原子计数器会成为缓存一致性流量的热点。通过分片(类似 xsync 的实现),可以大幅降低 CPU 核心间的竞争。这也暗示了 Go 标准库或 Runtime 内部可能会引入更高效的并发原语。

小结:Go 1.27 的期待

虽然 Go 1.26 尚未正式发布(RC2 刚出),但 Go 团队的目光已经投向了更远的 1.27以及后续版本。

从会议记录中,我们看到一个清晰的趋势:Go 正在从“能用”向“好用”和“极致性能”进化。无论是让诊断工具更人性化,还是对 Runtime 底层进行微秒级的压榨,都显示出这门语言旺盛的生命力。

让我们拭目以待,看看这些实验性的想法,有多少能最终落地为我们手中的工具。


你的期待清单

官方画的这些“饼”,每一个都让人心动。在你看来,哪个功能的落地最能解决你当前的痛点?是生产环境的 Race 检测,还是丝滑的 Trace 分析?

欢迎在评论区投出你的一票!让我们一起期待 Go 工具链的进化。

如果这篇文章让你对 Go 的未来充满了信心,别忘了点个【赞】和【在看】,并转发给你的 Gopher 朋友,告诉他们好消息!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

当机器开始“剁手”:详解 Google UCP 与 Agentic Commerce 的架构革命

本文永久链接 – https://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution

大家好,我是Tony Bai。

想象一下,未来的某一天,你们公司的电商网站流量突然暴涨了 1000 倍。

但奇怪的是,后台数据显示 PageView(页面浏览量)几乎为零,热力图一片空白,也没有任何用户在点击你的促销弹窗。

这并不是遭受了 DDoS 攻击,而是你迎来了第一批“机器顾客”

我们正在从“人机交互”的电商时代,跨入“Agentic Commerce(智能体商业)”的新纪元。在这个时代,代替人类下单的,是运行在手机、云端或眼镜里的 AI Agent(智能体)

如果你是技术负责人,你可能会感到背脊发凉:

现有的这套为人类设计的、充满图片、广告和前端渲染的电商基建,对于“硅基生物”来说,效率低得令人发指。

为了迎接这场变革,Google 近期开源了 UCP (Universal Commerce Protocol),微软研究院在去年也前瞻性地发布了 Agentic Economy报告,Aqfer 提出了 AIO (AI Agent Optimization) 概念。

今天,我们就结合这三份重磅资料,从协议、基建、经济三个维度,深度剖析这场正在发生的架构革命。

第一性原理:为什么我们需要 Agentic Commerce?

根据微软研究院(Microsoft Research)的报告,Agentic Commerce 的爆发并非偶然,而是经济学第一性原理的必然推论。

传统的电商交易链路充满了“通信摩擦(Communication Frictions)”

  • 人类: 搜索 -> 筛选 -> 比价 -> 阅读评论 -> 填表 -> 支付。
  • 摩擦: 每一个环节都在消耗人类有限的注意力认知带宽

AI 智能体的出现,本质上是在消除这些摩擦

未来的购物模式将简化为:意图 -> 交易

用户只需说:“帮我买一个去日本旅游用的轻便行李箱,预算 500 元内,要耐摔的。”

接下来的搜索、比价、看评论、下单、支付,全部由 Assistant Agent(助理智能体) 和商家的 Service Agent(服务智能体) 在后台通过协议谈判完成。

这不仅是用户体验的升级,更是交易效率的指数级跃迁

技术基座:Google UCP 协议详解

然而,理想很丰满,现实很骨感。目前的 Agent 购物面临一个巨大的工程难题$N \times N$ 的集成灾难

每个商家都有自己的私有 API,Agent 不可能适配全天下所有的电商接口。

为了解决这个问题,Google 提出了 UCP (Universal Commerce Protocol,通用商业协议)

你可以把 UCP 理解为电商界的“USB 接口”。它定义了一套标准化的语言,让消费者智能体(Consumer Agent)和商家后端(Business Backend)能够直接对话。

UCP 的核心架构设计

  1. 标准化发现 (Discovery)
    类似于 robots.txt,商家只需在 .well-known/ucp 路径下发布一个 JSON 清单,声明:“我是卖花的,我支持搜索、加购和 Google Pay。” Agent 读到这个文件,就知道了交互规则。

  2. 原子化能力 (Capabilities)
    UCP 定义了一组标准的原语(Primitives),如 ProductDiscovery(商品发现)、Cart(购物车)、Checkout(结账)。这些原语是跨平台的,无论是 Amazon 还是独立站,语义都一样。

  3. 灵活的传输层 (Transport)
    UCP 不仅支持传统的 REST API,还原生支持 MCP (Model Context Protocol)
    这意味着,你的 UCP 服务可以直接作为一个 MCP Server 挂载到 Claude 或 Gemini 中,让大模型“天生”就具备操作你店铺的能力。

Agent 看到的不再是 HTML,而是干净的 JSON:

// UCP Checkout Response Example
{
  "ucp": {
    "version": "2026-01-11",
    "services": { "dev.ucp.shopping": { "version": "2026-01-11", "spec": "https://ucp.dev/specs/shopping", "rest": { "schema": "https://ucp.dev/services/shopping/openapi.json", "endpoint": "http://localhost:8182/" } } },
    "capabilities": [
      { "name": "dev.ucp.shopping.checkout", "version": "2026-01-11", "spec": "https://ucp.dev/specs/shopping/checkout", "schema": "https://ucp.dev/schemas/shopping/checkout.json" },
      { "name": "dev.ucp.shopping.discount", "version": "2026-01-11", "spec": "https://ucp.dev/specs/shopping/discount", "schema": "https://ucp.dev/schemas/shopping/discount.json", "extends": "dev.ucp.shopping.checkout" },
      { "name": "dev.ucp.shopping.fulfillment", "version": "2026-01-11", "spec": "https://ucp.dev/specs/shopping/fulfillment", "schema": "https://ucp.dev/schemas/shopping/fulfillment.json", "extends": "dev.ucp.shopping.checkout" }
    ]
  },
  "payment": {
    "handlers": [
      { "id": "shop_pay", "name": "com.shopify.shop_pay", "version": "2026-01-11", "spec": "https://shopify.dev/ucp/handlers/shop_pay", "config_schema": "https://shopify.dev/ucp/handlers/shop_pay/config.json", "instrument_schemas": [ "https://shopify.dev/ucp/handlers/shop_pay/instrument.json" ], "config": { "shop_id": "d124d01c-3386-4c58-bc58-671b705e19ff" } },
      { "id": "google_pay", "name": "google.pay", "version": "2026-01-11", "spec": "https://example.com/spec", "config_schema": "https://example.com/schema", "instrument_schemas": [  "https://ucp.dev/schemas/shopping/types/gpay_card_payment_instrument.json"
 ], "config": { "api_version": 2, "api_version_minor": 0, "merchant_info": { "merchant_name": "Flower Shop", "merchant_id": "TEST", "merchant_origin": "localhost" }, "allowed_payment_methods": [ { "type": "CARD", "parameters": { "allowedAuthMethods": [ "PAN_ONLY", "CRYPTOGRAM_3DS" ], "allowedCardNetworks": [ "VISA", "MASTERCARD" ] }, "tokenization_specification": [ { "type": "PAYMENT_GATEWAY", "parameters": [ { "gateway": "example", "gatewayMerchantId": "exampleGatewayMerchantId" } ] } ] } ] } },
      { "id": "mock_payment_handler", "name": "dev.ucp.mock_payment", "version": "2026-01-11", "spec": "https://ucp.dev/specs/mock", "config_schema": "https://ucp.dev/schemas/mock.json", "instrument_schemas": [ "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" ], "config": { "supported_tokens": [ "success_token", "fail_token" ] } }
    ]
  }
}

基础设施危机:“海啸级”查询与营销失效

当 Agent 能够读懂 UCP 协议后,商家的技术架构将面临前所未有的挑战。Aqfer 在其白皮书中发出了警告:你的基础设施准备好迎接“机器海啸”了吗?

流量的量级跃迁

人类逛淘宝,一分钟看 5 个商品就累了。

AI 智能体为了帮主人找到“最优解”,可能会在几毫秒内扫描 1000 个 SKU,实时比对全网价格和库存。

你的 Read API QPS 可能会暴涨 100倍 – 1000倍。传统的缓存策略可能失效,因为 Agent 需要毫秒级的实时库存(Real-time Inventory)准确性。

营销逻辑的崩塌

这是最让市场部绝望的一点:AI 智能体对“情绪”免疫。

你在详情页上精心设计的品牌故事、氛围感图片、促销倒计时,对于 LLM 来说只是无意义的 Token 噪音。

Agent 只关心:Data (数据)

  • 价格是多少?(精确数字)
  • 材质是什么?(结构化参数)
  • 物流几天到?(SLA 承诺)

从 SEO 到 AIO (AI Agent Optimization)

未来的流量入口不再是搜索引擎,而是 AI 智能体。

如果你想被 Agent 选中,你需要的不是 SEO(针对关键词优化),而是 AIO(针对智能体优化)

Data is the UI. 你的商品数据必须是清洁的、结构化的、向量友好的。如果你还在用图片存参数表,你的商品在 Agent 眼里就是隐形的。

未来推演:围墙花园 vs. 开放网络

微软研究院的报告指出了两种可能的终局:

  • 路径 A:Agentic Walled Gardens(智能体围墙花园)
    OpenAI、Google、Apple 建立自己的“智能体 App Store”。商家必须适配它们的私有协议才能被其 Agent 访问。这会形成新的垄断。

  • 路径 B:Web of Agents(智能体开放网络)
    基于 UCPMCP 这样的开放标准,任何商家的 Service Agent 都可以和消费者的 Assistant Agent 自由交易,无需经过中心化平台。

这就是为什么 Google 要急于开源 UCP标准协议。协议之争,将决定未来十年的互联网商业格局。

小结:为“机器客户”重构系统

Agentic Commerce 不仅仅是一个技术热词,它是一场生产关系的重构

作为架构师,你的使命正在发生变化:

从“为人类构建漂亮的 UI”,转变为“为机器构建健壮的 API”

不要等到你的竞争对手已经被 AI 智能体“自动下单”买空了库存,你还在研究 Landing Page 的按钮颜色。

拥抱协议,结构化数据,迎接那个“万物皆可被 Agent 调用”的未来。

参考资料

  • The Agentic Economy – https://arxiv.org/abs/2505.15799
  • Under the Hood: Universal Commerce Protocol (UCP) – https://developers.googleblog.com/under-the-hood-universal-commerce-protocol-ucp/
  • Universal Commerce Protocol官网 – https://ucp.dev/
  • The Age of Agentic Commerce: When Machines Become Your Customers – https://aqfer.com/wp-content/uploads/2025/09/AgenticCommerce_9.3.25_final_v1.pdf

你的“机器顾客”准备好了吗?

Agentic Commerce 的未来听起来既科幻又紧迫。如果你的应用突然迎来了一波 AI Agent 的访问,你的 API 扛得住吗?你认为未来的电商是会被巨头垄断,还是通过 UCP 走向开放?

欢迎在评论区分享你的脑洞或担忧! 让我们一起为即将到来的“机器时代”做好准备。

如果这篇文章让你对未来的电商架构有了全新的认识,别忘了点个【赞】和【在看】,并转发给你的产品经理和老板,告诉他们:变天了!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

从入门到极致:VictoriaMetrics 教你写出最高效的 Go 代码

本文永久链接 – https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code

大家好,我是Tony Bai。

InfluxDB 转Rust 之后,VictoriaMetrics 迅速崛起,成为了 Go 生态中无可争议的第一时序数据库。凭借其惊人的写入性能、极低的内存占用以及对 Prometheus 生态的完美兼容,它赢得了大量Go开发者以及大厂的青睐。除了核心数据库,其家族还拥有 VictoriaLogsVictoriaTraces 等明星产品,它们共同构成了一个高性能的可观测性平台。

很多 Gopher 都好奇:为什么用同样的语言,VictoriaMetrics 能跑得这么快、省这么多内存?

答案就藏在它的源码里。VictoriaMetrics 的代码库堪称一本活着的“Go 高性能编程教科书”。从基础的工程规范,到极致的内存复用,再到对并发模型的精细控制,每一行代码都是对性能的极致追求。

今天,我们就来完整拆解 VictoriaMetrics 的核心编程模式,带你体验一场从入门到极致的 Go 性能进阶之旅。


入门——务实的工程基石

在追求极致性能之前,首先要保证代码是稳健且可维护的。VictoriaMetrics 在基础工程实践上,展现了极其实用主义的智慧。

日志系统的“自我保护” (Rate Limiting)

很多系统挂掉,不是因为 bug,而是因为错误引发的“日志风暴”耗尽了磁盘 I/O。VictoriaMetrics 教我们的第一课是:日志也需要限流

它不仅支持INFO/WARN/ERROR/FATAL/PANIC五级日志,以及默认支持 JSON 格式输出,便于结构化日志采集:

// lib/logger/logger.go
var (
    loggerLevel    = flag.String("loggerLevel", "INFO", "Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC")
    loggerFormat   = flag.String("loggerFormat", "default", "Format for logs. Possible values: default, json")
)

更引入了关键的限流参数,防止日志风暴导致磁盘 IO 过载:

// lib/logger/logger.go
var (
    // 启动参数控制日志级别和限流阈值
    errorsPerSecondLimit = flag.Int("loggerErrorsPerSecondLimit", 0, "Per-second limit on the number of ERROR messages...")
    warnsPerSecondLimit  = flag.Int("loggerWarnsPerSecondLimit", 0, Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit)
)

在输出日志时,根据日志限流配置,对ERROR和WARN级别日志进行限制:

func logMessage(level, msg string, skipframes int) {
    ... ...
    // rate limit ERROR and WARN log messages with given limit.
    if level == "ERROR" || level == "WARN" {
        limit := uint64(*errorsPerSecondLimit)
        if level == "WARN" {
            limit = uint64(*warnsPerSecondLimit)
        }
        ok, suppressMessage := logLimiter.needSuppress(location, limit)
        if ok {
            return
        }
        if len(suppressMessage) > 0 {
            msg = suppressMessage + msg
        }
    }
    ... ...

在你的高并发服务中,给 Error 日志加上限流开关。虽然可能丢失部分细节,但它能保护你的系统不被日志拖垮。

配置管理:Flag 的艺术

VictoriaMetrics 并未使用第三方的flag包,而是大量使用标准库 flag 包,但用得非常智能。它为每个配置项提供了清晰文档和合理默认值,并支持通过 lib/envflag 内部包从环境变量覆盖配置。这种设计既简单又符合云原生部署需求:

// lib/envflag/envflag.go
var (
    // -envflag.enable: 启用从环境变量读取标志
    enable = flag.Bool("envflag.enable", false, "Whether to enable reading flags from environment variables in addition to the command line. "+
        "Command line flag values have priority over values from environment vars. "+
        "Flags are read only from the command line if this flag isn't set. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#environment-variables for more details")
    // -envflag.prefix: 环境变量前缀
    prefix = flag.String("envflag.prefix", "", "Prefix for environment variables if -envflag.enable is set")
)

// Parse parses environment vars and command-line flags.
//
// Flags set via command-line override flags set via environment vars.
//
// This function must be called instead of flag.Parse() before using any flags in the program.
func Parse() {
    ParseFlagSet(flag.CommandLine, os.Args[1:])
    applySecretFlags()
}

模块化与克制的抽象

打开源码目录,你会发现

VictoriaMetrics 将功能拆分为独立的 lib 包,每个包职责单一:

  • lib/storage: 核心存储引擎
  • lib/mergeset: 合并索引
  • lib/encoding: 数据编码
  • lib/bytesutil: 字节工具函数
  • lib/workingsetcache: 工作集缓存

在VictoriaMetrics代码中,你很少能看到层层嵌套的接口或复杂的依赖注入框架。 这种结构既保持了模块化,又避免了过度抽象带来的性能损耗,

对于 CPU 密集型应用,函数调用的层级越少越好。简单、直接的代码不仅易于阅读,对编译器优化(如内联)也更友好。


进阶——内存管理的艺术

对于数据库而言,内存就是生命线。VictoriaMetrics 在内存管理上的造诣,是其高性能的核心秘诀之一。

sync.Pool 的高效对象复用模式

Go 的 GC 在处理海量小对象时会面临巨大压力。VictoriaMetrics 的策略是:能复用,绝不分配。 VictoriaMetrics 大量使用 sync.Pool 来复用对象,减少 GC 压力。它不仅复用简单的结构体,甚至复用复杂的切片对象,比如下面这段复用切片对象的代码。

// lib/encoding/int.go
var uint64sPool sync.Pool

// Uint64s holds an uint64 slice
type Uint64s struct {
    A []uint64
}

// GetUint64s returns an uint64 slice with the given size.
// The slice contents isn't initialized - it may contain garbage.
func GetUint64s(size int) *Uint64s {
    v := uint64sPool.Get()
    if v == nil {
        return &Uint64s{
            A: make([]uint64, size),
        }
    }
    is := v.(*Uint64s)
    // 关键技巧:复用底层数组,仅调整切片长度
    // 避免了重新 make([]uint64) 的开销
    is.A = slicesutil.SetLength(is.A, size)
    return is
}

// PutUint64s returns is to the pool.
func PutUint64s(is *Uint64s) {
    uint64sPool.Put(is)
}

这里用到了 slicesutil.SetLength,通过切片操作复用底层数组,避免了重新分配内存:

// lib/slicesutil/slicesutil.go

// SetLength sets len(a) to newLen and returns the result.
//
// It may allocate new slice if cap(a) is smaller than newLen.
func SetLength[T any](a []T, newLen int) []T {
    if n := newLen - cap(a); n > 0 {
        a = append(a[:cap(a)], make([]T, n)...)
    }
    return a[:newLen]
}

突破 sync.Pool 的限制:Channel 对象池

sync.Pool 虽好,但它有两个缺点:它是 per-CPU 的,且在 GC 时会被清空。对于极大的对象(如超过 64KB 的缓冲区),这可能导致内存使用量的不可控膨胀。

VictoriaMetrics 教你一招:用 Channel 当对象池,比 sync.Pool 更可控。

// lib/storage/inmemory_part.go

// inmemoryPart represents in-memory partition.
type inmemoryPart struct {
    ph partHeader

    timestampsData chunkedbuffer.Buffer
    valuesData     chunkedbuffer.Buffer
    indexData      chunkedbuffer.Buffer
    metaindexData  chunkedbuffer.Buffer

    creationTime uint64
}

// 容量严格限制为 CPU 核数,防止内存无限膨胀
// Use chan instead of sync.Pool in order to reduce memory usage on systems with big number of CPU cores,
// since sync.Pool maintains per-CPU pool of inmemoryPart objects.
//
// The inmemoryPart object size can exceed 64KB, so it is better to use chan instead of sync.Pool for reducing memory usage.
var mpPool = make(chan *inmemoryPart, cgroup.AvailableCPUs())

func getInmemoryPart() *inmemoryPart {
    select {
    case mp := <-mpPool: // 尝试从池中获取
        return mp
    default:
        return &inmemoryPart{} // 池空了,才新建
    }
}

func putInmemoryPart(mp *inmemoryPart) {
    mp.Reset()
    select {
    case mpPool <- mp: // 尝试归还
    default:
        // Drop mp in order to reduce memory usage.
        // 池满了,直接丢弃,等待 GC 回收
    }
}

VictoriaMetrics认为:当你需要严格控制大对象的总数量时,带缓冲的 Channel 是比 sync.Pool 更安全的选择。

切片复用的极致:[:0] 技巧

在处理数据流时,VictoriaMetrics 几乎从不通过 make 创建新切片,而是疯狂复用缓冲区。最常用的模式就是 buf = buf[:0],该模式清空切片但保留和重用底层数组,避免重新分配新切片(包括底层数组):

// lib/mergeset/encoding.go

func (ib *inmemoryBlock) updateCommonPrefixSorted() {
    items := ib.items
    if len(items) <= 1 {
        // There is no sense in duplicating a single item or zero items into commonPrefix,
        // since this only can increase blockHeader size without any benefits.
        ib.commonPrefix = ib.commonPrefix[:0]   // 重置切片长度为 0,但保留底层容量 (capacity)
        return
    }

    data := ib.data
    cp := items[0].Bytes(data)
    cpLen := commonPrefixLen(cp, items[len(items)-1].Bytes(data))
    cp = cp[:cpLen]
    ib.commonPrefix = append(ib.commonPrefix[:0], cp...) // append 操作会直接利用底层数组,无内存分配
}

智能的缓冲区分配策略

并不总是越大越好。VictoriaMetrics 实现了三种精细的缓冲区调整策略 (lib/bytesutil):

  1. ResizeWithCopyMayOverallocate:按 2 的幂次增长(减少未来扩容次数,空间换时间)。
  2. ResizeWithCopyNoOverallocate:精确分配(节省内存,时间换空间)。
  3. ResizeNoCopy…:扩容但不拷贝旧数据(用于完全覆盖写入场景,最快)。

过度分配可节省 CPU 但浪费内存;精确分配节省内存但可能频繁扩容,究竟使用哪种调整策略,需要根据实际情况权衡。


高级——并发与锁的智慧

面对高并发,如何让多核 CPU 跑满而不互相打架?

分片锁 (Sharding):化整为零

这是解决锁竞争的“银弹”。VictoriaMetrics 将大的数据结构拆分为多个分片,每个分片有独立的锁。

// lib/storage/partition.go

// The number of shards for rawRow entries per partition.
//
// Higher number of shards reduces CPU contention and increases the max bandwidth on multi-core systems.
// 1. 根据 CPU 核数决定分片数量
var rawRowsShardsPerPartition = cgroup.AvailableCPUs()

type rawRowsShards struct {
    flushDeadlineMs atomic.Int64

    shardIdx atomic.Uint32

    // Shards reduce lock contention when adding rows on multi-CPU systems.
    // 2. 创建一组分片,每个分片有独立的锁
    shards []rawRowsShard

    rowssToFlushLock sync.Mutex
    rowssToFlush     [][]rawRow
}

func (rrss *rawRowsShards) addRows(pt *partition, rows []rawRow) {
    shards := rrss.shards
    shardsLen := uint32(len(shards))
    for len(rows) > 0 {
        n := rrss.shardIdx.Add(1)
        idx := n % shardsLen
        tailRows, rowsToFlush := shards[idx].addRows(rows) // 在分片中添加row
        rrss.addRowsToFlush(pt, rowsToFlush)
        rows = tailRows
    }
}

func (rrs *rawRowsShard) addRows(rows []rawRow) ([]rawRow, []rawRow) {
    var rowsToFlush []rawRow

    rrs.mu.Lock() // 只锁定这一个分片,其他分片仍可并发写入
    if cap(rrs.rows) == 0 {
        rrs.rows = newRawRows()
    }
    if len(rrs.rows) == 0 {
        rrs.updateFlushDeadline()
    }
    n := copy(rrs.rows[len(rrs.rows):cap(rrs.rows)], rows)
    rrs.rows = rrs.rows[:len(rrs.rows)+n]
    rows = rows[n:]
    if len(rows) > 0 {
        rowsToFlush = rrs.rows
        rrs.rows = newRawRows()
        rrs.updateFlushDeadline()
        n = copy(rrs.rows[:cap(rrs.rows)], rows)
        rrs.rows = rrs.rows[:n]
        rows = rows[n:]
    }
    rrs.mu.Unlock() // 解除分片锁

    return rows, rowsToFlush
}

原子操作:无锁编程

对于简单的计数器和状态标志操作这种简单逻辑,VictoriaMetrics 大量使用 atomic 包替代 Mutex。在 Bloom Filter (lib/bloomfilter/filter.go) 中,它更是使用 atomic.LoadUint64 和 atomic.CompareAndSwapUint64 (CAS) 来实现无锁并发位设置,性能比互斥锁快 10-100 倍。

// lib/bloomfilter/filter.go
func (f *filter) Has(h uint64) bool {
    bits := f.bits
    maxBits := uint64(len(bits)) * 64
    bp := (*[8]byte)(unsafe.Pointer(&h))
    b := bp[:]
    for i := 0; i < hashesCount; i++ {
        hi := xxhash.Sum64(b)
        h++
        idx := hi % maxBits
        i := idx / 64
        j := idx % 64
        mask := uint64(1) << j
        w := atomic.LoadUint64(&bits[i])
        if (w & mask) == 0 {
            return false
        }
    }
    return true
}

func (f *filter) Add(h uint64) bool {
    bits := f.bits
    maxBits := uint64(len(bits)) * 64
    bp := (*[8]byte)(unsafe.Pointer(&h))
    b := bp[:]
    isNew := false
    for i := 0; i < hashesCount; i++ {
        hi := xxhash.Sum64(b)
        h++
        idx := hi % maxBits
        i := idx / 64
        j := idx % 64
        mask := uint64(1) << j
        w := atomic.LoadUint64(&bits[i])
        for (w & mask) == 0 {
            wNew := w | mask
            // The wNew != w most of the time, so there is no need in using atomic.LoadUint64
            // in front of atomic.CompareAndSwapUint64 in order to try avoiding slow inter-CPU synchronization.
            if atomic.CompareAndSwapUint64(&bits[i], w, wNew) {
                isNew = true
                break
            }
            w = atomic.LoadUint64(&bits[i])
        }
    }
    return isNew
}

本地化 Worker Pool:消除 CPU 间通信

通用的 Worker Pool 有一个全局任务队列,这会导致多个 CPU 核心竞争同一个锁,且任务在不同核心间切换会带来缓存失效。

VictoriaMetrics 实现了一种本地化优先的 Worker Pool:每个 Worker 优先处理分配给自己的任务(通过独立的 Channel),只有在空闲时才去“帮助”其他 Worker。这种设计极大提升了多核系统的可扩展性。

// app/vmselect/netstorage/netstorage.go

// MaxWorkers returns the maximum number of concurrent goroutines, which can be used by RunParallel()
func MaxWorkers() int {
    n := *maxWorkersPerQuery
    if n <= 0 {
        return defaultMaxWorkersPerQuery
    }
    if n > gomaxprocs {
        // There is no sense in running more than gomaxprocs CPU-bound concurrent workers,
        // since this may worsen the query performance.
        n = gomaxprocs
    }
    return n
}

var gomaxprocs = cgroup.AvailableCPUs()

// 根据 CPU 核数动态决定 worker 数量(最多 32 个)
var defaultMaxWorkersPerQuery = func() int {
    // maxWorkersLimit is the maximum number of CPU cores, which can be used in parallel
    // for processing an average query, without significant impact on inter-CPU communications.
    const maxWorkersLimit = 32

    n := min(gomaxprocs, maxWorkersLimit)
    return n
}()

func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, workerID uint) error) (int, error) {
    tswsLen := len(rss.packedTimeseries)
    if tswsLen == 0 {
        // Nothing to process
        return 0, nil
    }

    var mustStop atomic.Bool
    initTimeseriesWork := func(tsw *timeseriesWork, pts *packedTimeseries) {
        tsw.rss = rss
        tsw.pts = pts
        tsw.f = f
        tsw.mustStop = &mustStop
    }
    maxWorkers := MaxWorkers()
    if maxWorkers == 1 || tswsLen == 1 {
        // It is faster to process time series in the current goroutine.
        var tsw timeseriesWork
        tmpResult := getTmpResult()
        rowsProcessedTotal := 0
        var err error
        for i := range rss.packedTimeseries {
            initTimeseriesWork(&tsw, &rss.packedTimeseries[i])
            err = tsw.do(&tmpResult.rs, 0)
            rowsReadPerSeries.Update(float64(tsw.rowsProcessed))
            rowsProcessedTotal += tsw.rowsProcessed
            if err != nil {
                break
            }
        }
        putTmpResult(tmpResult)

        return rowsProcessedTotal, err
    }

    // Slow path - spin up multiple local workers for parallel data processing.
    // Do not use global workers pool, since it increases inter-CPU memory ping-pong,
    // which reduces the scalability on systems with many CPU cores.

    // Prepare the work for workers.
    tsws := make([]timeseriesWork, len(rss.packedTimeseries))
    for i := range rss.packedTimeseries {
        initTimeseriesWork(&tsws[i], &rss.packedTimeseries[i])
    }

    // Prepare worker channels.
    workers := min(len(tsws), maxWorkers)
    itemsPerWorker := (len(tsws) + workers - 1) / workers
    // 为每个 Worker 创建独立的 Channel
    workChs := make([]chan *timeseriesWork, workers)
    for i := range workChs {
        workChs[i] = make(chan *timeseriesWork, itemsPerWorker)
    }

    // Spread work among workers.
    for i := range tsws {
        idx := i % len(workChs)
        workChs[idx] <- &tsws[i]
    }
    // Mark worker channels as closed.
    for _, workCh := range workChs {
        close(workCh)
    }

    // Start workers and wait until they finish the work.
    var wg sync.WaitGroup
    for i := range workChs {
        wg.Add(1)
        qtChild := qt.NewChild("worker #%d", i)
        go func(workerID uint) {
            // Worker 优先处理自己 Channel 中的任务
            timeseriesWorker(qtChild, workChs, workerID)
            qtChild.Done()
            wg.Done()
        }(uint(i))
    }
    wg.Wait()

    // Collect results.
    var firstErr error
    rowsProcessedTotal := 0
    for i := range tsws {
        tsw := &tsws[i]
        if tsw.err != nil && firstErr == nil {
            // Return just the first error, since other errors are likely duplicate the first error.
            firstErr = tsw.err
        }
        rowsReadPerSeries.Update(float64(tsw.rowsProcessed))
        rowsProcessedTotal += tsw.rowsProcessed
    }
    return rowsProcessedTotal, firstErr
}

并发度控制:Channel 作为信号量进行限流

为了防止内存溢出,必须严格限制并发处理的数据块数量。VictoriaMetrics 使用带缓冲 Channel 作为信号量来实现限流。

// lib/mergeset/table.go

// Table represents mergeset table.
type Table struct {
    ... ...
    // inmemoryPartsLimitCh limits the number of inmemory parts to maxInmemoryParts
    // in order to prevent from data ingestion slowdown as described at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5212
    inmemoryPartsLimitCh chan struct{}
    ... ...
}

func (tb *Table) addToInmemoryParts(pw *partWrapper, isFinal bool) {
    // Wait until the number of in-memory parts goes below maxInmemoryParts.
    // This prevents from excess CPU usage during search in tb under high ingestion rate to tb.
    // See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5212
    select {
    case tb.inmemoryPartsLimitCh <- struct{}{}:
    default:
        tb.inmemoryPartsLimitReachedCount.Add(1)
        select {
        case tb.inmemoryPartsLimitCh <- struct{}{}: // 满则阻塞等待
        case <-tb.stopCh:
        }
    }
    ... ...
}

专家——黑魔法与算法优化

当常规手段用尽,VictoriaMetrics 开始使用一些“非常规”武器。

Unsafe 的零拷贝技巧

Go 的 string 和 []byte 转换通常涉及内存拷贝。在热点路径上,VictoriaMetrics 使用 unsafe 绕过。

// lib/bytesutil/bytesutil.go
// 零拷贝:[]byte -> string
func ToUnsafeString(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}

// 零拷贝:string -> []byte
func ToUnsafeBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

此外,它还使用 unsafe.Add 进行直接指针运算来获取子切片,以及直接将 uint64 转为字节数组指针进行哈希计算,这些都可以在热路径上减少了边界检查和内存分配。

警告:这是一把双刃剑。你必须确保原始数据在生命周期内有效且不可变,否则会导致严重的逻辑错误甚至 Panic。

汇编优化与算法选择

VictoriaMetrics 本身并不手写汇编,但它极其善于利用经过汇编优化的第三方库(如 xxhash, zstd)。

更重要的是,它针对时序数据特点,发明了 Nearest Delta 编码(最近邻 Delta 编码)。它不仅存储数值的“差值(delta)”,还通过位运算移除不必要的精度和末尾的零。

它还支持策略自适应,会智能判断数据类型(Gauge vs Counter),选择不同编码。甚至在压缩效果不佳时自动回退到存储原始数据,确保在 CPU 和存储空间之间取得最佳平衡。

内存布局优化:公共前缀提取

在索引存储中,有序数据的 Key 往往有很长的公共前缀。VictoriaMetrics 会自动提取首尾元素的公共前缀,只存储差异部分。这不仅减少了内存占用,更提高了 CPU 缓存的命中率。


小结:Gopher 的修行之路

通过完整剖析 VictoriaMetrics 的源码,我们看到了一条清晰的性能进阶之路:

  1. 入门:编写简单、直接、模块化的代码,利用 Flag 和日志限流构建稳健系统。
  2. 进阶:精通内存复用,灵活运用 sync.Pool 和 Channel 对象池,将 GC 压力降至最低。
  3. 高级:深刻理解并发,利用分片锁、原子操作和本地化队列,压榨多核 CPU 的极限。
  4. 极致:在热点路径上,敢于使用 unsafe 和自定义算法,通过对数据特征的深刻理解换取最后的性能提升。

性能优化没有黑魔法,只有对原理的深刻理解和对细节的极致打磨。 希望 VictoriaMetrics 的这些实战技巧,能帮助你在 Go 语言的修行之路上,更上一层楼。


你的性能优化“必杀技”

VictoriaMetrics 的代码确实让人叹为观止。在你的 Go 开发生涯中,有没有哪一个性能优化技巧(比如 sync.Pool 或 unsafe)让你印象最深刻,或者真的帮了大忙?

欢迎在评论区分享你的“优化故事”! 让我们一起挖掘更多 Go 语言的性能宝藏。

如果这篇文章让你对 Go 高性能编程有了新的领悟,别忘了点个【赞】和【在看】,并转发给你的团队,好代码值得被更多人看到!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

聚焦 Web 性能指标 TTI

如果你经常做网站优化,可能会陷入一个性能指标的泥潭即「面向指标优化」。真正的用户体验从来不是指标决定,相反它应该最真实的反映用户行为。

所以本节我们就来研究 TTI(Time to Interactive),话题展开之前,我们先来了解一些背景知识。

一、RAIL 模型

RAIL 是一种以用户为中心的性能模型。每个网络应用均具有与其生命周期有关的四个不同方面,且这些方面以不同的方式影响着性能:
RAIL 性能模型

1.响应:输入延迟时间(从点按到绘制)小于 100 毫秒。
用户点按按钮(例如打开导航)。

2.动画:每个帧的工作(从 JS 到绘制)完成时间小于 16 毫秒。
用户滚动页面,拖动手指(例如,打开菜单)或看到动画。 拖动时,应用的响应与手指位置有关(例如,拉动刷新、滑动轮播)。 此指标仅适用于拖动的持续阶段,不适用于开始阶段。

3.空闲:主线程 JS 工作分成不大于 50 毫秒的块。
用户没有与页面交互,但主线程应足够用于处理下一个用户输入。

4.加载:页面可以在 1000 毫秒内就绪。
用户加载页面并看到关键路径内容。

如果要提升网站用户体验,RAIL 是个不错的评估模型。

二、解读 TTI(页面可交互时间)

TTI 指的是应用既在视觉上都已渲染出了,可以响应用户的输入了。要了解 TTI,我们需要知道它的计算规则,先来看下面这张图:
TTI

官方文档中找到了如下描述:
First Idle is the first early sign of time where the main thread has come at rest and the browser has completed a First Meaningful Paint.

Time to Interactive is after First Meaningful Paint. The browser’s main thread has been at rest for at least 5 seconds and there are no long tasks that will prevent immediate response to user input.

我们可以简单的理解一下:
1.First Idle 是主线程处于静止状态且浏览器已完成 First Meanfulful Paint 的第一个早期迹象;
2.TTIFMP 之后,浏览器主线程静止至少 5s,并且没有可以阻断用户交互响应的「长任务」。

如果你对 FMP 还不了解,不妨先看看这篇文章:网站性能指标 - FMP。除此之外,第二条中提到的「长任务」又是什么呢?

三、Long Task(长任务)

对于「长任务」,我们通过如下图示说明:
长任务

对于用户而言,任务耗时较长表现为滞后或卡顿,而这也是目前网页不良体验的主要根源。

如何测量 Long Task

1
2
3
4
5
6
7
8
9
// Jartto's Demo
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// TODO...
console.log(entry);
}
});

observer.observe({entryTypes: ['longtask']});

控制台输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "self",
"entryType": "longtask",
"startTime": 315009.59500001045,
"duration": 99.9899999878835,
"attribution": [
{
"name": "unknown",
"entryType": "taskattribution",
"startTime": 0,
"duration": 0,
"containerType": "window",
"containerSrc": "",
"containerId": "",
"containerName": ""
}
]
}

Long Tasks API 可以将任何耗时超过 50 毫秒的任务标示为可能存在问题,并向应用开发者显示这些任务。 选择 50 毫秒的时间是为了让应用满足在 100 毫秒内响应用户输入的 RAIL 指导原则。

实际开发过程中,我们可以通过一个 hack 来检查页面中「长任务」的代码:

1
2
3
4
5
6
7
8
9
10
11
// detect long tasks hack
(function detectLongFrame() {
let lastFrameTime = Date.now();
requestAnimationFrame(function() {
let currentFrameTime = Date.now();
if (currentFrameTime - lastFrameTime > 50) {
// Report long frame here...
}
detectLongFrame(currentFrameTime);
});
}());

四、如何计算 TTI?

在计算之前,我们先来看一下 Timing API
Timing API

Google 官方文档中有一段描述:
Note: Lower Bounding FirstInteractive at DOMContentLoadedEndDOMContentLoadedEnd is the point where all the DOMContentLoaded listeners finish executing. It is very rare for critical event listeners of a webpage to be installed before this point. Some of the firstInteractive definitions we experimented with fired too early for a small number of sites, because the definitions only looked at long tasks and network activity (and not at, say, how many event listeners are installed), and sometimes when there are no long tasks in the first 5-10 seconds of loading we fire FirstInteractive at FMP, when the sites are often not ready yet to handle user inputs. We found that if we take max(DOMContentLoadedEnd, firstInteractive) as the final firstInteractive value, the values returned to reasonable region. Waiting for DOMContentLoadedEnd to declare FirstInteractive is sensible, so all the definitions introduced below lower bound firstInteractive at DOMContentLoadedEnd.

所以,我们可以通过 domContentLoadedEventEnd 来粗略的进行估算:

1
2
// 页面可交互时间 
TTI: domContentLoadedEventEnd - navigationStart,

domContentLoadedEventEnd:文档的 DOMContentLoaded 事件的结束时间。

The domContentLoadedEventEnd attribute MUST return a DOMHighResTimeStamp with a time value equal to the time immediately after the current document's DOMContentLoaded event completes.

如果你觉得上述计算过于复杂,可以通过 Google 实验室提供的 Polyfill 来获取。

五、TTI 指标监控

我们可以通过 Google TTI Polyfill来对 TTI 进行监测。
1.安装

1
npm install tti-polyfill

2.使用

1
2
3
4
import ttiPolyfill from './path/to/tti-polyfill.js';
ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
// Use `tti` value in some way.
});

很简单,就不细说了。推荐几篇 TTI 相关文章:
First Interactive and Consistently Interactive
User-centric performance metrics
Focusing on the Human-Centric Metrics

文章最后,打个小广告吧。如果你想搭上在线教育的快车,快速成长,不妨加入我们。一起成长,一起学习,一起挑战更多有趣的事情,「跟谁学-高途课堂」欢迎你,请将简历私我~

🔲 ☆

【直观算法】二叉搜索树算法总结

【阅读时间】
【阅读内容】结合Leetcode相关算法题总结二叉搜索树的相关算法,包括基本的二叉搜索构建和应用,附带一些关于AVL树,红黑树的基本概念梳理

是什么

二叉搜索树(Binary Search Tree)BST是大名鼎鼎的搜索算法。在算法界,$O(n)$ 到 $O(\log_2 n)$ 的效率优化大多和BST有关

用白话文来说,二叉搜索树是一颗对于所有节点左孩子 < 根右子树 > 根的二叉树

基本操作

构建

相关例题:108. Convert Sorted Array to Binary Search Tree

已经给出了定义,Leetcode中有一道将升序数组转换成平衡二叉搜索树的题目。根据二叉树遍历一节的内容,中序遍历的顺序是左 ➜ 根 ➜ 右,再结合二叉搜索树的定义。观察知,二叉搜索树的中序遍历就是一个升序数组。那么问题就转换成了,哪颗平衡二叉树的中序遍历是这个升序数组

因为题目要求平衡二叉树,保证所有子树的高度一样,必须二分输入序列

假设输入序列为[-10,-3,0,5,9],根节点一定在mid = (start + end) // 2 位置,由递归思维:假设再次调用的函数的返回值是已经完成的子树,也就是说只需把[0, mid-1]代表的树作为左子树,和[mid+1, end]代表的树作为右子树即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def sortedArrayToBST(self, nums):
"""
:type nums: List[int]
:rtype: TreeNode
"""
if not nums: return None

mid = len(nums) // 2
root = TreeNode(nums[mid])
root.left = self.sortedArrayToBST(nums[ : mid])
root.right = self.sortedArrayToBST(nums[mid+1 : ])

return root

查找

相关例题:700. Search in a Binary Search Tree

最常见的二叉树操作,查找一个对应节点,平均查找长度为 $\log_2(n)$ 。二叉搜索树性质,左孩子<根<右孩子,按照规律进行递归即可。省略迭代写法,只需要按照顺序进行一个节点一个节点顺下即可,非常简单

1
2
3
4
5
6
7
8
9
10
class Solution:
def searchBST(self, root, val):
"""
:type root: TreeNode
:type val: int
:rtype: TreeNode
"""
if not root or root.val == val:
return root
return self.searchBST(root.left, val) if val < root.val else self.searchBST(root.right, val)

判断

相关例题:98. Validate Binary Search Tree

【输入】给定一个树的结构【操作】判断这颗树是不是二叉搜索树【输出】True or False

① 使用中序遍历,结果是升序序列则为二叉搜索树(前面讲定义的时候已经讲解的原因)

② 去重复操作。①中在遍历过程就可做判断,不需要重新再做一次升序判断

这里实现使用迭代写法,递归写法比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution(object):
def isValidBST(self, root):
"""
:type root: TreeNode
:rtype: bool
"""

if root is None: return True
stack, inorder = [], []
p_node = root
pre_node_val = float("-inf")
while stack or p_node:
while p_node:
stack.append(p_node)
p_node = p_node.left
cur_node = stack.pop()
inorder.append(cur_node.val)
if pre_node_val >= cur_node.val:
return False
pre_node_val = cur_node.val
if cur_node.right:
p_node = cur_node.right
return True

删除

二叉查找树中的删除节点操作,详见链接

需要分为3种情况进行讨论

  • 没有孩子的节点 ➜ 直接将它删除即可,它的父节点的孩子替换成空
  • 只有一个孩子的节点 ➜ 直接上升孩子的位子替代被删除的即可
  • 两个孩子的节点 ➜ 此种情况比较麻烦,需要参看详细链接

相关题目

🔲 ☆

【直观算法】二叉树遍历算法总结

【阅读时间】7 - 10 min | 4300字
【阅读内容】结合应用场景,总结有关二叉树遍历的所有算法和对应Leetcode题目编号。基于Python代码,给出完整逻辑链。希望给读者一个线头,让你永远忘不了这几个遍历算法

遍历算法总览

遍历的含义就是把树的所有节点(Node)按照某种顺序访问一遍。包括前序中序后续广度优先(队列),深度优先(栈)5中遍历方法

遍历方法 顺序 示意图 应用
前序 根 ➜ 左 ➜ 右
想在节点上直接执行操作(或输出结果)使用先序
中序 左 ➜ 根 ➜ 右
二分搜索树中,中序遍历的顺序符合从小到大(或从大到小)顺序的
要输出排序好的结果使用中序
后序 左 ➜ 右 ➜ 根
后续遍历的特点是在执行操作时,肯定已经遍历过该节点的左右子节点
适用于进行破坏性操作
比如删除所有节点,比如判断树中是否存在相同子树
广度优先 层序,横向访问
树的高度非常高(非常瘦)
使用广度优先剑节省空间
深度优先 纵向,探底到叶子节点
每个节点的子节点非常多(非常胖),使用深度优先遍历节省空间
(访问顺序和入栈顺序相关,想当于先序遍历)

关于应用部分,选择遍历方法的基本的原则:更快的访问到你想访问的节点。先序会先访问根节点,后序会先访问叶子节点

需要说明的是,递归是一种拆分思维的具体问题类别的思维方法,其核心的思维我觉得和动态规划非常类似,都是假设子节点搞定了我现在应该干什么这个问题

先确定Python语言下的TreeNode定义

1
2
3
4
5
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None

需要输出遍历结果时直接输出保存val的数组即可

关于递归算法的解释,博主打算写一份【直观算法】汉诺塔问题最全解答,过后可能会更新,是一篇小品文,比较短,这篇文章只是希望让所有阅读的人能一次就直观的搞明白汉诺塔的算法是怎么做的,永远记住它,也搞懂递归算法

三种遍历方法,都有一个特点,无论是先序根 ➜ 左 ➜ 右,中序左 ➜ 根 ➜ 右,后序左 ➜ 右 ➜ 根,所谓的访问顺序,根是最重要,根才代表了访问这个动作(在我们的代码中,就是把节点中的加入到输出数组中),⭐️而在的位置决定了是否可以访问的条件

比如对于中序来说,的后面,意味着,只要当前操作的节点有节点,就不能输出里面的值

对于后序来说,有了这个直观理解,对理解三者的迭代算法有帮助

先序遍历

在线刷题:Leetcode 44. Binary Tree Preorder Traversal

递归算法

所谓递归(Recursive),即把函数本身看成一个已经有解的子问题

定义函数preorderTraversal(self, node)返回以node为答案的先序遍历结果的数组,假设它的两个孩子node.leftnode.right已经搞定了,即可以返回答案的输出数组。那么思考最终的输出数组是什么样的,很明显要满足根 ➜ 左 ➜ 右的规则,应该返回[node.val] + preorderTraversal(self, node.left) + preorderTraversal(self, node.right)(函数返回的就是一个数组,只需要把它们拼接起来即可)

之后再完善防御性编程的基本步骤(保证函数输入有效),按照这个思路就可以写出先序遍历的递归代码。Python代码的特点是可读性比较强,这样一行代码简洁明了,能简洁的表达上面的逻辑链推理过程

1
2
3
4
5
6
7
8
class Solution:
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None: return []
return [] if root is None else [root.val] + self.preorderTraversal(root.left) + self.preorderTraversal(root.right)

当然,如果不使用Python,在语法上无法写的这么简短。常见的标准写法是使用helper()函数,具体实现见下

1
2
3
4
5
6
7
8
9
10
def preorderTraversal1(self, root):
result = []
self.helper(root, result)
return result

def helper(self, root, result):
if root:
result.append(root.val)
self.helper(root.left, result)
self.helper(root.right, result)

迭代算法

同理,递归算法使用系统栈,不好控制,性能问题比较严重,需要进一步了解不用递归如何实现。为了维护固定的访问顺序,使用数据结构的先入后出特性

先处理根节点,根据访问顺序根 ➜ 左 ➜ 右,先入栈的后访问,为了保持访问顺序(先入后出),⭐️先把右孩子入栈,再入栈左孩子(此处需要注意,出栈才是访问顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root: return []

result, stack = [], [root]

while stack:
cur_node = stack.pop() # 访问根节点,直接进行操作(输出到数组)
result.append(cur_node.val)
if cur_node.right: # 先入栈右节点
stack.append(cur_node.right)
if cur_node.left: # 后入栈左节点,这样下一轮循环先访问左节点,维护了访问顺序
stack.append(cur_node.left)

return result

中序遍历

在线刷题:94. Binary Tree Inorder Traversal

递归算法

同理于前序遍历,一模一样的处理方法,考虑访问顺序为左 ➜ 根 ➜ 右即可,快速模仿并写出代码

1
2
3
4
5
6
7
8
class Solution:
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None: return []
return [] if root is None else self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)

同理在这里也附上使用helper()函数的标准写法,代码上来说,只变了名称访问顺序

1
2
3
4
5
6
7
8
9
10
def inorderTraversal1(self, root):
result = []
self.helper(root, result)
return result

def helper(self, root, result):
if root:
self.helper(root.left, result)
result.append(root.val)
self.helper(root.right, result)

迭代算法

核心思路依旧是利用栈维护节点的访问顺序:左 ➜ 根 ➜ 右。使用一个p_node来指向当前访问节点p是代表指针point,另外有一个变量cur_node表示当前正在操作节点(把出栈节点值加入输出数组中),算法步骤如下(可以对照代码注释)

① 访问当前节点,如果当前节点有左孩子,则把它的左孩子都入栈,移动当前节点到左孩子,重复第一步直到当前节点没有左孩子

② 当当前节点没有左孩子时,栈顶节点出栈,加入结果数组

当前节点指向栈顶节点的右节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None: return []
result, stack = [], []

p_node = root # 当前访问节点指针
while p_node or stack:

while p_node: # 把所有当前访问节点的左孩子都入栈
stack.append(p_node)
p_node = p_node.left

cur_node = stack.pop() # 操作栈顶节点,如果是第一次运行到这步,那么这是整棵树的最左节点
result.append(cur_node.val) # 因为已经保证没有左节点,可以访问根节点
if cur_node.right:
p_node = cur_node.right # 将指针指向当前节点的右节点

return result

如果想要精简代码,从逻辑上来看,p_node可以使用root代替,这样写只是为了让代码更可读,和逻辑链相切合,方便理解

后续遍历

在线刷题:145. Binary Tree Postorder Traversal

递归算法

同理先序遍历,代码如下

1
2
3
4
5
6
7
8
class Solution:
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None: return []
return [] if root is None else self.postorderTraversal(root.left) + self.postorderTraversal(root.right) + [root.val]

节省版面,使用helper()函数的写法只需要改变函数名访问顺序

迭代算法 1

后序遍历访问顺序要求为左 ➜ 右 ➜ 根,在对访问节点进行操作的条件是,它的左子树和右子树都已经被访问。这样算法的框架就出来了:只需要对每个节点进行标记,表示这个节点有没有被访问,一个节点能否进行操作的条件就是这个节点的左右节点都被访问过了。

因为栈先入后出,为了维护访问顺序满足条件,入栈顺序应该是根 ➜ 右 ➜ 左(和要求访问顺序相反)。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None: return []

result, stack = [], [(root, False)]

while stack:
cur_node, visited = stack.pop()
if visited: # 只有访问状态为True的节点才能被操作
result.append(cur_node.val)
else:
stack.append((cur_node, True))
if cur_node.right:
stack.append((cur_node.right, False))
if cur_node.left:
stack.append((cur_node.left, False))

return result

迭代算法 2

还有一种迭代算法利用后序遍历的本身属性,注意到后序遍历的顺序是左 ➜ 右 ➜ 根,那么反序的话,就直接倒序的输出结果,即反后序根 ➜ 右 ➜ 左,和先序遍历的根 ➜ 左 ➜ 右对比,发现只需要稍微改一下代码就可以得到反后序的结果,参考先序遍历,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None: return []

result, stack = [], [root]

while stack:
cur_node = stack.pop()
result.append(cur_node.val)
if cur_node.left: # 修改顺序
stack.append(cur_node.left)
if cur_node.right: # 修改顺序
stack.append(cur_node.right)

return result[::-1] # 反序操作

广度遍历

从上到下的层序102. Binary Tree Level Order Traversal

从下到上的层序(Bottom-up) 107. Binary Tree Level Order Traversal 2

按照层序进行遍历的的过程,有两种说法,一种是按照层序的从顶到底的(level order),另一种是从底到顶的(bottom up),具体实现上来说,就是输出反序即可。在具体问题设计上可能有区别,但是基本思路不变

广度遍历的核心思路就是使用队列,即先进先出 First-in First-out,这里很关键的一点就是以来作为入队和出队的判断条件。并且因为按照层的顺序,是从左到右,所以遍历顺序(入队顺序)为左 ➜ 右

基本思路参看代码注释,逻辑比较简单。实现上,使用Python中的自带类deque来实现,新建为queue = deque([]),入队为queue.append(),出队为queue.popleft()

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
from collections import deque

class Solution:
def levelOrder(self, root):
"""
:type root: TreeNode
:rtype: List[List[int]]
"""
if root is None: return []
result, queue = [], deque([root])

while queue:
level_len = len(queue) # 记录现在队列中的节点数量
level_nodes = [] # 每层输出
while level_len > 0: # 具体出队入队操作,保证本层所有节点的子节点都入队
cur_node = queue.popleft()
level_nodes.append(cur_node.val)
if cur_node.left:
queue.append(cur_node.left)
if cur_node.right:
queue.append(cur_node.right)
level_len -= 1
result.append(level_nodes)

return result

Brew作者被拒的题

226. Invert Binary Tree,就是一道基本的树的遍历题。有故事说Mac包管理工具Brew的作者Max在Google被面试这道题,没写出来,被拒了。之后Max去了Apple。个人感觉,对于遍历的理解,如果是真的根据逻辑链理解,且对递归有着深刻的理解,那实在不应该写不出这道题,因为真的很简单

题目是这样说的,要求把一颗二叉树的所有左右子树互换位置

递归算法

假设左右子树都搞定了,那么当前节点需要的操作为:把当前节点的左右孩子互换即可,写成递归非常简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root is None: return []
# 在本节点的操作,左右孩子互换
root.left, root.right = root.right, root.left
# 已经搞定的左右孩子,使用递归的思路写出函数表达式
self.invertTree(root.right) # 下面两句的顺序并不重要
self.invertTree(root.left)
return root

迭代算法

因为对于每一个节点,只需要把它的左右孩子互换位置,并且依次遍历即可,使用DFSBFS都是一样的,这里用使用栈的深度优先搜索举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root is None: return []
stack = [root]

while stack:
cur_node = stack.pop()
# 对当前节点进行操作
cur_node.left, cur_node.right = cur_node.right, cur_node.left
# 进行入栈操作,保证访问到每一个节点
if cur_node.left: stack.append(cur_node.left)
if cur_node.right: stack.append(cur_node.right)

return root

总结

二叉树遍历问题最关键的逻辑链记忆点如下

  1. 遍历顺序

    ⭐️遍历顺序非常重要,即某 ➜ 某 ➜ 某。如果这一点你不太记得,我认为在考试的过程中可以尝试向面试官确认,的带选项只有三个,就是,全排列也只有6种,长时间不用不记得也是情有可原的。所以在这里非常优秀

  2. 递归 ➜ 假设搞定了

确认遍历顺序后,写出递归方法的核心思维是:⭐️假设左右孩子搞定了(搞定的方式就是调用函数本身,替换自变量即可),现在怎么做才能得到最终答案

  1. 迭代 ➜ 根的位置

一般面试官会继续询问迭代方法如何写,这里的核心思维是:⭐️关注的位置对应的就是出栈输出的操作(在例题中就是添加到输出数组)

那么根据遍历顺序,⭐️只要之前的孩子不为空不能出栈输出,要继续入栈(办法自己想即可,每次可能写出来的代码都不同,但是思路相同。需要例子,可以参考中序后序里的迭代算法部分)

❌