标签 Go 下的文章

Gin 真的是“真菌”吗?—— 一篇引发热议的“反 Gin”檄文解读

本文永久链接 – https://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library

大家好,我是Tony Bai。

“Gin 就像是一种伪装成软件库的阴险真菌:它很容易感染,一旦沾上就几乎无法去除,除非你极其小心,否则还会传染给你的朋友。”

2025 年 12 月,Efron Licht 发布了一篇名为《Gin 是一个非常糟糕的软件库》的长文,用词之激烈、抨击之全面,瞬间引爆了 Go 社区。他将 Gin 比作“真菌”,并列举了从代码膨胀到 API 设计混乱的种种“罪状”。

这篇文章虽然充满了情绪化的发泄,但它同时也触及了许多资深 Gopher 心照不宣的痛点。Reddit 上的热烈讨论证明了这一点:虽然很多人不喜欢作者的语气,但绝大多数人承认他的技术批评是站得住脚的

今天,让我们剥离情绪,结合社区的反馈,深入剖析这篇檄文背后的技术逻辑:作为 Go 生态中最流行的 Web 框架,Gin 真的有那么不堪吗?

第一宗罪:惊人的代码膨胀 (Code Bloat)

作者首先指出的,是 Gin 与其解决的问题之间巨大的比例失调

  • 标准库 net/http:仅用 2.5 万行 代码就实现了完整的 HTTP 协议栈(包含客户端、服务端、TLS 等)。
  • Gin:为了实现路由和中间件等相对简单的功能,其依赖树竟然引入了 87 万行 代码和 55MB 的体积!

更令人咋舌的是,Gin 的依赖树中包含了至少 6 个不同的 JSON 库(包括 sonic, go-json, ugorji/go/codec 等)。一名Reddit 用户 证实了这一点,并指出即使在不使用 msgpack 的情况下,Gin 也会引入巨大的二进制开销。虽然可以通过 -tags nomsgpack 来缓解,但这并非默认行为。

这种“把厨房水槽都装进去”的依赖管理方式,对于追求简洁和二进制体积的 Go 项目来说,确实是一个沉重的负担。

第二宗罪:混乱的 API 设计与“抽象泄漏”

作者对 Gin 的 API 设计进行了无情的嘲讽,称其“表面积像工业散热器一样大,而且一样吸热(sucks)”。

  • gin.Context 的过度设计:这个核心结构体拥有超过 133 个方法!它混杂了请求参数解析、响应写入、内容协商、Cookie 处理甚至 HTML 模板渲染等所有功能。一位Reddit 用户评论道:“Gin 就是当每一个可能的使用场景都塞进同一个库时发生的事情。”
  • 奇怪的方法签名:相比标准库清晰的接口,Gin 提供了数十种获取参数的方法,甚至还有 BindYAML, BindTOML 等特定的绑定方法。这种设计不仅增加了学习成本,也让代码的可测试性大打折扣。

第三宗罪:致命的“锁定效应” (Lock-in)

这是作者认为最严重的问题,也是将其比作“真菌”的核心原因。

  • 单向兼容性:你可以很容易地将一个标准的 http.Handler 包装成 Gin 的 handler。
  • 无法逃离:但如果你想从 Gin 迁移回标准库,或者是迁移到其他框架(如 Chi, Echo),你会发现几乎不可能。因为你的业务逻辑已经深度耦合了 *gin.Context 中那 100 多个特有的方法。

正如 一位Reddit 用户所言:“如果你想不付出巨大的开发者纪律和克制,就在 Go 中实现‘按需付费’(只引入需要的依赖),那几乎是不可能的。Gin 让事情变得简单,所以人们就用了它,尽管方式很糟糕。”

社区声音:不仅是批评,更是反思

Reddit 上的讨论为这场批判提供了更多元的视角:

  1. “标准库至上”派的胜利:许多用户表示,他们早已放弃 Gin,转而投向 EchoChi。Chi 因为其极简的设计(仅 1000 多行代码)和对标准库接口的严格遵守,被多次点名表扬。
  2. 对“中间件地狱”的共鸣:一名用户指出,虽然标准库很美,但它的中间件链和上下文处理确实不如框架方便。Gin 的成功在于它填补了标准库在人体工程学 (Ergonomics) 上的空白,尽管是以一种臃肿的方式。
  3. 初学者的陷阱:多位用户提到,AI(如 ChatGPT)往往会默认推荐 Gin 给新手,导致许多内部服务和 API 仅仅因为“AI 推荐”就染上了这种“真菌”。这加剧了 Gin 的锁定效应。

小结:我们还需要 Gin 吗?

