本文永久链接 – https://tonybai.com/2025/05/22/go-mod-ignore-directive 大家好,我是Tony Bai。 在现代软件开发中,项目往往包含多种语言和技术栈。例如,一个典型的 Web 应用可能同时包含 Go 后端代码、JavaScript/TypeScript 前端代码(及其庞大的 node_modules 依赖目录)、由 Bazel 等构建系统生成的中间目录,以及其他各种配置文件和资源文件。 对于这类项目,Go 开发者经常面临以下挑战: 工具执行缓慢: 当使用 ./… 通配符执行 go list, go test, go vet 等命令时,Go 工具会遍历项目下的所有目录,包括那些与 Go 无关但文件数量巨大的目录(如 node_modules 可能包含数十万文件)。这会导致命令执行时间远超预期。 gopls 资源消耗过高: Go 语言服务器 gopls 在分析项目时,也可能因扫描这些无关目录而消耗大量 CPU 和内存资源,影响 IDE 的响应速度和开发体验。 go mod tidy 行为困扰: 如果被忽略的目录中意外包含了 Go 文件(例如某些 npm 包中携带的示例 Go 代码),go mod [...]
本文永久链接 – https://tonybai.com/2025/05/22/go-sbom-practice 大家好,我是Tony Bai。 近年来,软件供应链安全事件频发,从 SolarWinds 到 Log4Shell,每一次都给业界敲响了警钟。在这样的背景下,软件物料清单 (SBOM, Software Bill of Materials) 的重要性日益凸显。无论是甲方爸爸的硬性要求(尤其是在2B软件交付和白盒交付场景),还是我们自身对软件透明度和安全性的追求,SBOM 都已成为现代软件开发不可或缺的一环。 那么,SBOM 究竟是什么?它为何如此重要?市面上有哪些主流的 SBOM 标准?我们又该如何为自己的 Go 项目(当然,也适用于 Java、JS 等其他语言项目)生成和使用 SBOM 呢? 今天,我们就来一起深入探讨这些问题,为你揭开 SBOM 的神秘面纱。 SBOM:你的软件“配料表”,为何如此重要? 想象一下,我们购买食品时会关注配料表,了解其成分、产地和营养信息。SBOM 之于软件,就如同食品的配料表。它是一份正式的、结构化的清单,详细列出了构成某个软件产品的所有组件及其依赖关系。 SBOM 的核心价值在于提升软件供应链的透明度和可管理性,从而增强安全性: 透明度与可追溯性: 清晰展示软件由哪些“零件”(开源库、第三方组件、内部模块等)组装而成,包括直接依赖和传递依赖,让软件的构成不再是“黑盒”。 高效的漏洞管理: 当某个组件爆出新的安全漏洞时,通过 SBOM 可以快速定位所有受影响的软件产品,及时采取修复或缓解措施,大大缩短应急响应时间。 许可证合规性审计: 准确识别所有组件的开源许可证类型,确保符合合规要求,避免潜在的法律风险。 供应链风险评估: 了解组件的来源、版本、维护状态等信息,有助于评估整个软件供应链的潜在风险。 提升软件质量与可信度: 向客户和合作伙伴提供 SBOM,能够证明你对软件安全和质量的重视,建立信任。 可以说,SBOM 是构筑现代软件供应链安全防线的基石。 SBOM 标准巡礼:SPDX、CycloneDX、SWID 与 DSDX 要让 SBOM [...]
本文永久链接 – https://tonybai.com/2025/05/21/go-crypto-audit 大家好,我是 Tony Bai。 信息安全是我们数字时代的基石。对于 Go 语言而言,其标准库中强大的 crypto 系列包一直是开发者构建安全应用的重要依赖。近日,Go 官方博客发布了一篇重要文章,详细介绍了一次由独立安全公司 Trail of Bits 对 Go核心密码学包进行的安全审计结果。这次审计不仅再次印证了 Go 在密码学领域的严谨投入,也揭示了 Go 在后量子密码学 (PQC) 和未来密码学 API 发展上的清晰规划。 好消息是:审计结果非常积极! 仅发现一个低风险问题(已在 Go 1.25 开发分支修复,且涉及的是非默认启用、Google 内部使用的 Go+BoringCrypto 集成)和少量建议性信息。这充分肯定了 Go 团队在密码学库开发中对安全性的高度重视和卓越实践。 在这篇文章中,我们就来介绍这一对Go密码学领域具有里程碑意义的事件。 安全审计的范围与 Go 的密码学设计原则 Trail of Bits 的审计范围广泛,涵盖了 Go 标准库中核心的加密组件,这些组件同时也是新的原生FIPS 140-3模块的验证部分。具体包括: 密钥交换: ECDH 和后量子密码的 ML-KEM (如 crypto/mlkem 包)。 数字签名: ECDSA, [...]
本文永久链接 – https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go 大家好,我是 Tony Bai。 在我们享受数字时代便利的同时,信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑,正面临着来自未来的一个巨大挑战——量子计算机。一旦实用化的大规模量子计算机问世,我们当前广泛依赖的许多经典密码体系(如 RSA、椭圆曲线密码 ECC)可能在瞬间土崩瓦解。 这不是科幻电影,而是密码学界和全球科技巨头都在严肃对待的现实威胁。正因如此,“后量子密码学” (Post-Quantum Cryptography, 以下简称PQC) 应运而生,旨在研发能够抵御量子计算机攻击的新一代密码算法。 作为 Go 开发者,我们或许觉得量子计算机还很遥远,但“现在记录数据,未来量子破解”的风险已然存在。更重要的是,Go 语言作为一门以简洁、高效和安全著称的现代编程语言,其核心团队早已在为这个“后量子时代”积极布局。随着 Go 1.24 的发布,这一布局取得了实质性的进展:备受期待的 crypto/mlkem 包正式加入标准库! 那么,PQC 究竟是什么?crypto/mlkem 包为我们带来了什么?Go 语言在 PQC 的浪潮中又将扮演怎样的角色?今天,就让我们一起“未雨绸缪”,深入了解 PQC 及其在 Go 中的最新进展。 量子风暴将至:为何我们需要 PQC? 想象一下,你用 RSA 加密了公司的核心商业机密,或者用 ECDSA 签名了重要的合同。这些操作的安全性,都依赖于经典计算机难以在有效时间内解决某些数学难题(如大数分解、离散对数)。 然而,量子计算机一旦足够强大,Shor 算法就能在多项式时间内攻破这些难题。这意味着: 加密通讯不再私密: HTTPS、VPN 等都可能被破解。 数字签名不再可信: 软件更新、代码签名、身份认证都可能被伪造。 历史数据面临风险: 黑客现在就可以截获并存储加密数据,等待未来用量子计算机解密。对于需要长期保密的医疗记录、金融数据、国家机密等,这无疑是巨大威胁。 这就是我们迫切需要 PQC 的原因:寻找并标准化那些即使是量子计算机也难以破解的新密码算法。 PQC 的曙光:NIST 标准化与主流算法 [...]
本文永久链接 – https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal 大家好,我是Tony Bai。 在追求极致性能的道路上,Go 语言凭借其简洁的并发模型和高效的调度器,赢得了众多开发者的青睐。然而,随着现代服务器 CPU核心数量的不断攀升,一些我们曾经习以为常的“快速”操作,在高并发、多核环境下,也逐渐显露出其性能瓶颈。其中,原子操作 (atomic operations) 的扩展性问题,以及标准库中一些依赖原子操作的并发原语(如 sync.RWMutex)的性能表现,成为了社区热议的焦点。 最近,fasthttp 的作者及 VictoriaMetrics 数据库的联合创始人 Aliaksandr Valiakin (valyala) 在 X.com 上的一番“叹息”,更是将原子计数器的扩展性问题推向了前台: Valyala 指出:“基于原子操作的计数器更新性能在多 CPU 核心上无法扩展,因为每个 CPU 核心在增量操作期间都需要从慢速内存中原子加载实际的计数器值。因此,实际性能受限于内存延迟(约 15ns,即每秒 6 千万次增量)。通过使用可缓存于 CPU L1 缓存的 per-CPU 计数器,可以将单 CPU 核心性能提升至每秒数十亿次增量。遗憾的是,Go 语言本身并未提供高效处理 per-CPU 数据的函数。” 这番话点出了一个残酷的现实:即使是看似轻量级的原子操作,在多核“混战”中也可能成为性能的阿喀琉斯之踵。那么,这背后的深层原因是什么?Go 社区又在如何探索解决之道呢?今天,我们就来深入剖析这个问题,并解读 Go 项目 issue 中几个重要的相关提案,同时看看社区是如何先行一步尝试解决这类问题的。 原子操作为何在高并发多核下“失速”?sync.RWMutex 的痛点 要理解原子操作的瓶颈,我们需要潜入到 CPU 缓存的微观世界。现代多核 CPU 为了加速内存访问,都配备了多级缓存(L1, L2, [...]
本文永久链接 – https://tonybai.com/2025/05/17/java-at-30 大家好,我是Tony Bai。我的极客时间《Go进阶课》专栏已经上线,欢迎大家点击链接订阅学习,我们一起在Go语言的道路上共同精进! Go语言自开源以来,已走过十多个年头。从最初备受瞩目的“Google语言”,到如今在云原生、微服务领域独当一面,Go 凭借其简洁、高效与强大的并发能力,赢得了全球开发者的青睐,正从一个朝气蓬勃的少年”迈向更加成熟稳健的“壮年”。 然而,“成长的烦恼”也随之而来:生态如何持续繁荣?语言如何在保持核心优势与满足新兴需求之间取得平衡?如何应对一波又一波的技术浪潮冲击? 恰逢 Java 语言诞生 30 周年,The New Stack 对 Java 之父 James Gosling 进行了一次深度访谈。我刚接触 Java 时,它才发布 1.5 版本(Tiger),一晃近 20 年,Java 依然是全球最重要的语言之一。这位编程语言界的“老大哥”和它的创造者,其“长寿秘诀”无疑能为“风华正茂”的 Go 语言带来诸多启示。 Gosling 在访谈中分享了 Java 长盛不衰的关键,我提炼了几点,希望能为Go的未来之路提供一些借鉴与思考。 秘诀一:【解决真实问题,而非追逐时髦】—— Go 的初心与未来挑战 Java 的经验: James Gosling 强调:“Java 从不追求时髦,始终专注于有效解决问题,帮助工程师完成工作。” 这份对实用主义的坚守,是 Java 能够穿越多个技术周期的基石。 Go 的启示与思考: Go 语言的诞生,正是为了解决当时 C++ 开发的复杂性、Python 等脚本语言的性能瓶颈以及多核时代并发编程的困境。它以大道至简的哲学,直击痛点,迅速在云原生、分布式系统等领域找到了自己的核心价值。 如今,Go 已走过开源的第一个十年,生态日渐成熟。面对 [...]
本文永久链接 – https://tonybai.com/2025/05/16/energy-savings-if-abandon-https 大家好,我是Tony Bai。 如今,当我们浏览网页时,地址栏那把绿色的小锁和 HTTPS 前缀已是司空见惯。从网上银行到个人博客,再到每一个SaaS服务,HTTPS/TLS 加密几乎覆盖了互联网的每一个角落。它像一位忠诚的数字保镖,守护着我们在虚拟世界中的数据安全与隐私。 然而,这位保镖并非“免费服务”。HTTPS/TLS 在带来安全的同时,也无可避免地引入了额外的计算和传输开销,直观感受便是连接速度可能略有减慢,传输数据量也略有增加。而且,随着我们对安全的追求永无止境,为了抵御更强大的计算破解能力,加密算法的密钥长度也在不断增加(例如从 RSA 1024位到2048位甚至更高,ECC 曲线的复杂度也在提升),这无疑进一步加剧了这些开销。 那么,今天我们不妨来做一个大胆的,甚至有些“异想天开”的思想实验:如果在一夜之间,全球所有的网站都决定弃用 HTTPS/TLS,回归到“裸奔”的 HTTP 时代,理论上能为我们的地球节省多少电力呢? 重要声明: 这纯粹是一个思想实验,旨在通过一个极端的假设,引发我们对技术成本(特别是能源成本)和安全效益之间平衡的思考。我们绝非鼓吹放弃 HTTPS/TLS,其在现代互联网安全中的基石地位无可替代。 HTTPS 的“能源账单”:开销源自何方? 要估算节省的电量,首先得理解 HTTPS/TLS 的主要开销在哪里。这些开销主要体现在两个方面:计算开销和数据传输开销。 计算开销 (CPU 的额外负担) TLS 握手阶段: 这是计算密集型操作的重灾区。 非对称加密/密钥交换: 如 RSA、Diffie-Hellman 或 ECC (椭圆曲线加密),用于安全地协商后续通信所用的对称密钥。密钥长度的增加,使得这些运算的计算量呈指数级或更高阶的增长。 例如,一个 RSA 2048 位操作的计算量远超 1024 位。 证书验证: 客户端需要验证服务器证书链的有效性,这涉及到一系列的数字签名验证操作,同样消耗 CPU 资源。 对称密钥生成与哈希计算: 用于生成会话密钥、消息认证码 (MAC) 等。 数据传输阶段: 对称加解密: 建立连接后,所有应用数据的传输都需要经过对称加密算法(如 [...]
本文永久链接 – https://tonybai.com/2025/05/16/how-rune-came 大家好,我是Tony Bai。 作为 Gopher,我们每天都在和 rune 打交道。在 Go 语言中,它通常被解释为“一个 Unicode 码点”,官方文档也说引入这个术语是为了“简洁”。但你是否曾好奇,这个略带神秘色彩的词汇,究竟源自何方?仅仅是为了简洁吗? 最近,Connor Taffe的一篇精彩博文以及 Go语言之父 Rob Pike 的亲自确认,为我们揭开了一段跨越三十余年,从 Plan 9 操作系统到 UTF-8 编码诞生,再到 Go 语言的历史传奇。今天,就让我们一起,深入 rune 背后的故事。 一句“简洁”,一段 Plan 9 往事 Connor文章中引用的Adam Pritchard的关于限制字符串长度的文章中提到:“请注意,在 Go 中,Unicode 码点通常被称为‘rune’。(Go 似乎是为了简洁而引入了这个术语。)” 而 Go 官方博客《Strings, bytes, runes, and characters in Go》也说:“‘Code point’有点拗口,所以 Go 引入了一个更短的术语:rune。” 然而,真相远不止于此。Rob Pike 最近在 Bluesky 上澄清(如上图),rune [...]
本文永久链接 – https://tonybai.com/2025/05/15/go-json-v2 大家好,我是Tony Bai。 Go 语言标准库中的 encoding/json 包,无疑是我们日常开发中使用频率最高的包之一。它为 Go 社区服务了十多年,几乎无处不在。但与此同时,它也因一些历史遗留的 API 缺陷、行为不一致以及在某些场景下的性能瓶颈而受到过不少讨论和批评。社区中甚至涌现出像Sonic、go-json、easyjson 等一系列高性能的第三方 JSON 库作为替代。 令人兴奋的是,Go 官方团队终于开始着手对 encoding/json 进行一次意义深远的升级——这就是 encoding/json/v2 的由来。虽然json/v2 尚未正式发布,但其核心代码已经合并到 Go 的开发分支,并可以通过一个实验性特性标志 GOEXPERIMENT=jsonv2 来提前体验! 今天,我就来手把手带大家玩转这个实验性特性,通过官方提供的 gotip 工具,亲自动手体验一下 Go 下一代 JSON 库到底带来了哪些令人期待的改进,特别是在行为正确性和性能方面。 背景回顾:为何需要 json/v2?—— encoding/json (v1) 的“四宗罪” 在深入实践之前,我们有必要回顾一下 encoding/json (v1) 长期以来积累的一些核心痛点。这些痛点也是催生 json/v2 的根本原因。Go 官方的 json/v2 提案(详见 GitHub Issue #71497)将这些缺陷归纳为四大类: 行为缺陷 大小写不敏感的字段名匹配: v1 在反序列化时,JSON [...]
本文永久链接 – https://tonybai.com/2025/05/14/which-go-router-should-you-use 大家好,我是 Tony Bai。 最近,知名 Go 博主 Alex Edwards 更新了他那篇广受欢迎的文章——“Which Go router should I use?”,特别提到了 Go 1.22 版本对标准库 http.ServeMux 的显著增强。这篇文章再次引发了我们对 Go Web 开发中一个经典问题的思考:在选择路由库时,我们应该坚守标准库,还是拥抱功能更丰富的第三方库? 这个问题,其实并不仅仅关乎路由选择,它更触及了 Go 开发哲学中一个核心原则——“标准库优先” (Standard Library First)。今天,我们就以 Go 路由选择为切入点,聊聊这个原则,以及在实践中我们该如何权衡“坚守”与“拓展”。 “标准库优先”的魅力何在? Alex Edwards 在他的文章中旗帜鲜明地提出:“Use the standard library if you can”(如果可以,就用标准库)。这并非空穴来风,而是深深植根于 Go 语言的设计哲学和社区实践。为什么“标准库优先”如此有吸引力? 简洁性与零依赖:最直接的好处就是减少了项目的外部依赖。正如我们在之前讨论Rust 依赖管理时所看到的,过多的依赖会增加项目的复杂性、构建体积和潜在的安全风险。使用标准库,意味着你的 go.mod 文件更干净,项目更轻盈。 稳定性与兼容性:Go 语言以其著名的“Go 1 兼容性承诺”著称。标准库作为 Go 的核心组成部分,其 [...]
本文永久链接 – https://tonybai.com/2025/05/13/goos-none-proposal 大家好,我是Tony Bai。 Go语言凭借其简洁、高效和强大的并发模型,已在云原生和服务器端开发领域占据重要地位。但它的潜力远不止于此。一项备受关注的新提案 (#73608) 再次将目光投向了更底层的领域,建议引入 GOOS=none target。其核心并非简单添加一个操作系统类型,而是试图定义一套连接 Go 运行时与底层硬件/环境的接口,为 Go 语言铺设一条通往裸金属执行、安全固件开发乃至 Unikernel 和特定微控制器场景的桥梁。然而,这套接口能否以及如何实现“标准化”,并融入 Go 的兼容性承诺,成为了社区热议的焦点。 本文就来和大家一起看看这个提案的核心思想、技术细节及其对 Go 语言未来发展的潜在影响。 GOOS=none:定义 Go 与底层硬件的契约 提案的核心是允许 Go 程序在编译时指定 GOOS=none,编译产物将不依赖任何传统 OS 系统调用。所有必要的底层交互——从 CPU 初始化、时钟、随机数生成到基本输出——都将通过一组明确定义的接口委托给开发者提供的特定于硬件的板级支持包 (Board Support Package, BSP) 或应用层代码来实现。这些 BSP 和驱动同样可用 Go 编写。 这套接口的设计基于已成功实践多年的 TamaGo (自行扩展实现GOOS=tamago) 项目经验。提案者也已将接口定义文档化,方便社区查阅和讨论 (goos-none-proposal Repo, pkg.go.dev)。 下面是提案者粗略总结的关键运行时交互接口列表(需 BSP 或应用实现): cpuinit (汇编实现): 最早期的 CPU [...]
本文永久链接 – https://tonybai.com/2025/05/13/go-prefer-less-framework 大家好,我是 Tony Bai。 Go 语言自诞生以来,就以其简洁、高效和强大的并发模型赢得了全球开发者的青睐。它的设计者们,包括 Rob Pike、Ken Thompson 这些计算机界的巨匠,在创造 Go 的时候,秉持了一种鲜明的风格:“少即是多” (Less is More)。这不仅体现在其精简的语法和关键字上,更深刻地影响了 Go 社区对于“框架” (Frameworks) 的普遍态度。 虽然 Go 官方从未明确宣称“轻框架或无框架”是其核心哲学,但从其设计选择——如强大的标准库、鼓励组合优于继承——以及社区早期的主流声音来看,Go 显著地倾向于“轻框架”,或者说“反大型、侵入式框架”。 但这种在语言层面推崇的“轻盈”与“自由”,在实际的团队协作和大型项目开发中,究竟是解放生产力的“馈赠”,还是悄然套上了一层限制效率的“无形枷锁”?今天,我们就来探讨一下 Go 社区这种独特的“轻框架”理念。 “轻框架”的初心:拥抱简洁、掌控与标准库的力量 Go 社区对“轻框架”的偏爱,并非空穴来风,而是源于对传统大型框架某些弊端的回避,以及对 Go 自身优势的充分自信: 对“重框架”的反思: Go 的设计者们深谙大型框架(如 Java Spring, Ruby on Rails 等早期版本)在提供便利的同时,也可能带来学习曲线陡峭、过度设计、灵活性受限、性能开销以及难以捉摸的“魔法”等问题。Go 倾向于让开发者更接近底层,更清晰地理解代码的执行路径。 强大的标准库 “自带电池”: 这是 Go “轻框架”理念的底气所在。Go 标准库异常强大且全面,覆盖了网络、HTTP、JSON/XML 处理、加密、并发原语、测试等核心功能。许多在其他语言中需要依赖框架才能便捷实现的功能,Go 标准库直接提供,鼓励开发者首先“向内求”。 组合优于继承,接口驱动设计: Go 语言本身的设计哲学鼓励通过组合小而专注的组件来构建复杂的系统,并通过接口实现解耦和多态。这种范式使得代码更易于理解、测试和维护,自然降低了对庞大、层级复杂的框架的需求。 赋予开发者掌控权: [...]
本文永久链接 – https://tonybai.com/2025/05/12/go-advanced-course 大家好,我是Tony Bai。 今天,怀着一丝激动和期待,我想向大家宣布一个酝酿已久的好消息:我的新专栏“TonyBai · Go 语言进阶课” 终于在极客时间正式上架了! 这门课程的诞生,其实有一段不短的故事。它并非一时兴起,而是源于我对 Go 语言多年实践的沉淀、对 Gopher 们进阶痛点的洞察,以及一份希望能帮助更多开发者突破瓶颈、实现精通的心愿。 缘起:从 GopherChina 的线下训练营开始 故事的起点,要追溯到 GopherChina 2023 大会前夕。当时,我应邀开设了一期名为“Go 高级工程师必修课”的线下训练营。至今还清晰记得,在滴滴的一个会议室里,我与一群对 Go 语言充满热忱的开发者们,共同探讨、深入剖析了 Go 进阶之路上的种种挑战与关键技能。 那次线下课程的反馈非常积极,也让我深刻感受到,许多 Gopher 在掌握了 Go 的基础之后,普遍面临着“如何从熟练到精通”的困惑。他们渴望写出更优雅、更高性能的代码,希望提升复杂项目的设计能力,也期盼着能掌握更硬核的工程实践经验。 同年,我还临危受命,在 GopherChina 2023 上加了一场 “The State Of Go” 的演讲,与大家分享了我对 Go 语言发展趋势的观察与思考。这些经历,都让我更加坚信,系统性地梳理和分享 Go 语言的进阶知识,是非常有价值且必要的。 打磨:从线下到线上,不变的是匠心 将线下课程的精华沉淀下来,打磨成一门更普惠、更系统的线上专栏,这个想法在 2024 年就已萌生。但由于种种原因,特别是档期的冲突,这个计划暂时搁置了。 直到 2025 年,我与极客时间的老师们再次携手,投入了大量心血,对课程内容进行了反复打磨和精心编排。我们不仅希望传递知识,更希望启发思考,帮助大家建立起真正的“Go 语言设计思维和工程思维”。 正如我在专栏开篇词中提到的,如果你也正面临这些困惑: 感觉到了瓶颈? [...]
本文永久链接 – https://tonybai.com/2025/05/11/ian-lance-taylor-leave-go 大家好,我是Tony Bai。 今天,Go 语言社区传来一个令人瞩目又略感“悲伤”的消息:Go核心团队的元老级人物 Ian Lance Taylor在为 Google 效力 19 年后,宣布离开。对于许多 Gopher 来说,Ian Taylor 的名字与 Go 语言的早期发展、GCC Go 前端 gccgo 的诞生,以及历时多年最终在 Go 1.18 实现的泛型设计紧密相连。 他的离开,不仅仅是一位资深工程师的职业变动,更像是一个时代的注脚,引发我们对 Go 语言发展阶段、团队演进以及开源项目生命力的深层思考。我们是否可以说,Go 语言正在步入一个“后元老时代”?这又意味着什么?在这篇文章中,我们就来简单聊聊。 一位“老兵”的自白与 Go 的变迁 在 Ian Taylor 的告别博文《Leaving Google》中,他回顾了自己从 2008 年加入 Go 团队(几乎与 Russ Cox 同期)至今的历程。他对自己角色的定位是:“追踪我所能追踪的关于项目的一切,并寻找需要帮助的领域。” 从为 GCC 添加 Go 前端以确保语言规范的清晰,到为 Google 内部构建系统和 SWIG 添加 [...]
本文永久链接 – https://tonybai.com/2025/05/11/deep-into-pkg-go-dev 大家好,我是Tony Bai。 对于 Go 开发者而言,pkg.go.dev 不仅仅是一个查找包文档的网站,更是展示和推广自己辛勤成果的重要平台。理解其运作机制、掌握其使用技巧,并遵循其倡导的最佳实践,能显著提升你的 Go 包的专业度、可见性和社区友好度。本文将基于官方信息,和大家一起挖掘一下 pkg.go.dev 的宝藏知识,包括核心功能和关键建议。 让你的包“入住”pkg.go.dev pkg.go.dev 的数据来源于官方的 Go Module Proxy (proxy.golang.org),并通过 Go Module Index (index.golang.org) 定期监测新的包版本。如果你的包尚未被收录,可以通过以下任一方式主动添加: 直接请求收录: 访问你的包在 pkg.go.dev 上对应的 URL (即使它显示“Not Found”),例如 https://pkg.go.dev/example.com/my/module,然后点击页面上的 “Request” 按钮(如下图所示)。 触发 Proxy 请求: 向 proxy.golang.org 发送一个符合 Go Module Proxy 协议 的请求。例如,请求特定版本的 .info 文件: $curl https://proxy.golang.org/example.com/my/module/@v/v1.0.0.info 使用 go get 命令: 通过 [...]
本文永久链接 – https://tonybai.com/2025/05/10/rust-dependencies-scare-me 大家好,我是Tony Bai。 在现代软件开发中,高效的包管理系统和繁荣的开源生态极大地加速了我们的开发进程。Rust语言的Cargo及其crates.io生态便是其中的佼佼者,为开发者带来了前所未有的便捷。然而,这种便捷性是否也伴随着一些潜在的“代价”? 近期,一位名叫Vincent的国外Rust开发者在其博客文章《Rust Dependencies scare Me》中,就真诚地抒发了他对Rust依赖管理的深切忧虑。这篇博文在Hacker News等社区引发了热烈讨论,其指出的问题——从依赖的维护性到惊人的代码体积——或许也值得我们每一位使用现代包管理系统的开发者深思。 今天,我们就来一起解读Vincent的这篇文章,看看他遇到了哪些具体问题,并结合社区的智慧与我们的经验,探讨这些现象背后的启示。 Cargo的魅力:作者眼中的“美好一面” 在这位开发者看来,Cargo无疑是Rust生态的巨大优势。他强调,Cargo极大地提升了生产力,开发者无需像使用CMake(多用于C++项目)那样手动管理和链接文件。这使得在不同架构和操作系统(如他的M1 MacBook和Debian桌面)之间切换变得异常顺畅。 他坦言,在大部分情况下,Cargo让他几乎可以不必过多思考包管理本身,从而能更专注于核心代码的编写。这种“无感”的便捷体验,与上世纪80年代开发者需要为节省软盘空间而精打细算地“手动挑选和集成库代码”形成了鲜明对比,无疑是现代包管理系统追求的目标,也是Rust吸引开发者的重要原因之一。 当便捷遭遇“意外”:dotenv引发的警惕 然而,文章作者也指出,正是这种“不用思考”的便捷,可能让人变得“草率”。 他在一个生产项目中使用了许多Rust开发者都用过的dotenv库(用于加载.env文件)。项目平稳运行数周后,他偶然发现一则Rust安全通告指出,他所使用的dotenv版本已无人维护,并推荐了替代方案dotenvy。 这个小插曲让他开始反思:这个依赖真的必不可少吗?他尝试后发现,仅仅35行代码便实现了他所需的核心功能。他由此提出一个普遍性的问题:当依赖项(尤其是那些看似“微不足道”的)不再维护或出现安全漏洞时,我们该如何应对?那些我们真正“需要”的复杂依赖,又隐藏着哪些风险?这不仅仅是功能问题,更关乎依赖的信任链和维护者的责任。 百万行代码的“冲击波”:一个“小项目”的真实体积 Vincent的忧虑不止于此。他以一个自认为“微不足道”的Web服务项目为例——该项目使用广受好评的异步运行时tokio和Web框架axum,主要功能是处理请求、解压文件和记录日志。 当他尝试使用cargo vendor将所有依赖项本地化时,并用代码行数统计工具tokei进行分析,结果令他大吃一惊:总代码行数高达360万行!而他自己编写的业务代码仅有约1000行。 他将此与Linux内核的2780万行代码进行对比,发现他这个“小项目”的依赖代码量已接近后者的七分之一。他不禁发问:如何审计如此庞大的代码量?我们引入的重量级依赖,其绝大部分功能是否是我们项目真正需要的? Vincent的经历并非个案。Hacker News社区的讨论中,有开发者(如kion)指出,现代软件开发中‘库叠库’的现象十分普遍,每一层依赖可能只用到其功能的冰山一角,但最终却可能导致简单的应用膨胀到数百MB。更有甚者(如jiggawatts)通过计算发现,仅三层依赖的层层叠加,就可能导致最终应用中88%的代码是“死代码”或从未被真实业务逻辑触及的“幽灵代码”。 Rust依赖困境的“求解”:作者的困惑与社区的多元声音 面对如此庞大的依赖代码和潜在风险,该博主坦诚自己“没有答案”。他提及了社区中一些常见的讨论方向,例如扩展标准库的利弊、开发者自身的责任以及业界大厂的实践等。 Hacker News社区的讨论进一步丰富了这些思考: 编译时优化是否足够? 许多评论提到了链接时优化(LTO)、Tree Shaking等技术在剔除未使用代码方面的作用。Rust基于LLVM的优化确实能在这方面做出贡献。然而,正如一些评论者指出的,这些优化并非“银弹”,对于动态分发或包含大量可选编译特性的复杂依赖,完美剥离未使用部分仍充满挑战。 更细粒度的依赖控制: Rust的features机制为选择性编译提供了可能,但社区也在探索更根本的解决方案。有开发者甚至提出了“超细粒度符号和依赖”的设想,即每个语言构造都声明其精确依赖,按需构建最小代码集,尽管这在实现上极具颠覆性。 工具链的局限与期望: Vincent指出Cargo目前难以精确追踪最终编译产物包含的代码。社区也期待更强大的工具来分析依赖树、识别冗余、评估安全风险。 最终,文章作者将问题抛给了社区:我们应该怎么办? 我们的启示:从Rust的“依赖之忧”看现代软件供应链 Vincent的博文真实地反映了现代软件开发中普遍存在的“依赖困境”——我们享受着开源生态带来的便利,但也面临着供应链安全、代码膨胀、维护性等一系列挑战。 从他的分享和社区的热烈讨论中,我们可以得到以下几点启示: 审慎评估依赖,警惕“依赖膨胀”的陷阱,拥抱适度“复制”: “不要为了碟醋包饺子”。在引入任何依赖前,都应评估其必要性、维护状态、社区活跃度以及潜在的安全风险。正如Go社区所倡导的“A little copying is better than a little dependency. (一点复制代码胜过一点点依赖)”,有时为了避免引入一个庞大或不稳定的依赖,适度复制代码,或者自己实现一个轻量级的核心功能,可能是更明智的选择。Go语言设计者之一的 Rob Pike [...]
本文永久链接 – https://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice 大家好,我是 Tony Bai。 身处全球化的软件开发浪潮中,GitHub早已成为我们协作、学习、贡献的“宇宙中心”。但对于我们许多非英语母语的开发者来说,它既是机遇之地,有时也是“望而却步”的挑战场。 你是否也曾有过这样的经历? 面对一个棘手的 Bug,想在golang/go项目的 Issue 下寻求帮助,却因为担心自己的“蹩脚”英文描述不清,反复修改,最终默默关掉了页面? 看到一个热门讨论,你明明有绝佳的改进建议或独到的反驳观点,却因为组织不好地道的英文表达,只能眼睁睁看着讨论走向自己不希望的方向,最后无奈地打出“+1”? 或者,因为语言的障碍,你觉得自己与那些国际顶尖的Go开发者之间隔了一层无形的墙,错失了许多宝贵的交流与学习机会? 如果这些场景让你感同身受,那么今天的文章,就是为你量身打造的。如今,在ChatGPT、DeepSeek、Google Gemini等AI工具的辅助下,我们可以更自信地表达,但理解Github上的沟通的“套路”和文化依然重要。 通过对大量顶级 Go 开源项目(如Go官方仓库、Kubernetes、Docker/Moby、Prometheus等)的 Issues 和 Pull Requests 中社区互动的观察分析,以及AI的辅助整理,并结合这些项目通常倡导的沟通准则,我粗略整理出了一套在 GitHub Issues 中进行高效英语沟通的实用“模式”与“心法”。 本文旨在为你提供这套方法,希望能帮你打破沟通壁垒,自信地参与到全球 Go 开源社区中,让语言不再是你贡献智慧的拦路虎! GitHub Issue沟通的“潜规则”与礼仪 在我们深入学习具体的沟通“招式”之前,了解战场规则是制胜的前提。GitHub Issues 作为一个全球开发者协作的广场,自然也有一套约定俗成的“潜规则”和基本礼仪。Github上的有效沟通是建立在这些规则和礼仪之上的。掌握了这些,你的每一次发言才能更得体、更高效,也更容易获得他人的尊重和积极回应。记住,高效的沟通才是开源协作的基石。 协作至上 (Collaborative) 开源的本质是团队协作。你的每一个评论、每一个 Issue,都应服务于项目的整体目标,而非仅仅表达个人。 简洁明了 (Concise & Clear) 维护者和贡献者的时间都非常宝贵。用最少的文字清晰地表达你的观点至关重要。避免冗长和含糊不清。 建设为本 (Constructive) 即使你持有不同意见,甚至需要反驳他人,也务必保持建设性的态度和尊重的语气。对事不对人。 技术导向 (Technically Focused) 交流应始终围绕技术问题展开,避免无关的个人情绪或评论。 心中有了这些“潜规则”作为行事准则,我们就可以更有底气地进入实战演练了。面对 GitHub Issues 中形形色色的沟通场景——从报告一个恼人的 [...]
本文永久链接 – https://tonybai.com/2025/05/08/go-dwarf5 大家好,我是Tony Bai。 对于许多Go开发者来说,调试信息的格式可能是一个相对底层的细节。然而,这个细节却对编译速度、最终可执行文件的大小以及调试体验有着深远的影响。经过长达六年的讨论、等待生态成熟和密集的开发工作,Go 语言工具链终于在主干分支(预计将包含在 Go 1.25 中)默认启用了 DWARF version 5 作为其调试信息的标准格式(Issue #26379)。这一看似“幕后”的变更,实则为 Go 开发者带来了切实的链接速度提升和可执行文件体积的优化。在这篇文章中,我们就来对DWARF5落地Go这件事儿做一个简单的解读。 为何需要升级到 DWARF 5?旧格式的痛点 DWARF (Debugging With Attributed Record Formats) 是类 Unix 系统上广泛使用的调试信息标准。Go 之前使用的 DWARF 版本(主要是 v2 和 v4)虽然成熟,但在现代软件开发实践中暴露出一些不足: 大量的重定位 (Relocations): 旧版 DWARF 格式通常包含大量需要链接器处理的地址重定位信息。根据 2018 年的初步分析(by aclements),在当时的 go 二进制文件中,高达 49% 的重定位条目都源于 DWARF 数据。这显著增加了链接器的工作负担,拖慢了构建速度,尤其是对于大型项目。 冗长的位置和范围列表 (Location/Range Lists): 用于描述变量生命周期和代码范围的 .debug_loc 和 [...]
本文永久链接 – https://tonybai.com/2025/05/07/debug-with-diff-cover 大家好,我是Tony Bai。 调试,尤其是调试并非自己编写的代码,往往是软件开发中最耗时的环节之一。面对一个失败的测试用例和庞大的代码库,如何快速有效地缩小问题范围?Go团队的前技术负责人 Russ Cox 近期分享了一个虽然古老但极其有效的调试技术——差异化覆盖率 (Differential Coverage)。该技术通过比较成功和失败测试用例的代码覆盖率,巧妙地“高亮”出最可能包含Bug的代码区域,从而显著加速调试进程。 在这篇文章中,我们来看一下Russ Cox的这个“古老绝技”,并用一个实际的示例复现一下这个方法的有效性。 核心思想:寻找失败路径上的“独特足迹” 代码覆盖率通常用于衡量测试的完备性,告诉我们哪些代码行在测试运行期间被执行了。而差异化覆盖率则利用这一信息进行反向推理: 假设: 如果一段代码仅在失败的测试用例中被执行,而在其他成功的用例中未被执行,那么这段代码很可能与导致失败的 Bug 相关。 反之,如果一段代码在成功的测试中执行了,但在失败的测试中未执行,那么这段代码本身大概率是“无辜”的,尽管它被跳过的原因(控制流的变化)可能提供有用的线索。 如何实践差异化覆盖率? Russ Cox 通过一个向 math/big 包注入 Bug 的例子,演示了如何应用该技术: 假设 go test 失败,且失败的测试是 TestAddSub: $ go test --- FAIL: TestAddSub (0.00s) int_test.go:2020: addSub(...) = -0x0, ..., want 0x0, ... FAIL exit status 1 FAIL math/big 7.528s [...]
本文永久链接 – https://tonybai.com/2025/05/06/cheating-the-reaper-in-go 大家好,我是Tony Bai。 Go语言以其强大的垃圾回收 (GC) 机制解放了我们这些 Gopher 的心智,让我们能更专注于业务逻辑而非繁琐的内存管理。但你有没有想过,在 Go 这个看似由 GC “统治”的世界里,是否也能体验一把“手动管理”内存带来的极致性能?甚至,能否与 GC “斗智斗勇”,让它为我们所用? 事实上,Go 官方也曾进行过类似的探索。 他们尝试在标准库中加入一个arena包,提供一种基于区域 (Region-based) 的内存管理机制。测试表明,这种方式确实能在特定场景下通过更早的内存复用和减少 GC 压力带来显著的性能提升。然而,这个官方的 Arena 提案最终被无限期搁置了。原因在于,Arena 这种手动内存管理机制与 Go 语言现有的大部分特性和标准库组合得很差 (compose poorly)。 官方的尝试尚且受阻,那么个人开发者在 Go 中玩转手动内存管理又会面临怎样的挑战呢?最近,一篇名为 “Cheating the Reaper in Go” (在 Go 中欺骗死神/收割者) 的文章在技术圈引起了不小的关注。作者 mcyoung 以其深厚的底层功底,展示了如何利用unsafe包和对 Go GC 内部运作机制的深刻理解,构建了一个非官方的、实验性的高性能内存分配器——Arena。 这篇文章的精彩之处不仅在于其最终实现的性能提升,更在于它揭示了在 Go 中进行底层内存操作的可能性、挑战以及作者与 GC “共舞”的巧妙思路。需要强调的是,本文的目的并非提供一个生产可用的 Arena 实现(官方尚且搁置,其难度可见一斑),而是希望通过解读作者这次与 GC [...]
本文永久链接 – https://tonybai.com/2025/05/03/go-green-tea-garbage-collector 大家好,我是Tony Bai。 随着 CPU 核心数量的激增和内存访问速度日益成为瓶颈,现代计算系统对内存局部性(Spatial & Temporal Locality)和拓扑感知(Topology-awareness)提出了更高的要求。然而,传统的垃圾收集(GC)算法,包括 Go 当前使用的并行三色标记清除法,往往与这些趋势背道而驰。近期,Go 团队技术负责人Austin Clements公布了一项名为 “Green Tea” (绿茶) ** 的实验性垃圾收集器设计(Issue #73581),旨在通过一种内存感知 (memory-aware)** 的新方法,显著改善 GC 过程中的内存访问模式,降低 CPU 开销,尤其是在多核和 NUMA 架构下。该特性计划作为 Go 1.25 的一个可选实验加入,开发者将有机会提前体验。 在这篇文章中,我就来简要介绍一下这个新GC的设计、原型实现和当前状态。 当前 GC 的挑战:内存墙与低效扫描 Go 当前的 GC 算法本质上是一个图遍历过程,堆对象是节点,指针是边。这种“图泛洪”式的扫描在并发标记时,会频繁地在内存地址空间中跳跃,导致: 空间局部性差: 处理逻辑上相邻的对象时,物理内存访问可能跨越很大范围。 时间局部性差: 对同一内存区域的重复访问分散在整个 GC 周期中,未能有效利用缓存。 缺乏拓扑感知: 无法根据 CPU 核心与内存的物理距离进行优化。 其结果是,GC 的核心环节——扫描循环 (scan loop)——平均消耗了 GC [...]
本文永久链接 – https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling 大家好,我是Tony Bai。 使用Go语言有些年头的开发者,大多对其错误处理机制有着复杂的情感。一方面,我们认同 Rob Pike 所倡导的“错误即值 (Errors are values)”的核心哲学——错误不是需要特殊通道(如异常)处理的“二等公民”,它们是普通的值,可以传递、检查,甚至被编程。这赋予了错误处理极大的灵活性和明确性。 但另一方面,我们也不得不承认Go的错误处理有时可能相当冗长。标志性的if err != nil代码块几乎遍布在Go代码的各个角落,占据了相当大的代码比例,这常常成为社区讨论的热点。 有趣的是,近期另一门备受关注的系统编程语言 Zig,也采用了“错误即值”的哲学,但其实现方式却与Go大相径庭。 近期自称是Zig新手的packagemain.tech博主在他的一期视频中也分享了自己敏锐地观察到的Zig和Go在设计哲学上的相似性(都追求简洁、快速上手)以及在错误处理实现上的显著差异。 今天,我们就基于这位开发者的分享,来一场 Go 与 Zig 错误处理的对比,看看同一种哲学思想,是如何在两种语言中开出不同但各有千秋的花朵。 Go 的错误处理:接口、显式检查与可编程的值 我们先快速回顾下 Go 的错误处理方式,这也是大家非常熟悉的: error 接口 Go中的错误本质上是实现了Error() string方法的任何类型。这是一个极其简单但强大的约定。 // $GOROOT/src/builtin/builtin.go // The error built-in interface type is the conventional interface for // representing an error condition, with the nil value [...]
本文永久链接 – https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling 大家好,我是Tony Bai。 使用Go语言有些年头的开发者,大多对其错误处理机制有着复杂的情感。一方面,我们认同 Rob Pike 所倡导的“错误即值 (Errors are values)”的核心哲学——错误不是需要特殊通道(如异常)处理的“二等公民”,它们是普通的值,可以传递、检查,甚至被编程。这赋予了错误处理极大的灵活性和明确性。 但另一方面,我们也不得不承认Go的错误处理有时可能相当冗长。标志性的if err != nil代码块几乎遍布在Go代码的各个角落,占据了相当大的代码比例,这常常成为社区讨论的热点。 有趣的是,近期另一门备受关注的系统编程语言 Zig,也采用了“错误即值”的哲学,但其实现方式却与Go大相径庭。 近期自称是Zig新手的packagemain.tech博主在他的一期视频中也分享了自己敏锐地观察到的Zig和Go在设计哲学上的相似性(都追求简洁、快速上手)以及在错误处理实现上的显著差异。 今天,我们就基于这位开发者的分享,来一场 Go 与 Zig 错误处理的对比,看看同一种哲学思想,是如何在两种语言中开出不同但各有千秋的花朵。 Go 的错误处理:接口、显式检查与可编程的值 我们先快速回顾下 Go 的错误处理方式,这也是大家非常熟悉的: error 接口 Go中的错误本质上是实现了Error() string方法的任何类型。这是一个极其简单但强大的约定。 // $GOROOT/src/builtin/builtin.go // The error built-in interface type is the conventional interface for // representing an error condition, with the nil value [...]
本文永久链接 – https://tonybai.com/2025/04/29/hard-truths-before-switching-to-go 大家好,我是Tony Bai。 Go 语言近年来势头强劲,凭借其简洁、高效、出色的并发能力和工具链,吸引了大量开发者投身其中。甚至连TypeScript 团队也宣布将其编译器和工具集迁移到 Go,以提升性能。这无疑是对 Go 的巨大认可。 然而,正如一位拥有超过 15 年经验(主要使用 Java/Kotlin/TypeScript)、并在过去一年深度使用 Go 的开发者(以下简称“视频作者”)在其分享的油管视频中提到的那样,尽管 Go 非常出色,但光环之下并非没有阴影。在投入实际项目,特别是构建一些非同小可的东西之后,会发现 Go 的一些设计决策有利有弊,有些“简洁”的背后隐藏着需要注意的“真相”。 这位作者认为,计划学习或在下一个项目中使用 Go 的开发者,都应该了解这些潜在的“硬伤”或权衡。以下是他总结的、在转向 Go 之前你需要真正了解的五件事,主要转述自他的分享: 真相一:简洁的表象与表达力的代价 Go 最大的卖点之一是它的简洁性。表面上看,它确实如此。但视频作者认为,一旦你超越了教程的范畴,就会发现这种简洁很多时候是以牺牲表达力为代价的。 隐藏而非消除复杂性? 比如,Go 有 while 循环的功能,却没有 while 关键字,你需要用 for 循环省略条件来实现。 可见性(公有/私有)由首字母大小写决定,而非明确的 public/private 关键字。这虽然简洁,但在重构时容易忽略,更改大小写可能在没有编译器警告的情况下破坏 API。 枚举(Enum)也没有原生支持,而是通过 const 和 iota 的变通方法实现。 在作者看来,Go 似乎不惜一切代价追求简单和极简的外观,有时这意味着隐藏了复杂性,而不是真正消除了它。 真相二:多返回值并非“一等公民” 从函数返回多个值是 Go 的一个特色,尤其在错误处理上,(value, error) 模式初看很优雅,没有异常、没有 [...]
本文永久链接 – https://tonybai.com/2025/04/28/five-cache-strategies 大家好,我是Tony Bai。 在构建高性能、高可用的后端服务时,缓存几乎是绕不开的话题。无论是为了加速数据访问,还是为了减轻数据库等主数据源的压力,缓存都扮演着至关重要的角色。对于我们 Go 开发者来说,选择并正确地实施缓存策略,是提升应用性能的关键技能之一。 目前业界主流的缓存策略有多种,每种都有其独特的适用场景和优缺点。今天,我们就来探讨其中五种最常见也是最核心的缓存策略:Cache-Aside、Read-Through、Write-Through、Write-Behind (Write-Back) 和Write-Around,并结合Go语言的特点和示例(使用内存缓存和SQLite),帮助大家在实际项目中做出明智的选择。 0. 准备工作:示例代码环境与结构 为了清晰地演示这些策略,本文的示例代码采用了模块化的结构,将共享的模型、缓存接口、数据库接口以及每种策略的实现分别放在不同的包中。我们将使用Go语言,配合一个简单的内存缓存(带 TTL 功能)和一个 SQLite 数据库作为持久化存储。 示例项目的结构如下: $tree -F ./go-cache-strategy ./go-cache-strategy ├── go.mod ├── go.sum ├── internal/ │ ├── cache/ │ │ └── cache.go │ ├── database/ │ │ └── database.go │ └── models/ │ └── models.go ├── main.go └── strategy/ ├── cacheaside/ [...]
本文永久链接 – https://tonybai.com/2025/04/28/go-ecosystem 大家好,我是Tony Bai。 最近,Go社区里的一则消息引发了不少关注和讨论:广受欢迎的 go-yaml 库作者 Gustavo Niemeyer 宣布将项目正式标记为“归档(archived)”。这不仅让很多依赖该库的项目需要考虑迁移,也恰好触动了许多 Gopher 心中的一根弦。 就像我的知识星球“Go & AI 精进营”里的星友 Howe 所提出的那个精彩问题一样: “白老师…其实会发现,很多 Go 开源工具是没有持续更新维护的好像,不像 Java 那种,有一些框架甚至会有专门的组织去维护,比如 Spring,所以从这点来看,Go 的生态发展就比较担忧了,不知道会不会多虑了…” go-yaml 的归档,似乎成了这个担忧的一个现实注脚。一个维护了十多年、被广泛使用的基础库,说停就停了,这是否预示着 Go 的开源生态存在系统性的脆弱?我们是否真的应该为此感到焦虑? 在下结论之前,我们不妨先看看 go-yaml 作者 Gustavo 本人的说明,这其中透露的信息远比“停止维护”四个字要丰富得多: “这是我最早的 Go 项目之一…维护了十多年…可惜的是…个人和工作空闲时间都减少了…我原本希望通过将其转移到资源更丰富的专业团队…但最终也没能如愿…我也不能直接把维护工作‘交给’某个人或一个小团队,因为项目很可能会再次陷入无人维护、不稳定甚至被滥用的状态。…很抱歉。” Gustavo 的话语中,我们读到的不是草率的放弃,而是一个资深开源贡献者长达十年的坚持、后期的力不从心、以及对项目质量和用户负责任的审慎态度。这恰恰揭示了许多 Go 开源项目(乃至整个开源世界)的一个普遍现实:大量项目是由个人开发者或小团队利用业余时间驱动的,他们的热情和精力是项目持续发展的关键,但也可能成为单点故障。 在深入探讨之前,我们首先要向 go-yaml 的作者 Gustavo Niemeyer 致以诚挚的感谢。他凭借个人的热情和努力,将这个项目从 2010 年的圣诞假期启动,并坚持维护了超过十年之久,为 Go 社区贡献了一个极其重要的基础库。我们理解并尊重他因个人时间精力变化而做出归档的决定。需要明确的是,本文无意指摘这一事件本身,而是希望借此契机,与大家一同审视和思考 Go 开源生态系统的韧性与我们应如何看待其发展模式。 Go [...]
本文永久链接 – https://tonybai.com/2025/04/27/rob-pike-on-bloat 大家好,我是Tony Bai。 今年年初,Go语言之父、UTF-8编码的发明者Rob Pike的一篇题为”On Bloat”(关于膨胀)的演讲幻灯片(在2024年下旬做的)在技术圈,尤其是在Hacker News(以下简称HN)上,引发了相当热烈的讨论。Pike作为业界泰斗,其对当前软件开发中普遍存在的“膨胀”现象的犀利批评,以及对依赖管理、软件分层等问题的深刻担忧,无疑戳中了许多开发者的痛点。 HN上的讨论更是五花八门,开发者们纷纷从自身经历出发,探讨“膨胀”的定义、成因和后果。有人认为膨胀是“层层叠加的间接性”导致简单修改寸步难行;有人认为是“不必要的功能堆砌”;还有人归咎于“失控的依赖树”和“缺乏纪律的开发文化”。 那么,Rob Pike究竟在“抱怨”什么?他指出的软件膨胀根源有哪些?而作为我们Gopher,Go语言的设计哲学和工具链,能否为我们从纯技术层面提供对抗膨胀的“解药”呢?今天,我们就结合Pike的演讲精髓和HN的热议,深入聊聊软件膨胀的四大根源,并从Go的视角尝试寻找一下应对之道。 “膨胀”的真相:远不止代码大小和运行速度 在深入探讨根源之前,我们需要认识到,“膨胀”并不止是字面意义上我们理解的最终编译产物的大小或者应用的运行速度慢,Pike的观点和HN讨论中的“软件膨胀”体现在多个维度: 复杂性失控: 过度的抽象层次、复杂的依赖关系、难以理解的代码路径,使得维护和迭代变得异常困难。 维护成本剧增: 添加新功能的长期维护成本(包括理解、测试、修复Bug、处理兼容性)远超初次实现的成本,但往往被低估。 不可预测性与脆弱性: 庞大且快速变化的依赖树使得我们几乎无法完全理解和掌控软件的实际构成和行为,任何更新都可能引入未知风险。 下面我们具体看看Pike指出的“膨胀”几个核心根源: 根源一:特性 (Features) —— “有用”不等于“值得” Pike 指出,我们不断地为产品添加特性,以使其“更好”。但所有特性都会增加复杂性和成本,而维护成本是最大的那部分,远超初次实现。他警示我们要注意“有用谬论” —— 并非所有“有用”的功能都值得我们付出长期的维护代价。 HN讨论也印证了这一点:功能冗余、为了匹配竞品或满足某个高层“拍脑袋”的想法而添加功能、甚至开发者为了个人晋升而开发复杂功能的现象屡见不鲜。 技术层面:Go的“解药”在哪? 简洁哲学: Go从设计之初就强调“少即是多”,鼓励用简单的原语组合解决问题,天然地抵制不必要的复杂性。 强大的标准库: Go 提供了功能丰富且高质量的标准库,覆盖了网络、并发、加解密、I/O 等众多领域,减少了对外部特性库的依赖。很多时候,“自己动手,丰衣足食”(使用标准库)比引入一个庞大的外部框架更符合Go的风格。 关注工程效率: Go的设计目标之一是提高软件开发(尤其是大型项目)的工程效率和可维护性,这促使Go社区更关注代码的清晰度和长期成本。 注:技术层面包括语言、工具以及设计思路和方法。 根源二:分层 (Layering) —— 在错误的层级“打补丁” Pike 认为,现代软件层层叠加(硬件 -> 内核 -> 运行时 -> 框架 -> 应用代码),当出现问题时,我们太容易在更高的层级通过包装(wrap)来“修复”问题,而不是深入底层真正解决它。这导致了层层叠叠的“创可贴”,增加了复杂性和维护难度。他列举了ChromeOS文件App的例子,并强调要在正确的层级实现功能和修复。 在HN的讨论中,有开发者描述的修改按钮颜色需要穿透17个文件和多个抽象层的例子,正是这种“错误分层”或“过度抽象”的生动体现。 [...]
本文永久链接 – https://tonybai.com/2025/04/26/13-laws-of-software-engineering 大家好,我是Tony Bai。 做软件开发时间越长,越觉得背后似乎有只“无形的手”在影响着项目进度、团队协作、系统架构甚至技术决策。有些现象反复出现,从早期的一头雾水,到后来的似曾相识,再到最后的会心一笑(或许是苦笑),让人不得不感慨其中蕴含的某些“规律”。 最近看到国外一位开发者ANTON ZAIDES总结了软件工程领域的13条“定律”。它们中有些广为人知,有些则相对小众,但都非常实用。它们虽然不像物理定律那样严格精确,但确实精准地捕捉到了我们日常工作中经常遇到的挑战和现象,堪称是工程师和管理者都应该了解的宝贵“经验法则”或“心智模型”。 今天,就让我们一起来了解和学习一下这13条定律,看看它们是如何在我们身边运作的。 注:下面文中各条定律的配图也借自ANTON ZAIDES的原文章。 1. 帕金森定律 (Parkinson’s law) 定律:工作会不断扩展,填满所有可用的时间 (Work expands to fill the available time.) (任务总能拖到最后期限前完成?) 这是最著名的定律之一。简单说,如果你给一个任务设定了1周的期限,它很可能就会花掉1周;如果设定了2周,它就可能花掉2周。这常常被用来解释为什么设定“伪造”的(有时甚至不合理的)截止日期似乎能提高效率——它迫使人们在有限的时间内集中精力。但这一定律也容易被滥用,导致不切实际的期望和压力。 合理设定Deadlines是必要的,但要警惕其副作用,并结合对工作量的实际评估。它提醒我们时间管理的重要性,以及在没有明确时间约束时,任务可能无限膨胀的风险。 2. 霍夫施塔特定律 (Hofstadter’s law) 定律:事情总是比你预期的要花费更长的时间,即使你已经考虑了霍夫施塔特定律。 (It always takes longer than you expect, even when you take into account Hofstadter’s Law.) (估时永远不准?) 这是对软件项目估时困难最精准的自嘲。几乎所有的软件项目都会延期,即使你已经预留了缓冲时间。这一定律完美地平衡了帕金森定律:如果你因为帕金森定律而设置过短的Deadline,结果很可能是团队burnout或者项目持续延期。 软件工时评估极其困难,充满了不确定性。简单的缓冲时间往往不够。有效的项目管理需要在时间、资源和可协商的范围 (negotiable scope) 之间找到平衡,并依赖持续的沟通和实践经验。 3. 布鲁克斯定律 (Brooks’ [...]
本文永久链接 – https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver 大家好,我是Tony Bai。 在软件开发的世界里,细节决定成败,这句话在以简洁著称的Go语言中同样适用,甚至有时会以更出人意料的方式体现出来。 想象一下这个场景:你正在对一个稳定的Go项目进行一次看似无害的“无操作(no-op)”重构,目标只是为了封装一些实现细节,提高代码的可维护性。然而,提交代码后,CI系统却亮起了刺眼的红灯——某个核心基准测试(比如 sysbench)的性能竟然骤降了30%! (图片来源:Dolt博客原文) 这可不是什么虚构的故事,而是最近发生在Dolt(一个我长期关注的一个Go编写的带版本控制的SQL数据库)项目中的真实“性能血案”。一次旨在改进封装的重构,却意外触发了严重的性能衰退。 经过一番追踪和性能分析(Profiling),罪魁祸首竟然隐藏在代码中一个极其微小的改动里。今天,我们就来解剖这个案例,看看Go语言的内存分配机制,特别是值接收者(Value Receiver),是如何在这个过程中悄无声息地埋下性能地雷的。 案发现场:代码的前后对比 这次重构涉及一个名为 ImmutableValue 的类型,它大致包含了一个内容的哈希地址 (Addr)、一个可选的缓存字节切片 (Buf),以及一个能根据哈希解析出数据的ValueStore接口。其核心方法 GetBytes 用于获取数据,如果缓存为空,则通过 ValueStore 加载。 重构的目标是将ValueStore的部分实现细节移入接口方法ReadBytes中。 重构前的简化代码: // (ImmutableValue 的定义和部分字段省略) func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) { if t.Buf == nil { // 直接调用内部的 load 方法填充 t.Buf err := t.load(ctx) if err != nil { return nil, [...]
本文永久链接 – https://tonybai.com/2025/04/24/conventional-commits-guide 告别混乱Commit Log!用规范指引你写出有意义的提交! 大家好,我是Tony Bai。 Git的Commit Log (提交日志) 是项目演进的脉络,也是开发者之间沟通变更、追溯历史、理解代码演变的关键载体。然而,在实际开发中,我们常常面对杂乱无章、意义不明的提交信息——”fix bug”、”update code”、”wip” 等屡见不鲜。这些模糊的记录不仅让代码审查、问题排查和版本追溯变得异常困难,也阻碍了自动化流程的实施。Conventional Commits (约定式提交) 规范提供了一套清晰、简洁的指引,旨在将每一次提交都转化为有意义、结构化的信息单元,从而显著提升 Commit Log 的价值和可利用性。 在这篇文章中,我们将探讨Conventional Commits如何作为一项关键指引,帮助开发者和团队构建更清晰、更一致、更具信息量的提交历史。 1. Commit Log的困境:为何需要指引? 缺乏明确指引的Commit Log往往会陷入以下困境: 信息熵高,有效信息少: 大量模糊、随意的提交信息混杂在一起,难以快速定位关键变更或理解特定提交的目的。 沟通效率低下: 团队成员需要花费额外时间去解读他人的提交意图,代码审查效率降低。 历史追溯困难: 当需要回溯某个功能或 Bug 的引入/修复历史时,无结构的日志如同大海捞针。 自动化阻碍: 不一致、不可预测的提交信息使得自动化生成 Changelog、语义化版本控制(SemVer)等流程难以实现。 面对这些普遍存在的困境,业界亟需一套行之有效的规范来引导开发者记录更有价值的提交信息。这正是 Conventional Commits 规范所要解决的核心问题,它通过引入一套简洁而强大的结构化指引来实现这一目标。Conventional Commits并非强制性的铁律,而是一套强大的指引 (Guidance),它通过引入轻量级的结构化约定,引导开发者在提交时思考并明确表达变更的性质、范围和影响。 2. Conventional Commits 核心指引:结构化的力量 该规范的核心指引体现在其简洁的提交信息结构上(如下所示): [optional scope]: [optional body] [optional [...]
本文永久链接 – https://tonybai.com/2025/04/24/multiple-containers-pod-pattern 大家好,我是Tony Bai。 将Go应用部署到Kubernetes已经是许多团队的标配。在这个强大的容器编排平台上,除了运行我们的核心Go服务容器,Kubernetes还提供了一种灵活的设计模式——多容器Pod。通过在同一个Pod内运行多个容器,我们可以实现诸如初始化、功能扩展、适配转换等多种辅助功能,其中最知名的就是Sidecar模式。 这些“辅助容器”就像我们Go应用的“最佳拍档”,在某些场景下能发挥奇效。然而,正如 Kubernetes官方文档和社区讨论一直强调的那样,引入额外的容器并非没有成本。每一个额外的容器都会增加复杂度、资源消耗和潜在的运维开销。 因此,关键在于策略性地使用这些模式。我们不应将其视为默认选项,而应是解决特定架构挑战的精密工具。今天,我们就来聊聊Kubernetes中几种合理且常用的多容器Pod模式,探讨何时应该为我们的Go应用引入这些“拍档”,以及如何更好地利用Kubernetes v1.33中已正式稳定(GA)的原生Sidecar支持来实现它们。 图K8s v1.33发布 首先:警惕复杂性!优先考虑更简单的替代方案 在深入探讨具体模式之前,务必牢记一个核心原则:非必要,勿增实体。 对于Go这种拥有强大标准库和丰富生态的语言来说,许多常见的横切关注点(如日志记录、指标收集、配置加载、基本的HTTP客户端逻辑等)往往可以通过引入高质量的Go库在应用内部更轻量、更高效地解决。 只有当以下情况出现时,才应认真考虑引入多容器模式: 需要扩展或修改无法触碰源代码的应用(如第三方应用或遗留系统)。 需要将与语言无关的通用功能(如网络代理、安全策略)从主应用中解耦出来。 需要独立于主应用进行更新或扩展的辅助功能。 特定的初始化或适配需求无法在应用内部优雅处理。 切忌为了“看起来很酷”或“遵循某种时髦架构”而盲目添加容器。 下面我们看看常见的一些多容器模式以及对应的应用场景。 四种推荐的多容器模式及其Go应用场景 Kubernetes生态中已经沉淀出了几种非常实用且目标明确的多容器模式,我们逐一来看一下。 Init Container (初始化容器) Init Container是K8s最早支持的一种“sidecar”(那时候还不这么叫),它一般用在主应用容器启动之前,执行一次性的关键设置任务。它会运行至完成然后终止。 它常用于以下场景: 运行数据库Schema迁移。 预加载配置或密钥。 检查依赖服务就绪。 准备共享数据卷。 下面是官方的一个init containers的示例: apiVersion: v1 kind: Pod metadata: name: myapp-pod labels: app.kubernetes.io/name: MyApp spec: containers: - name: myapp-container image: busybox:1.28 command: ['sh', [...]
本文永久链接 – https://tonybai.com/2025/04/23/tips-for-reading-technical-books 大家好,我是Tony Bai。 今天是世界读书日。聊到读书,尤其是咱们技术人经常要面对的那些厚重的“技术砖头”,估计不少朋友都有过类似的挣扎:道理都懂,书很重要,但就是感觉难啃、读不进去,或者读完就忘,效果不彰。 技术书籍往往信息密集、逻辑严谨、内容晦涩,想要高效地从中汲取养分,确实需要讲究一些方法。我自己就是一个长期主义者,坚信持续学习和深入思考的力量。多年来,我不仅坚持阅读,也一直在我的博客tonybai.com以及本公众号上进行长期的、持续的输出,这个过程让我对如何高效阅读和内化知识,有了一些切身的体会和思考。 此外,如今AI工具日益强大,如何结合传统方法与智能辅助,是一个非常值得探讨的话题。 今天,我就结合我的长期实践,和大家分享一些个人实践,特别是在攻克难点和整理笔记环节,我也会着重谈谈AI如何能成为我的得力助手,希望能帮助你更好地攻克技术“硬书”,将知识真正转化为自己的竞争力。 心法一:明确目标,精准选书——为何而读? 在信息爆炸的时代,选对书可能比努力读更重要。开始前,先明确“为何而读”: 当前痛点/目标是什么? (深入Go并发?掌握K8s?学习AI Agent开发?) 这本书能解决问题吗? 通过看目录、序言、书评(例如在豆瓣读书、亚马逊评论区、O’Reilly Learning Platform、Manning官网 等站点优质站点查找)、作者背景来判断。 难度是否匹配? (是否需要前置知识?) 我的做法: 基于工作和学习规划、以及遇到的技术瓶颈选书,优先选择能直接解决我当下问题的、或者能为我未来方向打下坚实基础的书(这的确需要一些前瞻性的技术眼光)。带着明确的目的去读,效率和动力都会高很多。 心法二:主动出击,建立框架——如何开始? 面对“砖头书”,忌直接死磕。先做“侦察”,建立整体认知: 速览目录、序言、总结: 把握全书结构、核心思想。 带着问题阅读: 主动思考你想从中获得什么答案。 我的做法: 我通常会先花半小时到一小时快速“翻阅”全书,在脑海里构建一个大致的知识地图。然后根据我的目标,决定是通读全书,还是重点阅读某些章节。对于特别重要的章节,我会先看一遍小结,再带着问题去细读正文。 心法三:攻克难点,允许“跳过” 遇到难啃的概念或复杂逻辑卡壳时: 别死磕,标记跳过: 保持阅读节奏,避免挫败感。后续内容或整体理解可能有助于回头解决。 寻求外援: 查阅资料、社区提问,或同主题书籍的交叉阅读,从多个角度帮助理解难啃的技术概念。 AI在此环节的“神助攻” 在这个最容易卡壳、也最考验耐心的环节,AI展现出了惊人的辅助潜力,能显著提升我们攻克难点的效率。以下是一些你可以尝试的提示词示例(以经典书籍《The Go Programming Language》为例): 多角度解释: “请用一个现实生活中的例子,解释《The Go Programming Language》中描述的 Go channel 的概念,特别是带缓冲和不带缓冲 channel 的区别。” “我正在读 TGPL 关于 [...]
本文永久链接 – https://tonybai.com/2025/04/22/go-ai-knowledge-community-launch 大家好,我是Tony Bai。 首先,由衷感谢大家一直以来对我的关注和对我之前知识星球“Gopher部落”的支持!在过往的时光里,我们一起探讨Go语言的奥秘,分享实践中的得失,那是一段非常宝贵的共同成长的记忆。 随着人工智能浪潮的席卷而来,它正以前所未有的速度渗透和重塑着软件开发的方方面面。同时,我也在不断思考,如何能为大家提供更聚焦、更深度、更体系化的价值,真正助力各位在技术浪潮中乘风破浪。 基于这些思考,我今天郑重地向大家宣布:我的知识星球,原“Gopher部落”,现已正式升级更名为「Go & AI 精进营」! 这不仅仅是一次名称的变更,它代表着星球未来内容方向、服务重心以及我对各位星友价值承诺的全面进化。 「Go & AI 精进营」:聚焦两大核心支柱 未来,我的星球将更加专注于以下核心领域,致力于打造一个高质量、高价值的技术学习与交流社区,为您提供更高质量、更体系化的学习与交流体验。 高质量体系课创作 (核心价值) 我将倾注核心精力,打造一系列深度、连贯的体系课程,覆盖Go语言从底层原理到高级实践,以及AI赋能应用的前沿领域。目前规划及逐步更新的课程体系包括: Go深度进阶课: Go进阶课:覆盖语法强化、设计先行与工程实践三大领域,源自GopherChina大会会前高级训练营; Go原理课:深入理解语言底层机制; Go避坑课:识别并规避常见陷阱与缺陷; Go高级实践:学习真实场景下的工程化与最佳实践; Go版本/特性精解:紧跟官方动态,精通新版本特性; Go+AI 实战课: Go+AI应用实战:学习用Go构建AI原生的应用; Agent开发实战课:掌握前沿的AI Agent开发; Go与大模型:探索Go语言与LLM的结合与应用; AI工具实践:熟练运用AI工具提升开发效率; 生态与视野: 云原生与Go:介绍云原生生态中最新理念、工具与应用; GopherDaily精选:(升级) 提供更具洞察力的Go生态核心资讯解读; 年度技术盘点:把握Go及相关领域发展趋势; (附送) WebRTC/Rust专题:特定前沿技术深度探讨; 职业发展: 成长心法:学习顶尖技术人的思维模式与成长路径; 高质量核心服务 (持续保障) 深度问答: 我将继续坚持 6 小时内响应(工作日有效时间),并提供更深入、更有价值的解答 (星友问答专栏)。 独家资源: 持续分享精选学习资料、内部工具和专属福利 (资源福利区专栏)。 活动/互动/作业: 我们会定期组织低门槛、有价值的 UGC [...]
本文永久链接 – https://tonybai.com/2025/04/21/go-project-design-antipatterns 大家好,我是Tony Bai。 在软件开发这个行当里,“最佳实践”、“设计模式”、“标准规范”这些词汇总是自带光环。它们总是承诺会带来更好的代码质量、可维护性和扩展性。然而,当这些“圣经”般的原则被生搬硬套到Go语言的语境下时,有时非但不能带来预期的好处,反而可能把我们引入“歧途”,滋生出一些看似“专业”实则有害的“反模式”。 最近我也拜读了几篇国外开发者关于Go项目布局和设计哲学的文章,结合我自己这些年的实践和观察,我愈发觉得,Go社区中确实存在一些需要警惕的、流行的设计“反模式”。这些“反模式”很多人都或多或少的使用过,包括曾经的我自己。 在这篇文章中,我就总结一下我眼中的Go项目设计“七宗罪”,希望能帮助大家在实践中保持清醒,做出更符合Go精神的决策。 第一宗罪:为了结构而结构——过度分层与分组 表现: 项目伊始,不假思索地创建pkg/、internal/、cmd/、util/、model/、handler/、service/ 等层层嵌套的目录,美其名曰“组织清晰”、“符合标准”。 危害: * 违背简洁: Go 的核心哲学是简洁。不必要的目录层级增加了认知负担和导航成本。 * 过早抽象/耦合: 在需求尚不明确时就划分 service、handler 等,可能导致错误的抽象边界和不必要的耦合。 * pkg/ 的迷思: pkg/ 是一个过时的、缺乏语义的约定,Go官方在Go 1.4时将Go项目中的pkg层次去掉了,Go官方的module布局指南中也使用了更多有意义的名字代替了pkg。 * internal/ 的滥用: 它是 Go 工具链的一个特性,用于保护内部实现不被外部导入。但如果你的项目根本不作为库被外部依赖,或者需要保护的代码很少,强制使用 internal/ 只会徒增复杂性。 * cmd/ 的误用: 除非你的仓库包含多个独立的可执行文件,否则将单一的main.go放入cmd/毫无必要。 解药: 保持扁平!从根目录开始,根据实际的功能或领域需要创建有意义的包。让结构随着项目的增长有机演化,而不是一开始就套用模板。 注:笔者当年也是pkg的“忠实粉丝”,新创建一个项目,无论规模大小,总喜欢先将pkg目录预创建出来。现在是时候根据项目的演进和规模的增长来判断是否需要”pkg”这个有点像“namespace”的目录了,即当你有多个希望公开的库时,是否用pkg/作为一个顶层分组,这个是要基于项目的实际情况进行判断的。 第二宗罪:无效的“美化运动”——无价值的重构与移动 表现: 为了让代码看起来“更干净”、“更符合某种设计模式”或“消除Linter警告”,在没有明确收益(修复 Bug、增加功能、提升性能、解决安全问题)的情况下,大规模地移动代码、修改变量名、调整文件结构。 危害: * 浪费时间精力: 投入大量时间做无意义的表面文章。 * 引入风险: 任何修改都有引入新 Bug [...]
本文永久链接 – https://tonybai.com/2025/04/19/learn-go-in-ai-era 大家好!我是Tony Bai。 近来,AI领域的技术迭代速度惊人,尤其在代码生成能力上的显著提升,已有目共睹。现在,AI不仅能辅助编写Go代码片段,还能应对一些更复杂的逻辑结构,甚至还能完成一个完整工程的全部代码,这在开发者社区无疑引发了热烈讨论和对未来的思考。对于初学者来说,一个现实的问题摆在面前:我们还需要老老实实、一步一个脚印地去系统学习吗?比如像《Go语言第一课》专栏这样的系统课程还有必要去学吗? 这个问题确实值得我们认真思考。AI的便捷是显而易见的,但它能帮助我们构建起真正的专业能力吗? 如今,一个普遍的认知是,AI更像是一个能力的放大器,它能够增强你已有的知识和技能,但却很难从零开始为你构建坚实的基础。依赖于碎片化的AI交互,我们或许能快速“上手”,但要真正“掌握”某个领域,成为一名专业人士,可能还有很长的路要走。 那么,在AI时代,系统学习能为渴望成长的你,尤其是初学者,提供哪些不可替代的价值呢? 我觉得至少有如下几点: 为你构建坚实的知识体系,告别“东拼西凑” AI能帮你生成代码片段,但这就像给你一堆散落的乐高积木。系统学习则教你如何将它们搭建成一个完整的城堡。 初学一门语言,最怕的就是知识点零散、不成体系。以Go学习为例,系统课程精心编排了学习路径,覆盖了环境搭建、基础语法、核心特性(接口、并发等)到简单实践。 它就像一张清晰的地图,引导你系统性地认识Go的世界,为你提供“一站式”的基础构建服务,避免在碎片信息中迷失方向,让起步更高效、更稳固。 助你拓展认知边界,进入学习“拉伸区” 还是以Go学习为例,系统的课程学习不仅仅是知道“怎么做”,也会适时地告诉你一些“为什么”。比如了解Go的设计哲学(简洁、显式、组合、面向并发等),理解某些特性(如接口设计)背后的考量。 这并非要求初学者一开始就深究底层,而是像《认知觉醒》一书里说的,帮助你适度地拓展“舒适区”,进入能获得更快提升的“拉伸区”。 理解了这些,你会发现自己应用起来更灵活,学习新知识也更快,整体学习效率自然更高。这是一种更高效的学习方式。 奠定“专业”基石,让你真正驾驭AI AI能写出代码,但判断代码的好坏、进行复杂调试、做出架构决策,这些都需要你具备扎实的专业基础。 系统学习正是帮你奠定这个基础,让你未来能有效地指导和评估AI写出的代码,而不是仅仅停留在简单地复制粘贴,甚至让AI生成一堆你自己都看不懂、无法评估好坏的代码,这样的代码一旦上生产可能带来潜在的风险和隐患。 只有足够专业,你才能有效地向AI提问,辨别AI答案的优劣,最终驾驭AI,让它成为你专业能力的延伸,而非被其能力所取代。碎片化的学习,是无法构建起这种专业壁垒的。而系统的课程学习,正是你迈向专业之路的第一块、也是至关重要的基石。 提供可靠、经过验证的学习起点 网络信息真假难辨,AI的回答也可能存在谬误。对于初学者来说,一开始接触到准确、可靠的信息至关重要。 一门好的课程,其内容是经过作者和编辑团队反复打磨、验证和审校的,源自大量实践和教学经验,确保了其准确性和权威性。它为你提供了一个可以信赖的学习起点和参照系,这本身就是一种重要的学习服务。 所以,回到最初的问题:AI会写Go代码了,初学者还需要系统学习吗? 我的答案是:如果你不满足于“能用”,而是渴望真正“掌握”Go,渴望成为一名具备深度思考和解决复杂问题能力的Gopher,那么,系统学习依然是必经之路。 而《Go语言第一课》,这门我在极客时间打磨许久的专栏,正是基于上述理念设计的。它专注于为你铺设一条清晰、可靠、高效的Go入门之路,帮助你从“知道”走向“理解”,为未来成长为一名专业的Gopher奠定基础。 如果你正准备开始学习Go,或者希望巩固基础、构建体系,可以扫描下方二维码,订阅《Go语言第一课》专栏,为你的Go专业之路,打下第一个坚实桩基!,你也可以与其他订阅和学习该专栏的数万Gopher一起交流学习心得,共享学习成果。 最后补充一点信息: 为了让课程能更好地服务大家,最近我和极客时间的编辑老师一起,为《Go语言第一课》做了一次重要的课程迭代,增加了6篇“加餐”内容,涉及测试、性能、I/O、语言新特性等,希望能为你的学习之路提供一些额外的助力。当然,课程的核心价值,始终在于主体内容所构建的那个系统化的入门基础。 现在,轮到你了:作为Go学习者(特别是初学者),你如何看待AI对学习的影响?你认为系统学习最大的价值是什么?欢迎在评论区分享你的想法,我们一起探讨!如果觉得这篇文章说到了你的心坎里,请点个“在看**”支持一下吧! 愿我们都能拥抱AI,但不忘构建自身的专业核心! 原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧! 我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得: 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」,掌握 AI [...]
本文永久链接 – https://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent 大家好,我是Tony Bai。 人工智能Agent风头正劲,但构建它们真的那么难吗?本文深入解读Thorsten Ball 的“皇帝新衣”论,并通过一个 Go 标准库 + OpenAI Compatible API + DeepSeek的实战复现,揭示代码编辑 Agent 的核心简洁性,探讨真正的挑战与机遇。 引言:AI Agent 的神秘光环与现实 近来,AI Agent(人工智能代理)无疑是技术圈最炙手可热的话题之一。从能自主编码的软件工程师,到能规划执行复杂任务的智能助手,Agent 展现出的潜力令人兴奋。但与此同时,它们往往被一层神秘的光环笼罩,许多人觉得构建一个真正能工作的 Agent,尤其是能与代码交互、编辑文件的 Agent,必然涉及极其复杂的技术和深不可测的“炼金术”。 事实果真如此吗?Agent 的核心真的那么难以企及吗? 戳破泡沫:Thorsten Ball 的“皇帝新衣”论 著名开发者、“Writing A Compiler In Go”和“Writing An Interpreter In Go”两本高质量大作的作者Thorsten Ball最近发表了一篇振聋发聩的文章——《How To Build An Agent》(如何构建一个Agent),副标题更是直言不讳:“The Emperor Has No Clothes”(皇帝没有穿衣服)。 Thorsten 的核心观点非常清晰:构建一个功能齐全、能够编辑代码的 Agent,其核心原理并不神秘,甚至可以说没有所谓的“护城河”。他认为,那些看似神奇的 Agent(自动编辑文件、运行命令、从错误中恢复、尝试不同策略)背后并没有什么惊天秘密。 其核心不过是:一个强大的大语言模型 (LLM) [...]
本文永久链接 – https://tonybai.com/2025/04/17/go-is-badly-designed 大家好,我是Tony Bai。 今天刷X (前Twitter) 的时候,看到Golang Insiders社区下面这条推文,真是差点扑哧一声笑出来,感觉说得太形象了,必须分享给大家: 这位叫Lyes的开发者回应 “Go is badly designed” (Go 语言设计得很糟糕) 的说法,他打了个比方: 这让我想起了我的高中物理老师,我们当时都恨他,因为他从不‘放水’简化物理知识。课难、考试难,大部分人在他手下分数都不高,所以他自然成了‘坏老师’。 Go 语言就有点像他。它从不‘放水’,直面问题。你可以很快用它变得高效,写出远比用 Python 或 JavaScript 写得更好的软件。 但你也得知道,这门语言不会‘溺爱’你。当你的服务器因为一个 nil map 或其他新手常犯的错误而 panic 时,别生气。 不像 Rust,Go 的编译器不会在你编程生涯的每一刻都‘牵着你的手’。它会给你足够的方向让你知道该往哪走,满足你 80% 的需求,同时仍然保持你的生产力。 怎么样?看完这段话,是不是像极了我们初学Go时,被nil pointer dereference 或 index out of range 当头棒喝的瞬间? 像极了我们当年一边抱怨物理老师太严格、考试太变态,一边又不得不硬着头皮去啃那些公式和定理的样子? Lyes 的这个比喻,可以说精准地戳中了 Go 语言的一些核心特质,也解释了为什么关于“Go是否设计糟糕”的争论从未停止。咱们今天就借着这个“物理老师”的比喻,好好聊聊Go的“坏脾气”和它背后的设计哲学。 那个从不“放水”的“严格老师” Lyes 说 Go 不会 “dumb [...]
本文永久链接 – https://tonybai.com/2025/04/17/standardize-the-hash-function 大家好,我是Tony Bai。 随着Go泛型的落地和社区对高性能自定义容器需求的增长,如何为用户自定义类型提供一套标准、安全且高效的Hash计算与相等性判断机制,成为了Go核心团队面临的重要议题。近日,经过Go核心开发者多轮深入探讨,编号为#70471 的提案”hash: standardize the hash function”最终收敛并被接受,为Go生态引入了全新的maphash.Hasher[T] 接口,旨在统一自定义类型的Hash实现方式。 这个旨在统一自定义类型Hash实现的提案令人期待,但我们首先需要理解,究竟是什么背景和痛点,促使Go社区必须着手解决自定义 Hash 的标准化问题呢? 1. 背景:为何需要标准化的Hash接口? 在Go 1.18泛型发布之前,为自定义类型(尤其是非comparable类型)实现Hash往往需要开发者自行设计方案,缺乏统一标准。随着泛型的普及,开发者可以创建自定义的哈希表、集合等泛型数据结构,此时,一个标准的、能与这些泛型容器解耦的Hash和相等性判断机制变得至关重要。 更关键的是安全性。一个简单的func(T) uint64类型的Hash函数看似直观和易实现,但极易受到Hash 洪水攻击 (Hash Flooding DoS) 的威胁。 什么是Hash洪水攻击呢? 简单来说,哈希表通过Hash函数将键(Key)分散到不同的“桶”(Bucket)中,理想情况下可以实现快速的O(1)平均查找、插入和删除。但如果Hash函数的设计存在缺陷或过于简单(例如,不使用随机种子),攻击者就可以精心构造大量具有相同Hash值的不同键。当这些键被插入到同一个哈希表中时,它们会集中在少数几个甚至一个“桶”里,导致这个桶形成一个长链表。此时,对这个桶的操作(如查找或插入)性能会从O(1)急剧退化到O(n),消耗大量CPU时间。攻击者通过发送大量这样的冲突键,就能耗尽服务器资源,导致服务缓慢甚至完全不可用。 Go内建的map类型通过为每个map实例使用内部随机化的 Seed(种子)来初始化其Hash函数,使得攻击者无法预测哪些键会产生冲突,从而有效防御了此类攻击。hash/maphash包也提供了基于maphash.Seed的安全Hash计算方式。因此,任何标准化的自定义Hash接口都必须将基于Seed的随机化纳入核心设计,以避免开发者在不知情的情况下引入安全漏洞。 明确了标准化Hash接口的必要性,尤其是出于安全性的考量之后,Go核心团队又是如何一步步探索、权衡,最终从多种可能性中确定接口的设计方向的呢?其间的思考过程同样值得我们关注。 2. 设计演进:从简单函数到maphash.Hasher 围绕如何设计这个标准接口,Go 团队进行了广泛的讨论(相关issue: #69420, #69559, #70471)。 最初,开发者们提出的 func(T) uint64 由于无法有效防御 Hash 洪水攻击而被迅速否定。 随后,大家一致认为需要引入Seed,讨论的焦点则转向Seed的传递和使用方式:是作为函数参数(func(Seed, T) uint64)还是封装在接口或结构体中。对此,Ian Lance Taylor提出了Hasher[T]接口的雏形,包含Hash(T) uint64和Equal(T, T) bool方法,并通过工厂函数(如 MakeSeededHasher)来管理 Seed。 然而,这引发了关于Seed作用域(per-process [...]
本文永久链接 – https://tonybai.com/2025/04/16/ai-protocol-prefer-jsonrpc 大家好,我是Tony Bai。 在AI技术飞速演进的今天,底层通信协议的选择对系统效率和互操作性至关重要。细心的开发者可能已经发现,新兴的AI协议如模型上下文协议(MCP)和Agent2Agent(A2A)协议,都不约而同地将目光投向了JSON-RPC 2.0。这并非巧合,而是一个深思熟虑的技术选型。在这篇文章中,我将和大家一起看看JSON-RPC 2.0的起源、核心规范以及历史应用,并解读这个10多年前定义的“老协议”为何能在AI时代能再次获得青睐。 1. JSON-RPC 2.0:起源与核心规范 JSON-RPC协议的诞生,源于对早期RPC协议(如XML-RPC、SOAP)复杂性的反思,旨在提供一种更轻量、更简洁的远程过程调用机制。其2.0版本规范(基于2009年草案,正式发布于2010年左右)更是将这一理念发扬光大。其核心设计哲学正如规范开篇所言:“It is designed to be simple!” 很多开发者日常都是用过JSON-RPC 2.0,但可能没有对其规范做过深入的了解,借此篇文章机会,让我们依据其官方规范,深入了解其关键特性。。 1.1 核心原则 我们先来看一下JSON-RPC协议设计的几个核心原则。 Stateless (无状态): 每次请求都是独立的,服务器不保存客户端状态。 Light-weight (轻量级): 协议开销小,消息体紧凑。 JSON Data Format (JSON数据格式): 使用广泛流行、易于解析和人类可读的JSON(RFC 4627) 作为数据交换格式。 Transport Agnostic (传输无关): 协议本身不限定网络传输方式,可在HTTP、WebSocket、TCP、甚至进程内等多种环境使用。 接下来,我们再来看一下工作原理。JSON-RPC 2.0是一个相对简单的协议,其规范也就几页,因此其工作原理也非常好理解。 1.2 工作原理 JSON-RPC 的工作原理是向实现此协议的服务器发送请求。在这种情况下,客户端通常是打算调用远程系统的单个方法的软件。多个输入参数可以作为数组或对象传递给远程方法,而方法本身也可以返回多个输出数据(这取决于实现的版本。) 下面是对协议中的一些核心对象的解读。 1.2.1 Request Object (请求对象) Request Object是发起RPC调用的核心,由客户端发送请求到服务端。我们结合一个示例来理解请求对象的各个字段的含义: --> {"jsonrpc": "2.0", [...]
本文永久链接 – https://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize 大家好,我是Tony Bai。 最近在思考Go语言的发展时,不禁让我想起了当年学习C++的经历。Bjarne Stroustrup在《C++程序设计语言(特别版)》中就专门强调了“现代 C++”(Modern C++)的编程风格,鼓励使用模板、STL等新特性来编写更优雅、更高效的C++代码。 那么,我们热爱的Go语言,随着版本的不断迭代,是否也逐渐形成了一种“现代Go”(Modern Go)的风格呢?答案是肯定的。Go团队不仅在语言层面引入新特性(如泛型、range over int),也在标准库中添加了更强大、更便捷的包(如slices、maps)。 更棒的是,Go官方工具链gopls(Go Language Server Protocol的实现)中,就内置了一个名为modernize的分析器(Analyzer),专门用于帮助我们识别代码中可以用现代Go风格替代的“旧习”,并给出建议。 今天,我们就来深入了解一下gopls/modernize这个利器,看看它如何帮助我们的Go代码焕然一新,并学习一下它所倡导的11个“现代Go”风格语法要素具体包含哪些内容。 1. gopls/modernize分析器以及现代Go风格简介 gopls/modernize是golang.org/x/tools/gopls/internal/analysis/modernize 包提供的一个分析器。它的核心目标就是扫描你的Go代码,找出那些可以通过使用Go 1.18及之后版本引入的新特性或标准库函数来简化的代码片段。 modernize工具目前可以识别并建议修改多种“旧”代码模式。让我们逐一看看这些建议,并附上代码示例: (注:以下示例中的版本号指明了该现代写法是何时被推荐或可用的) 1). 使用min/max内建函数 (Go 1.21+) 旧风格: 使用 if/else 进行条件赋值来找最大/最小值。 func findMax(a, b int) int { var maxVal int if a > b { maxVal = a } else { maxVal = [...]
本文永久链接 – https://tonybai.com/2025/04/14/what-is-a2a-protocol 随着人工智能(AI)的飞速发展,AI 智能体(Agent)正成为企业自动化、提升生产力的关键力量。从处理日常重复任务到辅助复杂决策,智能体的应用场景日益广泛。然而,一个严峻的挑战随之而来:不同框架、不同厂商构建的智能体往往如同信息孤岛,难以有效协作,这极大地限制了它们在复杂企业环境中的潜力释放。 为了打破这一僵局,谷歌近日联合 Atlassian、Salesforce、SAP、LangChain、Cohere 等超过 50 家技术合作伙伴和领先服务提供商,共同发布并推动一个全新的开放协议——Agent2Agent(A2A)。该协议旨在为不同生态系统中的AI智能体提供一种标准的通信语言,使其能够安全地发现彼此、交换信息、协调行动,最终实现跨平台、跨应用的无缝协作。 在这篇文章中,我们就来结合示例快速了解一下A2A协议的设计哲学、核心机制、交互流程与对象模型,以及它与MCP(model context protocol)的区别。这可能是你看过的关于Agent互操作协议最清晰的解读之一。 1. A2A协议的设计哲学与核心机制 企业环境中,单一智能体往往难以应对复杂的端到端流程。例如,一个完整的客户服务请求可能需要客服智能体、订单系统智能体、物流跟踪智能体协同工作。A2A协议的诞生,正是为了满足这种日益增长的跨系统、跨智能体协作需求。 A2A的核心目标是促进智能体之间的互操作性(Interoperability),即使这些智能体基于不同的技术栈构建、不共享内部状态或工具集。谷歌及其合作伙伴在设计A2A时,明确了五大关键原则,这些原则深刻影响了协议的形态: 拥抱智能体能力 (Embrace agentic capabilities) 协议并非将智能体降级为简单的 API 或工具,而是承认并支持它们以更自然、有时甚至是非结构化的方式进行交互和协作。 基于现有标准 (Build on existing standards) 为了降低采用门槛和集成复杂度,A2A 建立在开发者熟悉的 HTTP/1.1 或 HTTP/2 之上,采用 JSON-RPC 2.0 作为请求/响应格式,并利用服务器发送事件 (Server-Sent Events, SSE) 实现流式通信。这使得 A2A 更易融入现有的企业 IT 架构。 默认安全 (Secure by default) 安全是企业级应用的基础。A2A 在设计上与 OpenAPI 的认证规范保持一致,支持如 OAuth2、API [...]
本文永久链接 – https://tonybai.com/2025/04/13/top-programmers-methods-mindset 这可能是我看到的关于‘如何成为顶尖程序员’最深刻的总结之一! 在快速迭代的技术世界里,每一位开发者或许都曾思考:是什么区分了“优秀”与“卓越”?仅仅是掌握了最新的框架或语言吗?Matthias Endler在他广受关注的文章《我所认识的最优秀的程序员》中,基于多年的观察,提炼出了那些真正顶尖的工程师们所共有的特质与习惯。这并非一份简单的技能清单,而更像是一份关于技术匠心、持续成长和专业心态的深度指南。在这篇文章中,我门将一同探索这些宝贵的洞见,希望能为你我的技术之路带来启发。 要深入理解顶尖程序员的与众不同之处,我们首先需要探究他们是如何构建坚实的技术基础,以及在日常工作中如何对待最基本的技术细节。 夯实基础:深度理解与精准调试 卓越并非空中楼阁,它建立在对一手资料和工具的深刻理解和对错误的精准把握之上。 深入理解工具 (Know Your Tools Well): 顶尖开发者追求对所用技术的基本原理的深刻理解(Grokking),这远超仅仅“会用”的层面。一个普通用户可能会在使用中磕磕绊绊、感到困惑、甚至用错方法而忽略优化。而专家则追求透彻理解,他们能够自信地写出配置,理解其中每一行的含义并能向同事解释清楚,不留任何疑问。要真正做到“深入了解”一个工具,你需要掌握它的: 历史: 谁创造了它?为何创造?旨在解决什么问题?了解背景有助于把握其设计哲学。 现状: 谁在维护?他们在哪里工作?当前开发的重点是什么?这关乎其发展方向和稳定性。 局限: 何时不适用?它的边界条件和可能失效的场景是什么?知其短板才能扬长避短。 生态: 有哪些关键的库或插件?社区活跃度如何?谁在广泛使用它?生态决定了其生命力和可扩展性。 正如文中所举的例子:如果你是一名重度使用 Kafka 的后端工程师,成为顶尖人才意味着你需要对 Kafka 有着系统和深入的认知,而非仅仅依赖于论坛上的零散信息。 阅读原始文档 (Read the Reference): 遇到问题时,他们的第一反应往往不是求助于Stack Overflow或LLM,而是直奔官方文档、规范或源代码。无论是Apache的配置、Python标准库,还是TOML 规范,他们相信第一手资料的价值。这种习惯让他们能够自信地配置工具的每一行参数,并清晰地解释其原因。深入了解技术的历史(Why)、现状(Who & What)和局限性(When not to use)是他们专业性的体现。如果你重度依赖Kafka,那么对Kafka的深入了解就应该是你的基本功。 细读错误信息 (Read The Error Message): 面对错误,他们不会惊慌失措或随意猜测,而是会真正地、深入地阅读错误信息,尝试理解其背后的含义。他们相信,错误信息本身就蕴含了解决问题的线索。这种从细微处推理的能力,让他们能够独立解决大部分问题,甚至在帮助他人时展现出惊人的洞察力。 拒绝猜测 (Don’t Guess): “面对模棱两可,拒绝猜测的诱惑”——《Python之禅》中的这条原则被顶尖开发者奉为圭臬。猜测可能会暂时“解决”问题,但错误的假设会构建脆弱的认知模型,遗患无穷。他们宁愿花费更多时间去问询、查阅资料、使用调试器,也要确保自己基于确凿的事实进行判断和行动。 掌握了扎实的基础知识固然重要,但真正的挑战往往在于如何运用这些知识去解决现实世界中的复杂问题。卓越的工程师在这方面同样展现出非凡的能力。 攻坚克难:拆解问题与拥抱挑战 拥有扎实的基础后,真正的较量在于如何面对并征服技术难题。解决复杂问题的能力,是衡量工程师价值的核心标尺。 分解问题 (Break [...]
本文永久链接 – https://tonybai.com/2025/04/11/uber-go-pgo-optimization 对于像Uber这样广泛采用Go语言(Uber 60%的CPU资源都用于支撑Go服务运行)的科技巨头而言,性能优化不仅关乎用户体验,更直接影响着运营成本。继多年前通过GOGC调优节省7万CPU核心后,Uber近期再次发力,分享了其在大规模Go服务中部署Profile-Guided Optimization (PGO) 的实践经验,并通过自动化框架和工具创新,克服了关键挑战,实现了显著的性能收益。在这篇文章中,我就来介绍一下Uber的PGO优化之旅,供大家参考。 1. PGO:Go近几个版本持续投入的性能优化手段 Profile-Guided Optimization (PGO),即配置文件引导的优化,是一种利用程序实际运行时的性能分析数据(Profile)来指导编译器进行优化的技术。相比传统的静态分析和启发式规则,PGO能够让编译器更精准地识别热点代码路径、函数调用频率、分支预测等,从而做出更优的优化决策,例如: 更智能的函数内联(Inlining): 基于实际调用频率,更精确地决定内联哪些“热”函数,即便这些函数在常规编译时可能不会被内联,从而减少函数调用开销。 接口调用的去虚拟化(Devirtualization): 在PGO数据表明接口变量在运行时通常指向特定具体类型时,可以将动态派发转换为更高效的直接调用。 优化的代码布局: 通过基本块重排、函数分割、函数重排等,改善指令缓存(iCache)和TLB的命中率,减少CPU前端停顿。 Go语言自Go 1.20版本开始引入对PGO的支持(最初侧重于内联优化),并在Go 1.21中,PGO实现生产可用,并增加了PGO驱动的去虚拟化(Devirtualization)。这表明Go官方对利用运行时信息提升性能的重视以及持续的投入。并且,通过用户的实际体验报告来看,PGO的确可以在一定程度上改善Go应用的性能,在Go 1.21及后续版本中,启用PGO 后,工作负载的性能常会有2%到7%的提升。 不过此前一直缺少来自大厂对PGO实践效果的声音,而Uber恰恰满足了Go社区的这个需求。 2. Uber的大规模PGO实践:自动化与挑战 面对数千个Go微服务,Uber在内部构建了一个持续优化的PGO框架: 其流程大致如下: 持续性能分析: 每日自动收集生产环境中多个服务实例的pprof CPU profiles。 配置文件聚合: 将收集到的profiles进行合并,生成具有代表性的服务性能画像。 服务注册: 通过配置系统,选择性地为特定服务开启PGO编译。 CI/CD 集成: 在持续集成环节,使用-pgo标志和生成的profile文件编译Go服务。 部署与监控: 将PGO优化的二进制文件部署到生产环境,并通过监控仪表盘追踪性能变化。 然而,大规模推广PGO并非一帆风顺。Uber很快遇到了一个关键挑战:启用PGO后,部分服务的编译时间急剧增加,最高可达8倍!这严重影响了开发和部署效率。 通过深入分析,团队发现根源在于Go编译器在为每个包编译时,都需要重复读取和解析完整的pprof文件,这在高并发的构建系统中造成了巨大的I/O和CPU开销,占据了PGO编译流程中高达95%的时间。 如何解决这个问题呢?我们接着看Uber工程师的创新方案。 3. 破局:创新的Profile预处理工具 为了解决编译耗时的瓶颈,Uber与Google Go编译器团队合作,开发并向上游贡献了一个profile预处理工具(该功能已集成到Go 1.23)。 这个工具的核心思想是“一次解析,多次使用”。它能够独立运行,提前读取原始的pprof文件,并解析profile数据以提取函数调用关系和频率信息。关键信息被转换并缓存为一种紧凑的中间格式(WeightedCallGraph,或加权调用图),使得Go编译器可以直接读取这种轻量级的中间格式,无需再解析庞大的pprof文件,从而显著降低编译开销。 在Uber内部部署该预处理工具并每日更新预处理后的profile后,有效解决了PGO带来的编译时间增加问题,大部分服务的编译耗时恢复到了接近优化前的水平,为PGO的大规模应用铺平了道路。 既然问题解决了,那PGO优化带来的最终效果如何呢?下面就来揭晓答案。 4. PGO的性能影响:实证与观察 [...]
本文永久链接 – https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis 嘿,各位Gopher! 你是否也在关心Go语言的最新动态?它还在快速增长吗?薪资水平如何?未来方向在哪? 这是我看到的关于2024年Go语言发展趋势最全面、数据最翔实的一份报告解读。 JetBrains,这家开发者们都非常熟悉的工具公司,最近发布了《Is Golang Still Growing? Go Language Popularity Trends in 2024》的研究报告文章。如果你是Go开发者,或者正在关注Go生态,这篇文章就是为你准备的,强烈推荐阅读! 在深入细节之前,先为你快速提炼报告的核心发现,让你高效把握重点: Go开发者规模依旧庞大且专业: 全球专业Go开发者估算超400万,且持续增长。 云原生主战场地位稳固: Web服务、云服务、IT基础设施是Go应用核心领域。 “钱景”诱人: Go开发者薪资普遍处于行业较高水平。 各大榜单表现亮眼: 在TIOBE、GitHub Octoverse等多个权威榜单中,Go排名稳定或显著上升。 与Rust互补而非替代: 两者定位不同,常被结合使用。 未来聚焦: 持续深耕云原生,并在GenAI基础设施领域崭露头角。 Go开发者画像:规模、角色与“钱景” 报告显示,全球使用Go的专业开发者规模可观。JetBrains估计近一年有410万专业人士使用Go,其中180万将其作为主要语言之一。SlashData的估算则更高,达到470万(包含学生和爱好者),而最新的Stack Overflow和SlashData数据推算更是达到了580万。 从上图中展示的开发者从事的软件类型来看: Web服务 (无GUI): 744,000 网站: 732,000 云服务: 681,000 开发者角色方面(如上图),除了大量的软件工程师/程序员 (约160万)外,DevOps/基础设施工程师(约50万)的比例也相当高,这凸显了Go在云原生基础设施和运维领域的巨大需求。 更让Gopher们关心的是薪资。报告明确指出,Go开发者是业内薪资最高的人群之一。美国Go开发者的平均年薪约为$76,000,经验丰富者甚至可达$500,000。 Go的应用版图:核心场景与行业分布 Go最常见的两大用例依然是: API/RPC服务(75%) 命令行工具(62%) 哪些行业在重度使用Go呢? 科技 (超过40%): Google, DataDog, K8s, HashiCorp, [...]
本文永久链接 – https://tonybai.com/2025/04/09/gomaxprocs-defaults-add-cgroup-aware Go官方出手!新提案自动优化容器内GOMAXPROCS,告别性能噩梦! 在Kubernetes等容器环境中运行Go应用时,一个常见的性能陷阱悄然存在:默认的GOMAXPROCS值基于节点CPU核心数,而非Pod的CPU限制(limit),导致资源争抢和性能下降。近期一篇广受关注的博客文章“Golang Performance Penalty in Kubernetes”通过实测数据揭示了这一问题带来的显著延迟增加(高达65%+)和吞吐量降低(近20%)。 不过近期,Go核心团队带来一则好消息,Go Runtime团队的Michael Pratt已正式提出一项提案(#73193),旨在让Go运行时默认感知Linux Cgroup的CPU quota限制并自动调整GOMAXPROCS值,该提案有望在Go 1.25中为开发者带来开箱即用的性能优化,告别在容器或Kubernetes中手动配置GOMAXPROCS的烦恼。 在这篇文章中,我会对当前GOMAXPROCS默认值在云原生环境引发的性能问题以及Pratt的提案做一个详细说明,供广大Gopher们参考。 1. 容器中GOMAXPROCS的“水土不服”与性能代价 自Go 1.5版本起,GOMAXPROCS默认设置为“可用的CPU核心数”(综合考虑机器核心数和CPU亲和性设置)。这在单租户或资源不受严格限制的环境下工作良好。然而,在普遍使用Cgroup进行资源隔离的容器化部署场景中,这一默认行为却常常与Pod的实际CPU限制limits.cpu)产生严重错位,引发一系列性能问题。 想象一下:一个Go应用部署在拥有32个vCPU的K8s节点上,但其Pod的limits.cpu被设置为1。Go运行时看到的是32核,于是默认将GOMAXPROCS设为32。这意味着Go运行时会尝试并发运行多达32个操作系统线程来执行Go代码,而Kubernetes(通过Cgroup的CPU Quota机制)却严格限制该Pod在每个调度周期内(如100ms)只能使用相当于1个CPU的计算时间。 这会带来什么后果? 正如Mansoor Majeed在其博客文章《Golang Performance Penalty in Kubernetes》中通过基准测试所生动展示的: 过度的上下文切换 32个活跃的Go线程争抢远少于此的可用CPU时间片(在此例中仅相当于1个CPU的时间),迫使操作系统内核进行大量、且低效的线程上下文切换。在他的测试中,错误配置GOMAXPROCS的场景下,上下文切换次数(context_switches_total)相比正确配置时飙升了近4倍(从约6.5k/s 增加到30k/s)。 CPU配额扼杀(Throttling)与调度延迟 应用(尤其CPU密集型任务,如博客中的Fibonacci计算)的并发线程迅速耗尽Cgroup分配的CPU时间配额(cpu.cfs_quota_us)。一旦耗尽,内核将强制暂停该Cgroup内所有线程的执行,直到下一个调度周期(cpu.cfs_period_us)开始。这直接导致了请求处理的延迟尖峰。博客中的”Process Schedule Stats”图表也显示,错误配置下,进程等待CPU的时间(Waiting for CPU)出现了高达34秒的峰值,而正确配置下仅约900毫秒。 应用性能显著下降 过度的上下文切换和频繁的CPU Throttling共同作用,导致应用端到端的性能大幅降低。博客的wrk基准测试显示,在CPU密集场景下,与正确设置GOMAXPROCS=1相比,使用默认GOMAXPROCS=32(基于节点而非Pod限制)导致的性能下降如下图所示: 我们看到:平均请求延迟增加了65% (从 20ms 上升到 33ms),最大请求延迟增加了82% (从255ms飙升到465ms)。整体RPS (每秒请求数) 下降了近20% (从50213减少到40356)。 GC 放大问题 Go的并发垃圾回收器(GC)的工作量与GOMAXPROCS挂钩。GC目标是使用25%的P(对应GOMAXPROCS数量)进行后台标记工作,并在空闲的P上运行额外的 idle worker。过高的GOMAXPROCS会导致GC期间产生远超实际可用CPU资源的并发请求,极易触发或加剧CPU配额扼杀,即使在非GC期间应用本身运行平稳。极端情况下,由于内核调度,可能出现大量GC [...]
本文永久链接 – https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir Go语言的testing包即将迎来两项备受期待的增强功能:标准化的测试属性(Test Attributes)和测试构件(Test Artifacts)管理。这两项提案(#43936 和#71287)均已获得Go团队的批准或高度认可,旨在显著提升Go测试的可观测性、调试效率以及与外部工具链(如CI/CD系统、测试管理平台)的集成能力。本文将深入解读这两项提案的设计理念、核心API、应用场景及其对Go开发者的潜在影响。 1. Go测试过程中的“痛点” 长期以来,Go开发者在处理测试过程中的元数据和输出文件时,常常面临一些挑战,不得不依赖非标准的约定或变通方法,这直接影响了测试效率和工具集成的流畅性。 1.1 痛点一:脆弱且混乱的测试元数据传递 现代开发流程中,我们常常需要将测试与外部系统关联起来。例如,将自动化测试结果上报给TestRail或Allure这样的测试管理平台,或者在CI/CD报告中直接链接到相关的Jira问题、代码提交或详细日志。 在t.Attr提案(#43936)出现之前,开发者通常只能通过t.Log或t.Logf输出特定格式的字符串来实现这一目标,例如类似以下的日志行: // 示例:试图通过日志传递元数据 TESTRAIL_CASE_ID: C12345 JIRA_ISSUE: PROJ-789 这种方法的弊端显而易见: 极其脆弱: 任何对日志格式、前缀或分隔符的微小改动,都可能导致依赖这些日志的外部解析工具(如CI脚本、报告生成器)失效。 缺乏标准: 每个项目或团队可能会发明自己的格式,导致工具难以复用和维护。 信息混杂: 重要的元数据与普通的测试日志信息混合在一起,增加了提取难度和误判的可能性。 工具集成困难: 像go test -json这样的官方工具,其输出的Action: output 事件并不区分普通日志和这种“伪装”的元数据,下游消费者需要进行额外的、不可靠的字符串解析。 总之,这种方式给需要自动化处理测试结果的场景带来了持续的维护负担和不确定性。 当然痛点不限于此,我们再来看一个。 1.2 痛点二:转瞬即逝的测试构件,调试与归档的障碍 Go testing包提供了t.TempDir函数,用于创建测试期间使用的临时目录和文件,这在隔离测试状态方面非常有用。然而,t.TempDir的核心特性——在测试(无论成功或失败)结束后自动清理其内容——在某些场景下反而成了阻碍。想象以下常见情况: 调试失败 一个复杂的集成测试失败了。测试过程中可能生成了详细的调试日志、服务间通信的网络抓包、或者是对比失败的实际输出文件。当你想检查这些文件以定位问题时,却发现它们随着测试的结束一同消失了。开发者不得不采取临时措施,比如注释掉t.Cleanup调用,或者在测试失败路径上手动复制文件到其他位置,过程繁琐且容易遗漏。 CI结果归档 在CI/CD流水线中,我们通常希望在测试失败时自动收集相关的诊断信息(如core dump、截图、性能剖析文件等)作为“构件(artifact)”进行归档,以便后续分析。虽然Go提供了-cpuprofile, -memprofile等标志并将结果放入-outputdir指定的目录,但对于测试代码自身产生的其他类型构件,缺乏一个统一且可靠的机制来指示它们需要被保留。 为了解决上述这些长期存在的痛点,Go社区积极讨论并推进了t.Attr和t.ArtifactDir这两项关键提案,旨在通过标准化的API为go testing包带来现代化的测试信息管理能力。 下面我们就来正式看看这两个提案究竟给我们带来了哪些测试过程中的便利。先来看看t.Attr提案。 2. t.Attr:为测试附加结构化元数据(#43936) 状态:已接受 (Accepted) 提案#43936旨在提供一种标准化的方式,将结构化的键值对元数据与特定的测试(或子测试)关联起来,并使其在go test -json的输出中易于访问。 [...]
本文永久链接 – https://tonybai.com/2025/04/03/waitgroup-go-proposal sync.WaitGroup是Go语言中处理并发任务同步最常用的原语之一。然而,其经典的Add(1)、go func() { defer wg.Done() … }()、Wait()模式虽然强大,却也因其固定写法和潜在的陷阱(如忘记Done或将Add误置于goroutine内部)而让开发者时常感到繁琐,对新手尤其不友好。近日,一项旨在简化这一模式的提案#63796在Go社区引发了广泛关注,并已被标记为Likely Accept,预示着sync.WaitGroup可能很快将迎来一个实用的新方法:Go。这也意味着Go开发者可以告别Add、defer Done的样板代码,并避免它们的“陷阱”可能导致的难以捕捉的代码错误。在这篇文章中,我就来简单介绍一下WaitGroup.Go这个提案。 1. 现有模式的痛点与WaitGroup.Go的提出 当前使用WaitGroup的标准模式通常如下所示: package main import ( "fmt" "sync" "time" ) func work(id int) { fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { // 注意:在 Go [...]
本文永久链接 – https://tonybai.com/2025/03/31/openpubkey-ssh-open-source 对于许多开发者和运维工程师而言,管理SSH密钥是一项繁琐且易出错的任务。正如SSH发明者、芬兰计算机科学家Tatu Ylonen所指出的,许多组织中过时授权密钥的数量甚至远超员工人数,这带来了巨大的安全隐患。现在,一个基于Go语言生态的创新项目——OpenPubkey SSH (OPKSSH),旨在彻底改变这一现状。近日,随着Cloudflare将OPKSSH代码捐赠给Linux基金会下的OpenPubkey项目并将其开源,开发者们终于可以拥抱一种更便捷、更安全的SSH认证方式:使用熟悉的单点登录(SSO)系统。本文将简要介绍OPKSSH项目及其技术基石OpenPubkey技术。 1. 核心看点:OPKSSH 开源与价值解读 OPKSSH (OpenPubkey SSH) 是一个巧妙的工具,它将OpenID Connect (OIDC) 等现代SSO技术与SSH协议集成起来,其核心目标是消除手动管理和配置SSH公私钥的需求,同时不引入除身份提供商(IdP)之外的任何新的可信第三方。 此前,虽然底层的OpenPubkey协议已于2023年成为Linux基金会的开源项目,但OPKSSH作为BastionZero(现已被Cloudflare收购)的产品,一直是闭源的。Cloudflare的此次捐赠,使得整个OpenPubkey技术栈的关键应用层实现也完全开放,这对于Go社区和整个基础设施安全领域都是一个重要进展。 2. OPKSSH解决了什么痛点? 通常,我们在进行远程服务器管理和运维操作时会使用SSH免密登录,即通过生成SSH密钥对并将公钥复制到远程服务器来实现。但这种传统方式的SSH密钥管理存在诸多问题: 密钥分发与轮换困难:需要手动将公钥部署到目标服务器,密钥泄露或员工离职后的吊销流程复杂。 长期密钥风险:长期存在的私钥增加了泄露风险,一旦泄露,影响范围广。 可见性差:难以清晰追踪谁拥有对哪些服务器的访问权限,公钥本身缺乏身份信息。 这些问题常常困扰企业的IT运维团队和安全管理人员,他们需要确保访问控制的安全性和可管理性,同时降低操作复杂性和人力成本。 那如何解决这些问题呢?OPKSSH带来了新的解决方案。 3. OPKSSH如何解决这些问题? OPKSSH基于OpenPubkey协议,带来了革命性的改进: 使用临时性密钥(Ephemeral Keys)提升安全性 OPKSSH使用按需生成的临时SSH密钥对取代长期密钥。用户通过SSO登录后,OPKSSH自动生成有效期较短(默认为24小时,可配置)的密钥。这大大缩短了密钥泄露的风险窗口。 通过单点登录(SSO Login)增强易用性 用户只需运行opkssh login,通过熟悉的IdP (如Google, Azure AD等) 进行SSO认证,即可自动获取所需的SSH密钥。无需手动生成、复制或管理私钥文件,即可在任何安装了opkssh的机器上进行SSH连接。 通过Identity-based Auth提升可见性与简化管理 授权不再基于难以管理的公钥列表(比如~/.ssh/known_hosts),而是基于易于理解和审计的用户身份(如Email地址)。管理员只需在服务器配置中指定允许访问的电子邮件地址列表即可。 到这里你可能会问:这么好用的OPKSSH是如何工作的呢?别急,我们下面就来介绍一下OPKSSH的工作原理。 4. OPKSSH的工作原理 Cloudflare的文章中有一个很好的介绍Opkssh工作原理的例子和示意图,这里也借用过来: 如图所示,当用户alice@example.com使用OPKSSH登录服务器,这个过程大致如下: 用户本地执行命令opkssh login触发OIDC流程,用户向IdP认证。 OpenPubkey协议介入,在OIDC流程中巧妙地将用户临时生成的公钥与用户的身份信息绑定,生成一个PK Token(本质上是一个增强的ID Token,包含了公钥信息并由IdP签名)。 OPKSSH将此PK Token打包进一个临时的SSH [...]
本文永久链接 – https://tonybai.com/2025/03/28/go-mod-verify-tag Go模块(module)在Go 1.11版本中引入,显著简化了依赖管理,使开发者能够通过go.mod文件明确声明和管理库依赖,支持语义版本控制,并提高了构建速度和可移植性。使得Go语言的依赖管理更加现代化和高效,提升了开发者的体验。 同时引入的校验和数据库 (sumdb) 也极大地增强了Go生态的依赖管理的确定性和安全性。然而,在模块作者发布新版本时,从本地代码库打上标签推送到代码托管平台,再到被Go Proxy和sumdb收录,这个过程中仍然存在一个微妙但关键的信任验证环节缺失。近期,Go团队接受了一项备受关注的提案(Issue #68669,旨在通过扩展go mod verify命令来弥补这一空白,为模块作者提供一种官方途径来验证他们本地的代码和标签确实与Go生态系统将收录的版本一致。在这一篇文章中,我就根据issue中的内容,来简单介绍一下这一新增安全机制的背景和运作原理。 注:该机制的提案刚刚被Accept,尚未确定在哪个版本落地,不过大概率是在Go 1.25版本中。 1. 问题背景:发布过程中的信任鸿沟 当前,Go开发者在发布一个新的模块版本时,通常的流程是: 在本地代码库完成开发和测试。 使用git tag (例如git tag v1.2.3) 创建版本标签。 使用git push –tags 将代码和标签推送到代码托管平台 (如 GitHub)。 等待Go Proxy (如proxy.golang.org) 拉取新版本,并将其信息提交给官方sumdb。 虽然sumdb保证了下游用户下载的模块代码未被篡改 (相对于sumdb中的记录),但它无法保证sumdb中记录的版本就精确地是模块作者在本地打标签时所期望的版本。潜在的风险点包括: 代码托管平台被篡改: 拥有强制推送权限的攻击者可能在标签推送后修改了标签指向的提交。 代码托管平台自身问题: 平台自身可能存在Bug或被攻击,导致返回给Go Proxy的代码与原始标签不符。 Go Proxy或sumdb问题: 尽管概率较低,但中间环节也可能存在问题。 正如提案贡献者和Go核心团队成员在讨论中指出的,目前缺少一个简单直接的方式让模块作者确认:“我本地标记为v1.2.3的代码,是否就是全世界通过Go工具链获取到的那个v1.2.3?”。 2. 提案核心:go mod verify -tag 为了解决这个问题,提案#68669建议为现有的go mod verify命令增加一个新的-tag标志。go mod verify命令目前用于检查本地缓存的依赖项是否被修改,而新的-tag标志则将关注点转向了当前模块本身。 [...]
本文永久链接 – https://tonybai.com/2025/03/27/remove-coretypes-from-go-spec Go 1.18引入泛型无疑是Go语言发展史上的一个里程碑,它带来了类型参数、类型约束等强大的新特性。伴随这些特性,一个名为“核心类型”(Core Type)的抽象概念也被引入,旨在简化泛型初期的规范定义和编译器实现。 然而,随着社区对泛型理解的深入和实践的积累,“核心类型”带来的复杂性和局限性也逐渐显现。近日,Go团队在提案#70128中正式决定,并已在开发分支中实施:将在即将到来的Go 1.25版本(预计2025年8月发布)中,从Go语言规范中移除“核心类型”这一概念。这项看似底层的改动,实则对Go语言的简洁性、易学性以及未来发展具有深远意义。 关于Go 1.18泛型语法概念以及实现的详细说明,可以阅读我的《Go语言第一课》专栏中的“泛型篇”。 “核心类型”:泛型时代的权宜之计 在Go 1.18设计泛型时,为了快速有效地更新语言规范以适应类型参数,Go团队引入了“核心类型”。这里对当前版本Go规范中对Core Types的说明进行了截图如下: Core Types概念的理解还是有门槛的,但结合泛型类型参数一起,简单来说就是: 对于非类型参数的类型,其核心类型就是其底层类型。 对于类型参数,其核心类型是其类型集(Type Set)中所有类型共同拥有的**唯一*底层类型。如果类型集中类型的底层类型不唯一,则该类型参数没有核心类型。 例如,下面约束类型的核心类型是[]int: interface{ ~[]int } 但对于下面约束类型Constraint: type Constraint interface { ~[]byte | ~string Hash() uint64 } 由于其包含[]byte和string两种不同的底层类型,它便没有核心类型。 这种设计在当时起到了“快捷方式”的作用,许多原先依赖“底层类型”的规范规则被直接替换为依赖“核心类型”。这在一定程度上简化了泛型引入初期的工作量。 “权宜之计”带来的困扰 然而,“核心类型”作为一个抽象且有特定规则(尤其对channel、append、copy等有复杂调整)的概念,逐渐暴露出一些问题: 过度限制: 基于核心类型的规则往往比基于类型集的规则更严格。例如,根据Go 1.24的规范,对类型参数为P Constraint (上文定义的Constraint) 的变量进行切片操作 (s[i:j]) 是不允许的,因为Constraint没有核心类型,即使切片操作对[]byte和string本身都是合法的。 增加认知负担: 开发者,尤其是初学者,在理解某些非泛型代码相关的规范(如切片表达式)时,也不得不去理解“核心类型”这个泛型相关的概念,增加了学习曲线。 规则不一致感: 像索引(a[x])、len、cap等操作的规则是基于类型集设计的(检查操作对类型集中所有类型是否有效),这使得它们看起来像是语言规则中的“特例”,而基于核心类型的规则反倒成了“常态”。 阻碍未来发展: “核心类型”的存在,使得一些本可以自然推广到泛型的特性难以落地。例如,提案#48522 设想允许访问类型集中所有结构体都共享的字段 (x.f),但在核心类型的框架下显得格格不入。类似的,它也限制了更灵活的切片操作和类型推断改进的可能性。 Go [...]
您可以订阅此RSS以获取更多信息