MoreRSS

site iconhuizhou | 萝卜修改

Golang 分布式相关的主题,读书笔记
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

huizhou | 萝卜的 RSS 预览

Go 1.26 黑科技:跳过 GC 直接释放内存,性能飙升 200%

2025-11-13 16:58:54

最近,Go 语言社区围绕一个全新的内存管理提案展开了激烈讨论:在不依赖垃圾回收 (GC) 的情况下直接释放并重用内存#74299 引入了 runtime.free 及相关机制,试图让编译器和标准库在特定场景下安全地跳过 GC,对短命的内存对象进行即时回收利用github.comgo.googlesource.com。此举被认为可能为 Go 带来一次性能上的革命:初步原型显示,在 strings.Builder 这样的场景中,利用该机制性能提升可达 2 倍github.com。本文将回顾 Go 内存管理领域从 arena 实验到 memory region 构想,再到 runtime.free 提案的探索之旅,并剖析这一新提案的技术细节、产生的意义、演化过程,以及对普通开发者的影响。

runtime.free 将在 Golang1.26 中 以 GOEXPERIMENT 的方式提供实验性支持。

背景:一场关于“手动”内存管理的漫长探索

自 Go 语言诞生以来,自动垃圾回收(GC)就是其核心特性之一。然而在对性能极度敏感的场景(如高吞吐的服务端程序)中,GC 带来的开销始终让开发者有所顾虑。为了进一步降低 GC 负担,Go 团队近年开始了一系列关于“手动”或“半自动”内存管理的探索尝试。

Arena 实验 —— 强大却难以融合

Arena 实验#51317是 Go 团队在 2022 年迈出的大胆一步。它引入了一个新的 arena 包和 Arena 类型,允许开发者将一组生命周期相同的对象分配到一个独立的内存区域中,并在不需要时一次性释放整个区域 这一做法类似其他语言的 region-based memory management 思想:大量对象集中分配、集中释放,从而降低常规分配/回收的成本。

Arena 的优点在某些场景下非常显著:所有对象统一释放,大幅减少了 GC 扫描和回收的工作量,谷歌内部测试显示对大型应用最高可节省约15%的 CPU 和内存开销。但是,Arena 随即暴露出严重的问题——API 侵入性太强。为了使用 Arena,几乎每个相关函数都不得不增加一个 arena.Arena 参数,这导致这种用法具有“病毒式”传播效应,破坏了 Go 一贯强调的简洁与可组合性。另外,Arena 在与 Go 现有特性(如隐式接口、逃逸分析)配合时也出现了诸多不兼容之处。最终,由于 API 难以融入生态,Go 官方在 2023 年初宣布 无限期搁置 Arena 提案,并明确表示 GOEXPERIMENT=arena 仅供实验、不建议在生产中使用。

Memory Region 构想 —— 优雅但实现复杂

吸取了 Arena 的教训,Go 团队接着提出了更贴合 Go 哲学的概念:内存区域(Memory Region#70257)它设想引入一种更透明的机制——例如通过一个 region.Do(func(){ ... }) 调用,将某段函数作用域内的所有内存分配隐式绑定到一个临时区域。当这段代码执行完毕时,该区域内分配的所有对象都可以一并释放。

Memory Region 的优点在于:对开发者而言几乎是透明的,无需修改函数签名或显式传递 Arena 对象。另外,通过运行时的巧妙设计,它依然能保持内存安全。具体来说,如果区域中的某个对象被外部保留(“逃逸”出了区域作用域),运行时会自动将该对象挪回全局堆由 GC 管理,从而避免类似 Arena 那样可能出现的 use-after-free 错误。这一设计既有手动内存管理的性能,又尽可能避免了手动管理常见的安全隐患。

然而,Memory Region 的问题在于实现极其复杂。要支持这种“区域化”的内存管理,需要对运行时和 GC 做重大改造。例如,开启区域时可能需要一个特殊的低开销写屏障来追踪对象逃逸情况,这增加了垃圾回收机制的复杂性和运行成本。虽然理论上可行,但要让这一方案高效稳健地落地,无疑是一项长期且充满不确定性的研究课题。迄今为止,Memory Region 仍停留在讨论和原型阶段,没有迅速融入 Go 主线。

最终的焦点:runtime.free

在 Arena 的侵入性和 Memory Region 的复杂性之间,Go 团队终于找到了一条更务实、工程上可行的中间路线——这就是本次的 runtime.free 提案。相比之前“大包大揽”的方案,runtime.free 走的是精细化局部优化的路子:与其让开发者手动管理整片内存,不如让更了解代码细节的编译器底层标准库来决定何时安全地释放特定的堆内存。换言之,runtime.free 旨在像一把手术刀,精准切除那些生命周期短暂且已确定不再使用的内存块,减少 GC 不必要的工作。

这种方法极大地缓解了 Arena 的可组合性难题(因为开发者不需要改动代码、一切由编译器和运行时自动处理),也避开了 Memory Region 那种对 GC 全局机制的大改动。更重要的是,它为解决 Go 长期存在的性能**“鸡与蛋”困局提供了新的思路:许多优化(例如更激进的逃逸分析)过去之所以收效甚微,是因为即便消除了某个原因,内存对象仍可能由于另一原因**逃逸到堆上,最终并未减少 GC 负担。而 runtime.free 的出现,相当于提供了一把钥匙,可以打破这种循环——一旦对象在运行时被判定“确实不再需要”,就立即释放,从而真正实现减少 GC 压力的初衷

runtime.free 的实现机制:编译器自动化 + 标准库配合

需要强调的是,runtime.free 并不打算提供给普通开发者一个手工调用 free 的新玩具。相反,它采取高度受控的“双管齐下”策略,通过编译器标准库的改进来实现内存释放优化,同时不向 Go 程序员暴露额外的复杂度。

编译器自动释放 (runtime.freetracked)

首先,也是整个提案最令人兴奋的部分:编译器将自动插入内存释放逻辑。具体而言,当编译器检测到某些场景下分配的内存可以安全提前回收时,就会在编译阶段悄悄地产生额外的代码来跟踪并释放这些内存:

  • 识别阶段: 对于典型的 make([]T, size) 切片分配,如果编译器发现该切片虽然因为长度或容量未知而必须逃逸到堆上,但它的使用范围不超过当前函数(例如不会被保存到全局或返回给调用者),那么编译器将把这次分配标记为可跟踪释放。这种情况下,会调用一个特殊的分配函数(如 makeslicetracked64)来分配对象,并将该对象的指针记录到当前函数栈上的一个追踪列表

  • 跟踪阶段: 编译器在栈上维护一个 freeables 数组(或切片),收集所有被标记为可释放的堆对象。当有新的可释放对象分配时,其指针会被追加到这个列表中。

  • 释放阶段: 在函数返回前,编译器会自动插入一行类似 defer runtime.freeTracked(&freeables) 的调用tonybai.com。这样,当函数退出时,这个延迟调用将执行,通知运行时回收 freeables 列表中记录的所有堆对象。这种做法确保了在作用域结束时,临时分配的对象立即被释放,而无需等待下一轮 GC。

未命名

对于开发者来说,这一切都是透明的:你完全可以像往常一样编写代码,而编译器在背后已经将其“悄悄优化”为一个更少堆分配、更少 GC 压力的版本。举个简单例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 开发者原始代码
func f() {
    buf := make([]byte, size)  // 可能逃逸到堆上
    // ... 使用 buf
} 

// 编译器优化后的等效代码(概念示意)
func f() {
    var freeables []unsafe.Pointer
    buf := runtime.makeslicetracked64(..., &freeables)  // 分配受跟踪的 slice
    // ... 使用 buf
    defer runtime.freeTracked(&freeables)  // 函数退出时释放 buf 对应的内存
}

经过这种改写,原本可能需要 GC 扫描回收的 buf 内存,将在函数结束时立即归还给运行时可用的内存池。因此,未来我们编写的一些看似会产生大量堆分配的代码,有望在不改变任何源码的情况下,由编译器替我们转换成“零 GC 压力”的高效版本——开发者对此毫无感知,但程序性能却因此获益。

标准库协助释放 (runtime.freesized)

另一方面,对于 Go 标准库中少数性能关键的组件,开发团队也在尝试手动加入 runtime.free 的调用。这并不是要把手动内存管理强加给所有库,而是利用标准库对自身情况的了解,在极有限的热点场景显式地释放内存,以追求极致性能。提案中提到的主要目标包括:

  • strings.Builder / bytes.Buffer 的扩容:当内部缓冲区需要增长时,旧的缓冲区实际上已经不再使用,完全可以当场释放,避免占用堆并减轻后续 GC 压力。
  • map 的扩容:Go 的 map 在扩容和重新哈希(rehash)时会分配新的底层数组,此时旧的 buckets 数组事实上已死,同样可以立即回收。
  • slices.Collect 等切片收集/拼接的操作:在构造最终结果过程中产生的大量中间切片,仅用于过渡,也可以及时释放。

对于这些场景,runtime.free 提供了一个内部运行时函数 runtime.freeSized(ptr, size, noscan)(提案原型中使用的是 freesized),允许在知道一个对象指针 ptr 及其大小后,立刻释放对应内存。这种调用仅限于非常底层且对内存使用有精确认知的代码。例如 Go 作者们在实验中修改了 strings.Builder 的代码,在扩容逻辑中加入对旧缓冲区的 runtime.freeSized 调用。结果表明:对于执行多次扩容的场景,新版 strings.Builder 性能提升了约 45%~55%,几乎快了一倍!换句话说,通过在正确的时机手动释放内存,可以实打实地换来巨大性能收益。

需要注意的是,这种手动调用只会出现在少数标准库内部。Go 团队并不打算在诸如 net/http 这样的高级库里遍地插入 runtime.free —— 毕竟那样又回到了“到处手动管理内存”的老路上。这一步更多是为了验证:在哪些特殊场景下,提前释放内存能够带来明显收益。如果证明效果显著,我们也许会在未来看到这些改进融入正式版本中;如果收益不大或风险高,也可以根据讨论再决定是否采纳。

性能影响与收益

让 GC “少管一些事”听起来很美好,但也要评估此举本身的性能代价。插入额外的跟踪和释放逻辑,会不会拖慢常规代码的速度?根据目前的原型测试结果,答案是几乎可以忽略。对比启用 runtimefree 实验前后的基准数据表明:*在没有可释放对象的普通分配场景下,新机制对性能的影响在 -1.5% 到 +2.2% 之间,几何平均值几乎为零。也就是说,如果你的代码并不存在那些可以提前释放的内存对象,启用这个功能对性能既不会造成明显负担,也几乎不会带来益处——它基本是“零成本”(pay-for-what-you-use)*的。

而在命中了优化路径的情况下,收益则是多方面的

  • 减少 GC 的 CPU 消耗: 这是最直接的好处。部分内存由运行时立即回收,意味着 GC 每次需要标记、扫描的对象变少,从而降低了 GC 自身的CPU占用
  • 拉长 GC 间隔、缩短写屏障时间: 垃圾变少了,GC 自然可以更久运行一次。对于 Go 程序来说,这意味着更多时间处于无 GC 干扰的状态,写屏障(write barrier)启用的总时间减少,进而让应用代码本身跑得更快。
  • 提高缓存局部性: 被 runtime.free 释放的对象立即回收到对应大小类的空闲链表中,下一个相同大小的新对象分配很可能重用这块内存。 这样一来,内存分配/释放形成类似栈式(LIFO) 的模式,新分配的内存地址往往与刚释放的相同,对 CPU 缓存非常友好。相比任由 GC 随机回收、重新从堆中找内存,这种局部性有望进一步提升运行效率。
  • 减少 GC 停顿和辅助操作: 总体上,GC 工作量变小后,STW(stop-the-world)暂停的时间和 GC 辅助运行(assist)的触发频率都会降低,让应用更平稳

除此之外,新的垃圾回收器 Green Tea 也可能从这种优化中受益——例如更高的每个span内存利用率,等等。尽管这方面还是推测,但runtime.free 提案的出现显然为未来 GC 和内存优化的融合创造了更多可能。

意义与展望:开发者获得什么?

从开发者的角度来看,runtime.free 究竟意味着什么?一言以蔽之:性能提升,几乎无需额外付出。对于普罗大众的 Go 开发者来说,这个提案不会改变我们日常编码的方式——没有新语法、也无需调用新的 API。所有魔法都发生在幕后:编译器变得更聪明,运行时/标准库替我们多做了一些工作。然而,它的影响可能是深远的:

首先,这标志着 Go 的内存管理正在探索 “自动 GC”之外的第三条道路。传统上,我们有完全自动的 GC(简单易用但性能牺牲)和手工的内存管理(复杂易出错但性能可控)。而 Go 的 runtime.free 尝试证明,两者并非水火不容:语言运行时本身可以变得更智能,在保证内存安全的前提下,帮我们完成一些人工才能做到的优化。从某种意义上说,Go 正在尝试“靠自己”变得更快,而不是把负担转嫁给开发者。

其次,对性能敏感的Go程序将直接受益于此。在未来的版本(提案目前计划针对 Go 1.26),当这一实验正式上线后,你或许会发现某些场景下 GC 压力突然降低了。例如,大量使用临时切片进行计算的函数,不再生成那么多短命的垃圾;频繁扩容的 bytes.Buffer、构建巨型 slice 的代码,在新版标准库里跑得飞快。这些性能改进都是 “开箱即得” 的,开发者甚至不需要知道 runtime.free 的存在,就已经享受到了它的好处。

当然,runtime.free 仍处于试验和完善阶段。它目前通过 GOEXPERIMENT=runtimefree 提供,说明官方也在审慎评估其效果和风险。接下来社区会继续打磨细节,确保不会引入难以预料的错误(比如要严格杜绝“提前释放仍在用的对象”这种灾难性情况)。好消息是,到目前为止初步验证并未发现不可逾越的技术障碍,核心团队成员也给予了正面反馈。

总体而言,runtime.free 提案代表了 Go 内存管理上务实而具有前瞻性的一步。它不追求颠覆性的架构重写,而是聚焦于具体的瓶颈问题,寻求切实的优化突破;它也不牺牲类型安全和简洁性,将复杂度限定在编译器和运行时内部。这种思路一旦被证明行之有效,未来完全可以推广到更多模式(例如识别更多 append 循环的场景等),进一步减少 Go 程序的内存开销和 GC 次数。

对于普通开发者来说,这意味着更快的程序更少的垃圾回收停顿,而你依然可以像过去一样专注于业务逻辑,无需为手动内存管理操碎心。随着编译器与运行时不断进化,Go 有望在保持“一键爽跑”的易用性的同时,在性能上再攀新高峰——这一切,值得我们拭目以待。

引用资料:

HTTP/3:看似无处不在,实则难觅踪影

2025-11-11 18:57:08

Featured image of post HTTP/3:看似无处不在,实则难觅踪影

HTTP/3 的研发至少可追溯至 2016 年,而其底层传输协议 QUIC 更是由 Google 在 2013 年率先提出。如今这两项技术均已确立国际标准:获得 95% 浏览器的支持,**Cloudflare 处理的 HTTP 请求中已有 32% 采用该协议**,并且在 HTTP Archive 数据集中,有 35% 的网站 宣称支持HTTP/3 (通过 alt-svc 或 DNS)。

我们不仅成功开发出全新一代 HTTP 协议,更已将超三分之一的网络流量迁移至该协议——这堪称里程碑式的进展。

然而矛盾的是,包括 Node.js、Go、Rust、Python 和 Ruby 在内的主流编程语言,其标准库均未内置对 QUIC 或 HTTP/3 的支持。Curl 虽然近期新增了相关功能,但仍标记为实验性质且在大多数发行版中默认禁用。某些语言虽有第三方实现库,但均处于实验阶段,且无法与核心网络 API 协同工作。更值得注意的是,尽管移动网络是 HTTP/3 的关键应用场景,Android 主流 HTTP 库 OkHttp 仍明确不支持该协议。Nginx 仅提供实验性模块且默认关闭,Apache 既无支持计划也未公布路线图,而 Kubernetes 最流行的反向代理 Ingress-Nginx 更是完全放弃了支持计划,将相关功能移交至尚未发布的新一代项目。

事实上,目前几乎找不到能完整支持 HTTP/3 的流行开源工具——这项技术的推广部署仍处于萌芽阶段。

这种矛盾现象背后究竟隐藏着什么?

本文假设读者已了解 HTTP/1.1、HTTP/2 与 HTTP/3 的核心差异。如需入门资料,curl 创始人 Daniel Stenberg 撰写的 http2-explainedhttp3-explained 是绝佳参考。

让我们回溯根本:为什么这很重要?如果浏览器和大型 CDN 已支持 HTTP/3,其他客户端或服务端实现是否还有必要跟进?

有观点认为,在负载均衡器之后使用 HTTP/2 意义有限。其核心论点是:HTTP/2 的多路复用主要解决延迟与队头阻塞 (Head-of-line blocking) 问题,但在延迟极低的内部网络中,通过长连接即可规避这些问题

这个论点同样适用于 HTTP/3:它对高延迟、多请求的 浏览器-CDN 场景有益,但对其他场景价值有限。但即使只考虑 HTTP/1.1 与 HTTP/2,多路复用优势的现实情况也更加复杂:

  • 响应延迟不仅来自网络传输:服务端处理缓慢同样会阻塞 TCP 连接
  • 负载均衡器常与后端服务异地部署(例如通过全球 CDN 提供服务时,动态请求仍需回源至独立后端)
  • 长连接 TCP 连接并不可靠:即使在数据中心内,网络故障也时有发生,“保持连接”只是理想状态。HTTP 协议本身也会强制中断连接(如响应体传输中途失败时)
  • 流量波动会导致 TCP 连接数失衡:要么长期维持冗余连接池,要么在峰值时建立新连接,面临慢启动、往返延迟与 TLS 握手开销
  • 非网站类流量(移动应用、API 服务、物联网设备)同样面临网络延迟与服务端阻塞问题,这些场景都能从 HTTP/2/3 中获益

除多路复用外,HTTP/2 还有更多跨场景优势:

  • 头部压缩(HTTP/2 的 HPACK 与 HTTP/3 的 QPACK)显著减少传输数据量,对内部长连接效果尤为明显
  • 双向流通信(仅 HTTP/2/3 支持)开启全新交互模式,gRPC 即基于此特性构建,类似 WebSocket 但完全兼容 HTTP 语义
  • 请求优先级控制允许服务端优化资源分配,这对负载均衡器与后端通信同样重要

HTTP/3 更在以下方面实现突破:

  • 通过取消 TCP 严格包序,使单个流独立传输,避免流间阻塞
  • 结合 TLS 1.3 与 QUIC 实现 0RTT 握手,首次请求无需等待 TLS 握手完成
  • 降低传输开销与连接数,减少客户端能耗与服务端资源消耗
  • 支持连接迁移,IP 变更时保持会话连续性,未来甚至支持多路径传输
  • 采用 BBR 拥塞控制等先进算法,提升网络适应能力
  • WebTransport 提供基础,实现低延迟双向通信的同时解决 WebSocket 的队头阻塞等问题

实际测试数据同样佐证其价值。RequestMetric 的基准测试显示:

各类网站在 HTTP/1.1、2、3 下的加载时间对比,显示 HTTP/3 显著提速

Fastly 也在实际环境中观测到首字节时间的大幅优化:

Fastly 实测 HTTP/3 降低首字节时间 18%

显然,这是一项具有实质价值的技术。

既然 HTTP/3 已完成标准化、获得广泛支持并经过实践检验,没理由不让所有开发者都能通过常用开发工具链享受这些技术红利。

割裂的网络

现实却截然相反:尽管技术优势明显且网络流量占比显著,大多数开发者仍难以端到端部署 HTTP/3。这种现象折射出互联网长期存在的分层现状。如今的网络流量已分化为两种形态:

  • 超大规模流量:主流浏览器与特定移动应用通过精心调优的客户端,与少数科技巨头的自有基础设施或大型 CDN 通信
  • 长尾流量:后端服务、中小型应用、物联网设备、学术研究等多样化场景,依赖开源生态与共享技术栈

这两大阵营的核心差异包括:

  • 长尾流量规模更大: 67% 的网页请求直连源站,Cloudflare 2024 年数据表明其 30% 流量来自自动化程序,60% 属于 API 调用
  • 长尾生态天然碎片化,主要依赖志愿维护的开源项目
  • 超大规模阵营由少数利益相关方主导,能快速协调标准制定与落地
  • 商业动机高度集中:性能毫秒级提升直接关联企业收益
  • 长尾完全依赖开源实现,而超大规模玩家拥有自研定制的实力与资源
  • 版本迭代速度差异巨大:长尾工具注重稳定性,超大规模阵营追求快速迭代

这种分化并非善恶对立——从工程角度,HTTP/3 正是跨组织协作的卓越成果。但问题在于:当下一代网络技术由少数群体定义并优先服务自身需求时,大多数开发者只能通过购买 CDN 服务间接获取技术红利,这无疑限制了创新生态的健康发展。

OpenSSL 与 QUIC 的兼容困局

这种分化最具体的体现就是 OpenSSL 对 QUIC 的支持策略。作为最基础的 TLS 库,OpenSSL 的态度直接影响整个开源生态。事件脉络如下:

  • BoringSSL 2018 年即提供 QUIC API
  • OpenSSL 长期缺失该功能,催生 QuicTLS 等兼容分支
  • 现有 HTTP/3 实现生态(Quiche、msh3、nghttp3 等)均基于 BoringSSL 或分支构建
  • OpenSSL 3.2 起采用不兼容的实现方案,导致生态分裂

curl 的项目现状图清晰展现了这种割裂:

curl 支持的 HTTP/3 组件体系,现有实现均构建于 OpenSSL 替代方案之上

对大多数项目而言,放弃 OpenSSL 转向其他方案成本过高,这导致它们至今无法原生支持 QUIC。Node.js 曾讨论切换方案,但考虑到系统兼容性、长期支持等现实因素,最终难以实施。

这正是双层网络差异的典型体现:开源工具必须保持向后兼容,而超大规模玩家可以为了技术先进性承担更大变更成本

未来走向

组织架构差异正在导致互联网技术栈的分裂。虽然长尾场景未必急需 HTTP/3,但若放任不管,可能导致:

  • 性能差距扩大:超大规模网站在移动网络环境下体验优势加剧
  • 工具链分化:前端框架等基础设施逐渐以 HTTP/3 为默认假设
  • 技术壁垒形成:缺乏 HTTP/3 支持可能成为被限流或验证的依据
  • 生态恶性循环:长尾需求逐渐被技术演进忽略

所有这些都还有一段距离,而且是相当假设性的!我怀疑其中一些假设会在某种程度上发生,但可能性范围很广。不过值得注意的是,这不仅仅适用于 HTTP/3:少数 CDN 和网络客户端的这种集中和协调很容易在许多其他类型的技术改进中也以类似的方式发生。

至少对于 HTTP/3 而言,我希望这里能有一个愉快的解决方案来及时改善这种分裂,尽管我不知道它是否会足够快以避免明显的后果。许多 QUIC 和 HTTP/3 的外部库和实验性实现会随着时间的推移而成熟,而且我认为最终 (我真的非常希望) OpenSSL QUIC API 的分裂将得到解决,从而为 许多基于 OpenSSL 的环境中的 QUIC 支持打开大门,要么通过适配器支持这两种方法,要么通过直接支持 OpenSSL 模型的新 HTTP/3 和 QUIC 堆栈。
然而,所有这一切都不会在今天发生,因此不幸的是,如果您想在您的应用程序中端到端地使用 HTTP/3,您可能还需要经历一段时间的艰难时期。敬请关注。


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。

浏览器的地址栏除了输入网址还能干什么?——玩贪吃蛇

2025-10-02 14:53:26

Featured image of post 浏览器的地址栏除了输入网址还能干什么?——玩贪吃蛇

浏览器的地址栏除了输入网址还能干什么?——玩贪吃蛇

当一个十年前的疯狂创意重新走红,它揭示了互联网正在失去的东西

打开Chrome浏览器,在地址栏输入 https://demian.ferrei.ro/snake# ,然后 你就看到一串奇怪的符号。
是的,你没看错。就盯着你的地址栏,在用箭头键或WASD控制方向。
你会看到一条由奇怪符号组成的小蛇(░░░░░░░⠠⠤⠄⡀░░)在地址栏里爬行,吃掉食物,越长越长。整个游戏就在那个你平时只用来输入网址的细长文本框里运行。
Area

没错,这是一个完整的贪吃蛇游戏,运行在浏览器的URL地址栏里。

上周,这个十年前的项目突然在Hacker News上爆红,获得840个点赞和数百条评论。人们的第一反应都是:“这太疯狂了,你们是怎么想出来的?”

但更疯狂的是这个游戏背后的故事——以及它揭示的关于现代互联网的真相。

一次偶然的好奇心,引发了一场技术冒险

创作者Francisco Uzo回忆起项目的起源时说:“我想知道盲文系统是怎么工作的。”

这就是极客精神的起点——不是为了解决问题,而是纯粹的好奇。

当他深入研究盲文时,发现了一个迷人的规律:每个盲文符号由2x4的点阵组成,8个点位,每个点有"凸起"或"平坦"两种状态,正好产生2^8=256种可能的组合。而Unicode系统完整收录了所有这256个盲文字符。

某天的凌晨三点,他突然意识到:“等等,这不就是一个现成的8位像素字符集吗?”

这种顿悟的瞬间,正是每个极客最享受的时刻——当两个看似无关的知识点在脑海中碰撞,迸发出创意的火花。

然后就是典型的极客行为模式:不管有没有用,先做出来再说
整个实现充满了黑客思维:

  • 将游戏世界映射到盲文字符网格——每个字符就是一个2x4像素块
  • 蛇的身体用实心点(⣿)表示,空白区域用空盲文(⠀)表示
  • 使用JavaScript的history.replaceState()API实时更新URL而不污染历史记录
  • 每帧刷新,整个游戏画面就在地址栏里流畅运行

Francisco的透明度也很"极客":“源代码没有任何压缩或混淆处理。Ctrl+U就能看到全部代码。“整个实现只用了几百行JavaScript,简洁而巧妙。在真正的极客眼中,代码本身就应该是可读的、可学习的、可启发的。

等等,我的游戏怎么看起来乱七八糟的?

如果你现在打开这个游戏,很可能会看到一堆奇怪的%20、反斜杠和乱码。游戏勉强能玩,但画面支离破碎。或者根本不能动起来。

这不是你的问题,也不是代码的问题。问题在于:十年过去了,浏览器变了。

Francisco在讨论中坦言:“这个项目是为十年前的浏览器设计的。自那以后,浏览器进行了一些所谓的’安全改进’,严重削弱了基于地址栏的游戏能力。”

最致命的改变是:现代浏览器开始强制转义URL中的所有空格字符。

在游戏的设计中,空盲文字符(⠀)代表空白区域。但现代浏览器把它们转义成%20或其他字符,导致画面变形。Francisco甚至开发了一套基于Canvas的字体测量系统来检测和替换被转义的字符,但效果仍然有限。

他在浏览器的问题追踪器中提出了申诉。开发者表示理解和同情,但最终的决定是:“安全性照例压倒了趣味性。”

用户报告的游戏体验差异巨大:

  • 有人在Chrome上轻松得到2144分
  • 有人在Firefox上看到满屏乱码
  • 有人在移动Safari上根本看不到游戏
  • 有人的浏览历史被每一帧都污染了

同一个代码,在不同浏览器上的表现天差地别。这就是现代Web开发的现实。

那些年,我们在浏览器里玩过的花样

地址栏贪吃蛇不是唯一一个这类创意项目。Hacker News的讨论中,大家开始回忆起一整个"非常规浏览器游戏"的黄金时代。

URL里的2048游戏

有人分享了 https://aquova.net/games/2048/ ——将风靡一时的2048益智游戏移植到地址栏中。同样的盲文字符技巧,同样的实时URL更新。

浏览器标题栏的贪吃蛇

Francisco提到,Reddit上最近有人发布了新版本——把游戏渲染在浏览器标签页的标题里。虽然这是一种"认输方案”(放弃了在地址栏显示的坚持),但它确实避免了URL转义的问题。

TinyJS贪吃蛇

2023年,另一个开发者用不同的技术路径实现了类似的项目。那时浏览器的限制还没有这么严格。

地址栏3D漫游

有人分享了一个更疯狂的项目:用非常规方式在浏览器中实现交互式3D世界
评论者说:“虽然不是Doom,但你可以在3D世界里四处走动。”

这些项目有什么共同点?它们都是程序员纯粹出于好奇心和乐趣的创造。

没有商业价值,没有实际用途,甚至算不上"最佳实践”。但它们代表了早期互联网的一种精神:这是一个可以玩耍、可以实验、可以突破界限的地方。

为什么这些项目现在越来越难做了?

问题不只是空格转义。过去十年,浏览器经历了大量"安全改进":

历史API的限制

Francisco最初使用history.replaceState()来更新URL而不创建历史记录。但现代浏览器对这个API的调用频率有严格限制。当游戏帧率太高时,系统被迫降级到location.hash方案——结果是你的浏览历史被每一帧游戏都污染了。

一位用户吐槽:“我最烦的就是这个。访问某些网站后查看历史记录,发现被我输入的每个按键都创建了条目。这一切都是以’用户体验’的名义。”

URL长度限制

不同浏览器对URL长度有不同的限制,从2000字符到几万字符不等。对于需要在URL中编码整个游戏状态的项目来说,这是个硬性天花板。

字符集限制

越来越多的特殊Unicode字符被标记为"不安全",在URL中被自动转义或过滤。这直接打击了所有基于Unicode艺术的创意项目。

移动端兼容性

许多移动浏览器根本不显示完整的URL,或者在用户滚动时隐藏地址栏。这让地址栏游戏变得不可玩。

Francisco总结道:“自那以后,浏览器的许多改变都让这类项目越来越难实现。我在浏览器问题追踪器中得到了一些同情,但安全性总是优先的。”

当安全性扼杀了互联网的乐趣

这里出现了一个根本性的矛盾。

**没人会反对浏览器安全性的提升。**URL注入攻击、跨站脚本漏洞、钓鱼网站——这些是真实存在的威胁,影响着数十亿用户。浏览器开发者有责任保护用户安全。

但与此同时,每一次"安全改进"都关闭了某些创造性实验的大门。

Francisco的游戏十年前完美运行,今天却在大多数浏览器上支离破碎。不是因为代码有问题,而是因为浏览器"进化"了。这种"进化"让用户更安全,但也让互联网变得更加统一、更加受控、更加……无聊。

失去的不仅仅是游戏

在Hacker News的讨论中,有人提出了尖锐的批评:“为什么人们不去做有用的事情?别滥用互联网。”

这个评论遭到了社区的集体反驳:

  • “制作这类项目的学习价值是巨大的”
  • “创造性休息和技能开发同样重要”
  • “这就是我来Hacker News的原因——100%的黑客精神”
  • “ChatGPT发明不出这个。我爱这种创造力”

最精辟的回应来自一位用户:“对普通人来说这可能不算什么,但对我来说这太疯狂了。你们是怎么想出这些东西的?”

**这就是问题的核心。**这些项目的价值不在于"有用",而在于它们代表了一种纯粹的创造冲动——因为好奇而探索,因为有趣而创造,因为"为什么不试试"而突破界限。

当我们为了安全而设置越来越多的限制,我们不只是在关闭安全漏洞,我们也在关闭创新的可能性。

权衡是必要的,但我们应该意识到代价

这不是在指责浏览器开发者。他们面对的是极其艰难的权衡决策:

  • 允许灵活的URL编码 vs. 防止注入攻击
  • 支持高频API调用 vs. 避免性能问题
  • 显示任意Unicode字符 vs. 防止钓鱼欺诈

没有完美的答案。 每个决定都有其代价。

但我们至少应该意识到并讨论这些代价。当我们说"安全第一"时,我们应该问:“我们因此失去了什么?“当我们说"用户体验优化"时,我们应该问:“这优化了谁的体验?”

Francisco在浏览器问题追踪器中得到的回复是典型的:“我理解你的困扰,但安全性必须优先。“这个回答在技术层面无可指摘,但它也代表了一种更广泛的趋势:在现代互联网上,控制和安全正在系统性地压倒创造和乐趣。

在AI时代,这种人类创造更加珍贵

当讨论转向AI时,Francisco说了一句发人深省的话:

“这个游戏已经存在十年了,所以很可能在这些AI的训练数据中。机器人也许能复制它,但它们肯定无法享受它!(至少现在还不能)”

这句话触及了当前技术文化的核心焦虑。在ChatGPT可以瞬间生成代码、Copilot可以自动补全函数的时代,什么样的编程项目还值得人类去做?

答案可能就在这个地址栏贪吃蛇里。

AI可以看到盲文字符,可以理解Unicode编码,可以生成JavaScript代码。但AI不能体验:

  • 凌晨三点突然意识到"盲文可以当像素"的那一刻顿悟
  • 第一次看到蛇在地址栏里动起来时的惊喜
  • 花几个小时调试字符转义问题时的执着
  • 十年后看到陌生人喜欢你的项目时的满足感

这些纯粹的人类创造体验,在AI可以复制几乎所有代码的时代,反而变得更加珍贵。

Francisco的项目不是为了解决问题而创造,而是为了探索和乐趣而创造。这种动机,这种过程,这种纯粹的创造喜悦——这是AI永远无法真正拥有的。

我们需要更多像这样的项目

这个地址栏贪吃蛇重新走红,不是因为它特别有用或特别赚钱。它走红是因为它提醒了我们一些重要的事情:

互联网不应该只是一个高效、安全、标准化的工具。它也应该是一个充满惊喜、可以玩耍、可以实验的地方。

当Francisco十年前问自己"盲文是怎么工作的"时,他不是在解决商业问题或优化用户体验。他只是好奇,然后他创造了一个让数千人微笑的项目。

在这个越来越同质化、越来越"安全”、越来越被算法优化的互联网上,我们需要更多这样的项目。不是因为它们有用,而是因为它们提醒我们:

技术的意义不仅仅是效率和安全——它也应该是充满惊奇和乐趣的。

在这个游戏上你能得多少分?你的浏览器支持得好吗?

也许更重要的问题是:这个项目让你想起了什么?你有没有做过类似的"无用但有趣"的创造?

Go 官方再谈错误处理:新语法为何迟迟无法落地?

2025-06-04 20:14:24

Pasted image 20250605093005

Go 官方再谈错误处理:新语法为何迟迟无法落地?

近日,Go 官方博客发布了一篇名为《On | No syntactic support for error handling》的文章。这篇文章没有带来期待中的语法突破,反而再次回顾了 Go 语言过去在错误处理语法上多次尝试的失败经验。
Go 团队为什么发表这么一篇文章?

为什么 Go 团队反复探讨“错误处理”?

自诞生以来,Go 语言以简洁明了著称,但却始终背负着一个无法回避的问题:错误处理语法过于冗长。
我们熟悉的 Go 错误处理通常长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
```Go
func printSum(a, b string) error {
    x, err := strconv.Atoi(a)
    if err != nil {
        return err
    }
    y, err := strconv.Atoi(b)
    if err != nil {
        return err
    }
    fmt.Println("result:", x + y)
    return nil
}

反复出现的 if err != nil 显得单调乏味,代码中大量的错误处理重复模式经常被批评为“机械且冗余”。因此,过去数年,Go 团队多次尝试设计更为简洁的新语法,但最终都未能成功落地。

曾经尝试却未成功的那些提案

Go 团队在过去提出了几种备受关注但最终放弃的方案:

  • 2018年:checkhandle 提案
    • 尝试用新关键字简化错误传播,但社区担忧复杂度增加,被否决。
  • 2019年:try 提案
    • 引入内置函数 try,期望简化语法。但社区认为该方案过于隐式,破坏了代码的明确性,也被搁置。
  • 2024 年:?
    Ian Lance Taylor 参考 Rust 的实现提出了 “使用 ? ”减少错误处理样板 也遭遇到了大量的反对意见, 我也水了一篇博客

这些尝试的失败不仅仅是技术上的,更体现了 Go 社区一种特殊的文化和理念——明确胜于隐式

真正的动机是什么?

细读这篇博客,我们会发现 Go 团队并不是单纯地“回忆过去”,而是在认真回应社区持续发酵的讨论:

究竟有没有必要为错误处理引入新语法?

文章的字里行间传递了一个关键的讯号:

“Go 社区对于新语法的需求,并非简单的‘yes or no’,而是深入涉及到语言设计哲学和社区价值观的问题。”

换句话说,Go 团队的动机并不是要立即给出一个新提案,而是希望借此引导社区回到初心,认真审视“我们真正需要什么样的语言设计?”

“明确胜于隐式”:Go 社区无法放弃的原则

Go 语言的成功,正是因为其清晰且明确的设计哲学。任何新语法都必须经过社区的严格审视:

  • 新语法能否保持明确性?
  • 新语法能否带来足够的收益,值得牺牲现有的简单性?
  • 新语法是否有可能带来其他负面影响?
    在这种原则下,每个提案都难免面临严格审视和质疑。这也是过去的尝试频频受挫的根本原因。

五、未来的路在哪里?

这篇官方博客所释放的另一个信息是:

Go 团队已经接受了短期内不会推出新错误处理语法的现实。

他们似乎在暗示:错误处理的语法简化并非完全不可行,但至少现在还未找到完美的答案,社区需要更多时间的探索、实践与反思。
因此,未来很长一段时间内,Go 程序员仍需忍受现有模式的重复。但同时也意味着,Go 语言暂时保持了它清晰、直接、没有魔法的风格。

Go 团队主动发布这样一篇文章,看似回顾历史、承认失败,实则希望引发社区更深层次的讨论:

  • 我们究竟希望 Go 变成什么样子?
  • 为了简洁,我们愿意牺牲多少明确性?

无论你是否赞同当前的处理方式,这样的反思和讨论对于语言生态的健康发展,都是极其重要的。
也许下一次提案会再次失败,也许未来某一天,社区终究找到满意的平衡点。但至少,我们可以肯定的是:
Go 社区一直在探索,一直在反思,也一直在进步。
原文链接:https://go.dev/blog/error-syntax


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。

Go 的 map 为什么变慢了?.zh-cn

2025-05-20 22:18:57

Featured image of post Go 的 map 为什么变慢了?.zh-cn

这篇文章解释了为什么在 Go 1.24 版本中,你的程序可能因为 map 变慢了,以及 Go 团队是怎么计划修复这个问题的。

Golang 1.24 中最吸引我的功能就是 SwissMap,在以前的非官方实现中,有些场景能够提升50% 的性能,官方的实现中,也有不小的性能提升。
详情参考我以前的文章:
SwissTable 会成为 Golang std map嘛?
Go 1.24 的 Swiss Map:兼容性、扩展哈希与遗留问题

但是 如果你在使用 Go 1.24 时可能会发现SwissMap 没有达到预期的表现,甚至程序运行变慢了,特别是在 Map很大的时候,这不是你的幻觉,确实存在这个问题。
Pasted image 20250520225045
https://x.com/valyala/status/1879988053076504761

这个问题记录在 Issue #70835 中,开发者们正在努力解决它。

问题出在哪?

Go 的 map 在新版中使用了Swiss Table,它在小 map 和高并发的场景下非常快。但是当 map 很大、数据又不在 CPU 缓存里(也就是说,数据是“冷”的),就会变慢。

为什么会这样呢?

因为 SwissMap 的内部结构比较复杂,它会分几层来存储数据:

  1. 首先是一个 map 的头部结构
  2. 它指向一个目录,这个目录是多个 table 的指针组成的列表
  3. 每个 table 里面有控制信息、key 和 value

当你查找一个 key 的时候,可能需要进行 4 到 6 次跳转,每一次都可能遇到缓存未命中。这样就会导致 CPU 忙着从内存取数据,速度就慢了。

像 Prometheus 这样的大型项目就发现了这个问题。他们升级到 Go 1.24 后,CPU 使用率上升了很多,经调查发现就是因为 map 的查找变慢了。

是怎么发现的?

问题并不是在测试用例里发现的,而是在真实的线上场景中出现的。

  • 使用了很大的 map(比如几兆字节大小)
  • 经常读取 map 里的数据
  • 数据不在 CPU 的高速缓存里

Go 团队的工程师 Michael Pratt 通过做很多测试,找到了 map 访问变慢的原因,并在 Issue #70835 中详细说明。

怎么修?

为了让 map 更快,他们计划做以下几件事:

  • 简化目录结构:把原来存指针的列表改成直接存结构,减少一次跳转
  • 控制信息更紧凑:把控制信息安排得更集中,这样更容易被 CPU 一次加载
  • 分离 key 和 value:改成“key-key-key + value-value-value”的结构,这样可以优化加载顺序
  • 对齐控制字节:把控制信息按照 CPU 缓存对齐,减少未命中

这些改动并不简单,因为会影响到 Go 的运行时核心:

  • 要确保垃圾回收能正常工作
  • map 扩容和缩容逻辑要更新
  • 要确保对小 map 的性能没有影响

哪些地方在讨论这个?

这个问题被广泛讨论和跟踪,可以通过Issue #70835 了解更多的 细节。 Go Release Dashboard 中已经标记这个问题将会在 Go1.25 中解决
此外 Issue #71368 中也讨论了 另一个与内存布局的问题。

总结

Go 团队一直在努力让语言运行得更快更稳。SwissMap 是个好改进,但它也带来了新挑战,比如这次的冷缓存性能下降。
Issue #70835 展示了 Go 是如何通过社区反馈不断进步的。感谢像 Prometheus 这样的开源项目,他们的报告帮助 Go 做得更好。
如果一切顺利,Go 1.25 就能把速度和稳定性都带回来。
我们一起期待吧!

Green Tea GC: Golang 的 ZGC?

2025-05-05 16:30:26

Featured image of post Green Tea GC: Golang 的 ZGC?

近年来,Go 语言的垃圾回收(GC)机制虽然经历了多个版本优化,但它的性能瓶颈,尤其在高并发与大规模内存场景下,依然是开发者关注的重点。最近,Go 官方在 GitHub 上提出的 Green Tea GC(#73581)引发了热议:它能否进一步解决 Go GC 的耗时问题?本文将深入解析 Go GC 的设计、缺点、实测表现,并带你了解 Green Tea GC 的技术突破。

📦 Go GC 的设计与实现

自 Go 1.5 起,Go 使用并发标记-清除(concurrent mark-sweep)算法,结合“三色标记”模型与 Yuasa 写屏障。

简而言之,Go GC 会在后台并发地遍历堆内存,标记可达对象,并逐步清除未被引用的内存块。整个回收过程中,Go 追求低延迟、低停顿

✅ 并发标记、并发清除
✅ 不会移动对象(即 no compaction)
✅ 按 span(内存块)分批清扫,减少单次 STW(Stop-the-World)时长

这种设计的直接好处是:应用大部分时间能与 GC 并行工作,最大停顿时间通常低于毫秒级。

🚧 Go GC 的已知问题

虽然 Go GC 的延迟表现优秀,但它在耗时和扩展性上仍有几个硬伤,尤其体现在:

1️⃣ 内存访问低效
GC 的标记阶段会跨对象跳跃,导致 CPU 频繁 cache miss、等待内存,约 35% 的 GC CPU 周期被耗在“等内存”。这在 NUMA 架构或多核大内存机器上尤为明显。

2️⃣ 缺乏分代收集
Go GC 没有分代机制,所有对象一视同仁,这在高分配率场景下显得笨重。Pinterest 工程师曾指出,内存压力一旦增大,GC 就会暴增 CPU 消耗,引发延迟激增。

3️⃣ 频繁 GC 带来的 CPU 占用
Twitch 工程团队曾报告:即便在中小堆内存下(<450 MiB),系统稳态下每秒会触发 8–10 次 GC,每分钟累计 400–600 次,GC 占用约 30% 的 CPU 时间。这直接挤压了业务线程的执行空间。

📊 性能测试:GC 对 Go 程序的影响

我们来看几个实际基准的变化:

  • Go 1.3/1.4(并发 GC 前)
    大堆(10GB+)上的 GC 停顿:以秒计算。
  • Go 1.5(并发 GC 引入后)
    相同条件下,GC 停顿压缩到 <1ms。
    Pasted image 20250504161704
  • Go 1.6–1.8
    最大堆 200GB,GC 停顿控制在 20ms 以下,甚至常态 1ms。

这些进步非常亮眼,但注意:
✅ 延迟控制好了
⚠️ 总耗时和 CPU 消耗依然显著,特别是高负载或高分配场景。

🌿 Green Tea GC:全新优化方案

面对这些问题,Go 官方提出了 Green Tea GC。它的核心优化点是:

从单对象扫描,升级为按 span(内存块)批量扫描。

具体来说:

  • 小对象(≤512B)标记由单个对象粒度提升为 span 粒度。
  • 每个 span 中,只有首次标记的对象会将整个 span 推入扫描队列。
  • GC 扫描阶段批量处理整个 span,极大提升了内存访问局部性。

此外,Green Tea 改进了并行队列管理,采用类似 Go 调度器的工作窃取机制,进一步提高了多核扩展性。

⚡ Green Tea GC 实测表现

从初步基准来看,Green Tea GC 带来了有选择性的性能提升:

Tile38 基准(高扇出树结构)

  • GC 开销降低约 35%
  • 吞吐、延迟、内存使用全面优化

bleve-index 基准(低扇出、频繁变异)

  • 对象分布散乱,内存局部性差
  • Green Tea 与常规 GC 性能相近,有时略低

总结:Green Tea 并非“银弹”,但在内存局部性良好、多核扩展场景下,它展现了明显优势,并为未来 SIMD 加速等硬件优化奠定了基础。

🏁 总结

比较项 当前 Go GC Green Tea GC
标记粒度 单对象 span(批量)
内存局部性 差,随机跳跃 高,同 span 内批量
多核扩展性 受限 改进,采用工作窃取队列
性能提升 已接近低延迟上限 某些场景下 GC 耗时降 35%
应用适用范围 普通场景 内存局部性好、分配密集场景

对于追求极限性能的开发者,Green Tea GC 提供了一个值得关注的新方向。想要试验 Green Tea,可以在 Go 1.25+ 开启实验标志体验。

📝 参考资料


  • 本文长期链接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。