Efron Licht 的批评固然犀利,但也存在幸存者偏差。对于初学者或快速原型开发来说,Gin 提供的“一站式”体验(路由、参数绑定、验证、JSON 序列化)确实极大地降低了门槛。

然而,随着 Go 标准库的不断进化(特别是 Go 1.22 引入了增强的 http.ServeMux),以及像 Chi 这样更轻量且优秀的替代品的成熟,原生开发的体验已经今非昔比

给 Go 开发者的一些建议:

  1. 对于新项目:建议评估 标准库 + ChiEcho。它们提供了更好的模块化和更小的依赖负担。
  2. 对于已使用 Gin 的项目:不要恐慌,但要警惕。在编写 handler 时,尽量将 *gin.Context 限制在最外层,将业务逻辑抽离到与框架无关的 Service 层中。
  3. 警惕“便利性”陷阱:在引入任何“全家桶”框架之前,问自己一个问题:我引入的这 55MB 依赖,真的只是为了少写几行 if err != nil 吗?

Go 的哲学是“少即是多”。Gin 在某种程度上,是对这一哲学的背离。这篇文章虽然激进,但它是一记警钟,提醒我们在享受便利的同时,不要忘记了软件工程中那些关于复杂性、依赖管理和可维护性的永恒真理。

资料链接:

  • https://eblog.fly.dev/ginbad.html
  • https://www.reddit.com/r/golang/comments/1pifcca/gin_is_a_very_bad_software_library/

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

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

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


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

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

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

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

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


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

Jepsen 报告震动 Go 社区:NATS JetStream 会丢失已确认写入

本文永久链接 – https://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes

大家好,我是Tony Bai。

近日,一则重磅消息在 Go 社区引发了不小的震动。分布式系统领域的“终极拷问者”——Jepsen——发布了一份针对 Go 生态中流砥柱级消息系统 NATS 及其子系统 JetStream 的深度分析报告

报告的结论是严峻的,甚至可以说是颠覆性的:在特定的、可复现的故障模式下,NATS JetStream 可能会丢失已经被服务器确认 (acknowledged) 并声称“已成功持久化”的数据

对于一个以持久化和可靠性为核心卖点的系统而言,这无异于一声惊雷。这份报告,对于所有正在使用或考虑使用 NATS JetStream 的 Go 开发者来说,都是一份必读的“警示录”。它深刻地揭示了在一个分布式系统中,“持久化”的承诺与现实之间的微妙鸿沟。

背景科普:NATS 与 JetStream 是什么?

在深入 Jepsen 的发现之前,让我们先快速了解一下今天的主角。

  • NATS:是一个用 Go 语言编写的、开源、高性能的消息中间件。它以其极致的性能、简单的 API 和轻量级的设计,在 Go 社区乃至整个云原生领域享有盛誉。其核心(有时被称为 “Core NATS”)提供的是一种“尽力而为” (best-effort) 的消息传递,速度飞快,但不保证消息的持久性或送达。

  • NATS JetStream:这是 NATS 的一个内置子系统,旨在为需要更高可靠性的场景提供解决方案。通过引入 Raft 共识算法,JetStream 在 NATS 的核心之上,构建了一个持久化的、可复制的日志(流)。它向用户承诺提供“至少一次” (at-least-once) 的消息传递保证——即已被确认的消息,不应丢失。

正是 JetStream 的这份“不丢失”的承诺,成为了 Jepsen 本次“拷问”的核心目标。

核心发现:“懒惰”的 fsync 默认策略

Jepsen 报告中最核心、也最具普遍警示意义的发现,在于 NATS JetStream 的默认 fsync 策略

问题根源
NATS JetStream 宣称,一旦客户端的 publish 请求被服务器确认,该消息就“已成功持久化”。然而,Jepsen 的测试发现,这并不完全准确。

默认情况下,NATS JetStream 每两分钟才调用一次 fsync 将数据从操作系统的页面缓存 (page cache) 刷入物理磁盘。

这意味着,在任何两次 fsync 之间,都存在一个长达两分钟的窗口期。在这段时间内,所有被服务器立即确认的写入,实际上只存在于内存中

后果是什么?
如果在这两分钟的窗口期内,发生协调性的断电内核崩溃、或多个节点快速连续地重启,那些仅仅存在于内存中的、已经被确认为“持久化”的数据,将永久丢失

Jepsen 的测试通过一个名为 LazyFS 的工具,精确地模拟了这种“断电即失忆”的场景,并成功复现了数据丢失:在一个测试运行中,NATS 丢失了大约 30 秒的写入,共计 131,418 条已被确认的消息。

与 Raft 理论的背离
这实际上与 Raft 论文的建议相悖。Raft 明确指出,节点在响应客户端之前,必须“将新的日志条目刷入 (flush) 它们的磁盘”。MongoDB, etcd, TiDB, Zookeeper 等其他基于共识的系统,都遵循了这一“先落盘,再确认”的原则。

NATS 的选择,是一种典型的性能与持久性之间的权衡。通过异步 fsync,它获得了极高的写入吞吐量,但牺牲了对“灾难性事件”的防护能力。

NATS 团队的回应
NATS 团队已经意识到了这个问题,并在文档中补充说明了这一风险。他们建议,对于需要更强持久性保证的用户,可以将 sync_interval 设置为 always,但这会将吞吐量降低到每秒几百条消息。

更深层次的风险:文件损坏与脑裂

除了 fsync 的问题,Jepsen 还发现了几个在文件损坏场景下,可能导致更严重后果的漏洞。

数据块 (.blk) 文件损坏导致大量数据丢失

Jepsen 发现,即使只是在一个 5 节点的集群中的少数节点上,对 JetStream 的数据块文件 (.blk) 引入单个比特位的错误或截断,也可能导致集群丢失大量已确认的写入,甚至出现数据分歧(脑裂)——不同的节点返回不同的消息集,整个流的数据变得像“瑞士奶酪”一样千疮百孔。

在一个测试中,对两个节点的文件进行比特翻转,最终导致三个节点丢失了高达 78% 的已确认消息。

快照 (snapshot) 文件损坏导致流被删除

更令人不安的是,当快照文件损坏时,一个节点可能会错误地认为某个流已经“孤立”(orphaned),并做出删除该流所有数据的决定。在 Jepsen 的测试中,一个数据已损坏的节点,竟然成功地成为了集群的领导者,并立即删除了包含所有测试消息的流,导致了数据的完全丢失

这暴露了 NATS 在面对数据损坏时,其领导者选举和恢复机制的潜在脆弱性。

一个单一的 OS 崩溃也可能导致数据丢失和脑裂

Jepsen 还设计了一个精巧的实验,证明在异步网络环境下,仅仅一次单节点的操作系统崩溃(模拟断电),就可能导致已提交写入的丢失和持久性的脑裂

场景复现

  1. Leader 节点将一次写入复制给了 Follower A,并收到了确认。此时,写入在 Leader 和 Follower A 的内存中被认为是“已提交”的。
  2. Leader 节点在将这次写入刷入磁盘之前,也还未成功复制给 Follower B 的时候,突然发生了 OS 崩溃。
  3. Leader 节点重启后,它内存中那份“已提交”的写入已经丢失
  4. 此时,集群中存在两个“干净”的节点(重启后的 Leader 和从未收到写入的 Follower B)。它们可以组成新的多数派,选举出新的领导者,并继续处理请求。
  5. 从这个新的多数派的视角看,那次丢失的写入仿佛从未发生过

Jepsen 的测试成功地在 NATS 2.12.1 中复现了这一理论场景,并导致了持久性的副本分歧(脑裂)。

Go 开发者的核心启示

这份报告,并非对 NATS 的“死刑判决”,而是一次深刻的、关于分布式系统复杂性的现实教育。对于 Go 社区的开发者,它至少带来了三点核心启示:

  1. 魔鬼在默认配置中:永远不要盲目相信软件的默认配置。NATS JetStream 默认的sync_interval,是为了性能而优化的,而非持久性。你需要根据你的业务场景(是能容忍丢失少量近期数据,还是要求金融级别的“绝不丢失”),来审慎地做出权衡和配置。

  2. “已确认”不等于“已落盘”:在与任何分布式存储系统交互时,请仔细阅读其文档,搞清楚一个“成功的”写入响应,其背后的持久性承诺到底是什么级别的。是“已写入 Leader 内存”、“已写入多数派内存”,还是“已在多数派节点上 fsync 到磁盘”?这三者之间,差之毫厘,谬以千里。

  3. 拥抱混沌工程:Jepsen 的工作方法,正是混沌工程思想的极致体现。它告诉我们,仅仅通过单元测试和集成测试,永远无法发现分布式系统在真实世界故障模式下的脆弱性。我们需要引入更复杂的、模拟真实世界混乱(网络分区、进程暂停、磁盘错误)的测试手段。

小结

NATS 依然是一个出色、高性能的 Go 原生消息系统。Jepsen 的这份报告,如同一次严苛的“体检”,指出了它在追求极致性能的过程中,所做出的一些高风险权衡。对于我们 Gopher 而言,这不仅是一次了解 NATS 内部工作原理的机会,更是一堂关于如何批判性地思考、审慎地选择和配置我们所依赖的基础设施的必修课。

资料链接:https://jepsen.io/analyses/nats-2.12.1


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

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

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


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

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

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

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

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


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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats