标签 Gopher 下的文章

从 Go “叛逃”到 Java,再回归:一位开发者关于“魔法”与“显式”的深度反思

本文永久链接 – https://tonybai.com/2025/10/22/back-to-go-after-defection-to-java

大家好,我是Tony Bai。

“我离开了 Go,因为我觉得它啰嗦又笨重。我以为编程本该是简单轻松的……但事实证明,河对岸的草不见得更绿。”

近日,在 r/golang 社区,一篇标题为《一篇完全没有建设性但又无比真实的,关于 Go 和 Java 的咆哮》的帖子引发了热议。作者讲述了一个“Gopher 叛逆-回归”的经典故事:因不满 Go 社区对 ORM 和 DI 框架的“抵触”,以及 Go 语言本身的“繁琐”,他转投了企业级 Java 的怀抱。然而,在亲身体验了 Java 生态中无处不在的“魔法”之后,他如今“无比怀念 Golang”。

这篇“咆哮”,与其说是在抱怨,不如说是一次深刻的顿悟。它以一种极具戏剧性的方式,揭示了 Go 与 Java 在设计哲学上的根本冲突,以及 Go 语言“显式优于隐式”这一核心价值观的真正分量。

“魔法”的代价——“我根本不知道火箭从哪儿来”

作者坦言,他最初无法理解人们为何抱怨 Java 的“魔法”。框架“做了所有繁重的工作,你只需要创建和注册工厂,不是吗?”

在亲身实践后,他发出了痛苦的哀嚎:“我终于明白了。我无比痛恨 Java 使用的魔法。你根本不可能知道火箭是从哪里发射的。”

他精准地指出了几个让他崩溃的“魔法”重灾区:

Spring 的依赖注入 (DI):“@Service my ass”

在 Spring 框架中,一个简单的 @Service 注解,就能让一个类被自动扫描、实例化并注入到任何需要它的地方。这看似便捷,但当系统变得复杂时,它就成了一个黑盒。作者咆哮道:“你只是接受了某个地方、某个时候会调用你的工厂——只要你设置了正确的 profile。@Service my ass。”

这种控制反转 (IoC) 的极致,让代码的调用关系变得极其隐晦。想找到一个 JWT 令牌的验证逻辑在哪里被触发?想知道 PEM 密钥在哪里被设置?祝你好运。这与 Go 中清晰、明确的函数调用和依赖传递,形成了鲜明的对比。

Hibernate 的 ORM:“它写的查询简直骇人听闻”

作者曾是 TypeORM 的忠实拥趸,但 Hibernate 让他领教了重量级 ORM 的恐怖。他质问道:“为什么它不直接用 JOIN,而是去执行那 40 条额外的查询?为什么我只是想取个名字,它却加载了整个银行数据?”

这正是“魔法”的另一面:为了提供一个看似简单的对象操作接口,ORM 在底层生成了极其复杂、低效、且难以预测的 SQL 查询(即著名的 N+1 问题)。当魔法失效,你需要深入调试时,你面对的将是 HQL (Hibernate Query Language) 这种“又一门需要学习的查询语言”,而不是你早已精通的 SQL。

MapStruct 的代码生成:“我如何给它加断点?”

从模型 (Model) 到数据传输对象 (DTO) 的转换,在 Java 中也充满了“魔法”。像 MapStruct 这样的库,通过注解和代码生成,自动完成对象之间的映射。作者的质问直击要害:“你从中得到了什么?我如何给它加一个断点?”

当代码不再是你亲手编写,而是由工具在编译时“变”出来的时候,你就失去了最宝贵的武器:可调试性可预测性

社区的激辩——Go 真的“反框架”吗?

这篇“咆哮”自然也引发了社区的激烈辩论。许多评论者指出,作者所憎恨的,并非 Java 语言本身,而是其生态中过度使用“魔法”的特定框架文化(尤其是 Spring 和 Hibernate)。

同时,也有 Gopher 指出,Go 社区并非完全拒绝高级抽象。像 Uber 开源的 fx 框架,就是一个功能强大的依赖注入库;而 gomock 也是从 Go 官方团队交由 Uber 维护的重要项目。

然而,这场辩论最终揭示了一个核心的文化差异

  • Java 企业级生态:倾向于提供“全家桶”式的、重量级的框架。这些框架试图用“魔法”为开发者包办一切,隐藏复杂性。其哲学是“约定优于配置”的极致体现。
  • Go 社区生态:更倾向于提供小巧、正交、可组合的库。它鼓励开发者理解并亲手“管道”这些构建块。其哲学是“显式优于隐式”。Go 开发者不害怕“重新发明轮子”,因为他们认为“对轮子的控制权”本身就是一种价值。

重新审视 Go 的“繁琐”——是缺陷,还是守护?

作者的回归之旅,让我们得以用一个全新的视角,重新审视那些曾被他(以及许多初学者)视为“繁琐”的 Go 特性。

if err != nil:繁琐背后的清晰

当社区讨论 Go 的“繁琐”时,99% 的情况下,他们指的都是 if err != nil。然而,在经历了 Java 中可以随时随地抛出、难以追踪的未经检查的异常 (Unchecked Exceptions) 之后,Go 这种将错误作为普通值的处理方式,其优势便凸显出来:

  • 清晰的控制流:错误处理路径是代码中明确、可见的一部分,而不是通过 try-catch 或全局异常处理器实现的“隐形跳转”。
  • 强制的责任:编译器强制你关注每一个可能出错的地方,这从根本上提升了代码的健壮性。

拥抱 database/sql:显式控制的自由

在关于 ORM 的激烈辩论中,一位 Gopher 的评论掷地有声:“当魔法失效时,从 ORM 回退到 SQL 查询,比从一开始就写 SQL 要痛苦十倍。”

这并非是在断言“Go 社区完全拒绝 ORM”。事实上,Go 生态中拥有像 GORM、ent、sqlc、sqlx 这样流行且功能强大的数据访问工具。然而,与 Java 生态中 Hibernate 几乎一统天下的地位不同,Go 社区对于是否使用 ORM,以及如何使用,始终保持着一种审慎和多元的态度

这种态度的根源,在于 Go 的标准库 database/sql。它本身并非一个 ORM,而是一个轻量级的、提供了数据库操作最小抽象的接口。它刻意地将开发者保留在离 SQL 很近的地方。

这种“刻意的简陋”,恰恰赋予了开发者一种宝贵的自由:

  1. 完全的 SQL 控制权:你永远不必去猜测框架会生成什么样的“怪物”SQL。你可以亲手编写最高效、最符合你业务场景的查询,可以轻松地使用数据库的高级特性,也可以在需要时对查询进行精确的性能调优。
  2. 清晰的数据流:数据从数据库行到你的 struct 的映射过程是显式的。无论是 rows.Scan() 还是 sqlx 的 db.StructScan(),你都能清晰地看到数据的流转路径。
  3. 更低的认知负荷:学习 database/sql 和基础 SQL,其学习曲线远比掌握一个像 Hibernate 这样庞大、复杂的 ORM 框架要平缓得多。

当然,这意味着你需要编写更多的“繁琐”的 SQL 语句和手动映射代码。但 Go 社区的普遍哲学认为,这种可预测、可控制的“繁琐”,远胜于那种在 90% 的时间里都很神奇,但在剩下的 10% 的时间里会让你陷入调试地狱的“魔法”。

对于许多 Gopher来说,选择 database/sql 或 sqlx 这样的轻量级工具,并非“重新发明轮子”,而是一种主动的选择——选择将复杂性掌握在自己手中,而不是将其外包给一个难以捉摸的黑盒。

小结:简单性的“甜蜜点”

这位“叛逆”Gopher 的回归故事,是一堂关于软件设计哲学的生动课程。它告诉我们,设计一门简单的语言并不容易

“要让事情变简单,你必须隐藏复杂性。但如果你隐藏了太多的复杂性,你实际上会让事情变得更复杂——因为复杂性只是被隐藏了,而非被消除了。”

Java 的“魔法”生态,通过注解、反射和代码生成,将复杂性深深地隐藏在了一个难以触及的黑盒中。而 Go,则努力地寻找着一个“甜蜜点”:它提供了足够高的抽象(如 Goroutine 和 GC),让你不必关心线程调度和内存分配的底层细节;同时,它又保持了足够的透明度,让你能清晰地看到程序的控制流和数据流。

最终,这场从 Go 到 Java 再回到 Go 的旅程,并非一次简单的技术选择,而是一次深刻的哲学回归。它证明了在长期维护、大规模协作和复杂问题调试的战场上,清晰、显式和可预测性,远比任何华丽的“魔法”都更加珍贵。

资料链接:https://www.reddit.com/r/golang/comments/1o7u5b6/a_completely_unproductive_but_truthful_rant_about/


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

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

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

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

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


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

Go 考古:defer 的“救赎”——从性能“原罪”到零成本的“开放编码”

本文永久链接 – https://tonybai.com/2025/10/15/go-archaeology-defer

大家好,我是Tony Bai。

在 Go 语言的所有关键字中,defer 无疑是最具特色和争议的之一。它以一种近乎“魔法”的方式,保证了资源清理逻辑的执行,极大地提升了代码的可读性和健壮性。f, _ := os.Open(“…”); defer f.Close() 这一行代码,几乎是所有 Gopher 的肌肉记忆

然而,在这份优雅的背后,曾几何时,defer 却背负着“性能杀手”的恶名。在 Go 的历史长河中,无数资深开发者,包括标准库的维护者们,都曾被迫在代码的可维护性与极致性能之间做出痛苦的抉择,含泪删掉 defer 语句,换上丑陋但高效的手动 if err != nil 清理逻辑。

你是否好奇:

  • defer 的早期实现究竟“慢”在哪里?为什么一个简单的函数调用会被放大数十倍的开销?
  • 从 Go 1.13 到 Go 1.14,Go 团队究竟施展了怎样的“魔法”,让 defer 的性能提升了超过 10 倍,几乎达到了与直接调用函数相媲美的程度?
  • 为了实现这场“性能革命”,defer 在编译器和运行时层面,经历了怎样一场从“堆分配”到“栈上开放编码(open-coded defer)”的“心脏手术”?

今天,就让我们再一次化身“Go 语言考古学家”,在Go issues以及Go团队那些著名的演讲资料中挖掘,并结合 Go 官方的设计文档,深入 defer 性能演进的“地心”,去完整地再现这场波澜壮阔的“救赎之路”。

“事后”的智慧:Defer 的设计哲学与独特性

在我们深入 defer 性能的“地心”之前,让我们先花点时间,站在一个更高的维度,欣赏一下 defer 这个语言构造本身的设计之美。defer机制 并非 Go 语言的首创,许多语言都有类似的机制来保证资源的确定性释放,但Go中defer 机制的实现方式却独树一帜,充满了 Go 语言独有的哲学。

保证“清理”的殊途同归

下面是几种主流语言的资源管理范式,这让我们能更清晰地看清 defer 的坐标:

  • C++ 的 RAII (Resource Acquisition Is Initialization):

这是一种极其强大和高效的范式。资源(如文件句柄、锁)的生命周期与一个栈上对象的生命周期绑定。当对象离开作用域时,其析构函数 (destructor) 会被编译器自动调用,从而释放资源。RAII 的优点是静态可知、零运行时开销。但它强依赖于 C++ 的析构函数和对象生命周期管理,对于一门拥有垃圾回收(GC)的语言来说,这种模式难以复制。

  • Java/Python 的 try-finally:

这是另一种常见的保证机制。finally 块中的代码,无论 try 块是正常结束还是抛出异常,都保证会被执行。try-finally 同样是静态可知的,编译器能明确地知道在每个代码块退出时需要执行什么。

这两种机制的共同点是:它们都是块级 (block-level) 的,并且清理逻辑的位置往往与资源获取的位置相距甚远

Defer 的三大独特优势

相比之下,Go 的 defer 提供了三种独特的优势,使其在代码的可读性和灵活性上脱颖而出:

  1. 就近原则,极致清晰 (Clarity):

这是 defer 最为人称道的优点。清理逻辑(defer f.Close())可以紧跟在资源获取逻辑(os.Open(…))之后。这种“开闭成对”的书写方式,极大地降低了程序员的心智负担,你再也不用在函数末尾的 finally 块和函数开头的资源申请之间来回跳转,从而有效避免了忘记释放资源的低级错误。

  1. 函数级作用域,保证完整性 (Robustness):

defer 的执行时机与函数(而非代码块)的退出绑定。这意味着,无论函数有多少个 return 语句,无论它们分布在多么复杂的 if-else 分支中,所有已注册的 defer 调用都保证会在函数返回前被执行。这对于重构和维护极其友好——你可以随意增删 return 路径,而无需担心破坏资源清理的逻辑。更重要的是,在 panic 发生时,defer 依然会被执行,这为构建健壮的、能从异常中恢复的常驻服务提供了坚实的基础。

  1. 动态与条件执行,极致灵活 (Flexibility):

这是 defer 与 RAII 和 try-finally 最本质的区别。defer 是一个完全动态的语句,它可以出现在 if 分支、甚至 for 循环中。

if useFile {
    f, err := os.Open("...")
    // ...
    defer f.Close() // 只在文件被打开时,才注册清理逻辑
}

这种条件式清理的能力,是其他静态机制难以优雅表达的。

“动态”的双刃剑

然而,defer 的动态性也是一把双刃剑。

正是因为它可以在循环中被调用,defer 在理论上可以被执行任意多次。编译器无法在编译期静态地知道一个函数到底会注册多少个 defer 调用。

这种不确定性,迫使 Go 的早期设计者必须借助运行时的帮助,通过一个动态的链表来管理 defer 调用栈。这就引出了我们即将要深入探讨的核心问题——为了这份极致的灵活性和清晰性,defer 在诞生之初,付出了怎样的性能代价?而 Go 团队又是如何通过一场载入史册的编译器革命,几乎将其“抹平”的?

现在,让我们带上“考古工具”,正式开始我们的性能探源之旅。

“原罪”:Go 1.13 之前的 defer 为何如此之慢?

在GopherCon 2020上,Google工程师Dan Scales为大家进行了一次经常的有关defer性能提升的演讲,在此次演讲中,他先为大家展示了一张令人震惊的性能对比图,也揭示了一个残酷的事实:在 Go 1.12 及更早的版本中,一次 defer 调用的开销高达 44 纳秒,而一次普通的函数调用仅需 1.7 纳秒,相差超过 25 倍

这巨大的开销从何而来?答案隐藏在早期的实现机制中:一切 defer 都需要运行时(runtime)的深度参与,并且都涉及堆分配(heap allocation)。

让我们通过 Go 团队的内部视角,来还原一下当时 defer 的工作流程:

  1. 创建 _defer 记录: 每当你的代码执行一个 defer 语句时,编译器会生成代码,在堆上分配一个 _defer 结构体。这个结构体就像一张“任务卡”,记录了要调用的函数指针、所有参数的拷贝,以及一个指向下一个 _defer 记录的指针。

  1. deferproc 运行时调用: 创建好“任务卡”后,程序会调用运行时的 runtime.deferproc 函数。这个函数负责将这张新的“任务卡”挂载到当前 goroutine 的一个链表上。这个链表,我们称之为“defer 链”。

  1. deferreturn 运行时调用: 当函数准备退出时(无论是正常 return 还是 panic),编译器会插入一个对 runtime.deferreturn 的调用。这个函数会像“工头”一样,从 defer 链的尾部开始(后进先出 LIFO),依次取出“任务卡”,并执行其中记录的函数调用。

看到了吗?每一次 defer,都至少包含:

  • 一次堆内存分配(创建 _defer 记录)。
  • 两次到运行时的函数调用 (deferproc 和 deferreturn)。

堆分配本身就是昂贵的操作,因为它需要加锁并与垃圾回收器(GC)打交道。而频繁地在用户代码和 runtime 之间切换,也带来了额外的开销。正是这“三座大山”,让 defer 在高性能场景下变得不堪重负。

Go 1.13 迈出了优化的第一步:对于不在循环中的 defer,编译器尝试将 _defer 记录分配在栈上。这避免了堆分配和 GC 的压力,使得 defer 的开销从 44ns 降低到了 32ns。这是一个显著的进步,但离“零成本”的目标还相去甚甚远。defer 依然需要与 runtime 交互,依然需要构建那个链表。

“革命”:Go 1.14 的 Open-Coded Defer

Go 1.14 带来的,不是改良,而是一场彻底的革命。Dan Scales 和他的同事们提出并实现了一个全新的机制,名为 “开放编码的 defer (Open-Coded Defer)”。

其核心思想是:对于那些简单的、非循环内的 defer,我们能不能彻底摆脱 runtime,让编译器直接在函数内生成所有清理逻辑?

答案是肯定的。这场“革命”分为两大战役:

战役一:在函数退出点直接生成代码

编译器不再生成对 deferproc 的调用。取而代之的是:

  1. 栈上“专属”空间: 在函数的栈帧(stack frame)中,为每个 defer 调用的函数指针和参数预留“专属”的存储位置。
  2. 位掩码(Bitmask): 同样在栈上,引入一个 _deferBits 字节。它的每一个 bit 位对应一个 defer 语句。当一个 defer 被执行时,不再是创建 _defer 记录,而是简单地将 _deferBits 中对应的 bit 位置为 1。这是一个极快、极轻量的操作。

当函数准备退出时,编译器也不再调用 deferreturn。它会在每一个 return 语句前,插入一段“开放编码”的清理逻辑。这段逻辑就像一个智能的“清理机器人”,它会逆序检查 _deferBits 的每一位。如果 bit 位为 1,就从栈上的“专属空间”中取出函数指针和参数,直接发起调用:

看到了吗?在正常执行路径下,整个过程没有任何堆分配,没有任何 runtime 调用!defer 的成本,被降低到了几次内存写入(保存参数和设置 bit 位)和几次 if 判断。这使得其开销从 Go 1.13 的 32ns 骤降到了惊人的 3ns,与直接调用函数(1.7ns)的开销几乎在同一个数量级!

战役二:与 panic 流程的“深度整合”

你可能会问:既然没有 _defer 链表了,当 panic 发生时,runtime 怎么知道要执行哪些 defer 呢?

这正是 Open-Coded Defer 设计中最精妙、也最复杂的部分。Go 团队通过一种名为 funcdata 的机制,在编译后的二进制文件中,为每个使用了 Open-Coded Defer 的函数,都附上了一份“藏宝图”。

这份“藏宝图”告诉 runtime:

  • 这个函数使用了开放编码。
  • _deferBits 存储在栈帧的哪个偏移量上。
  • 每个 defer 调用的函数指针和参数,分别存储在栈帧的哪些偏移量上。

当 panic 发生时,runtime 的 gopanic 函数会扫描 goroutine 的栈。当它发现一个带有 Open-Coded Defer 的栈帧时,它就会:

  1. 读取这份“藏宝图” (funcdata)。
  2. 根据“藏宝图”的指引,在栈帧中找到 _deferBits。
  3. 根据 _deferBits 的值,再从栈帧中找到并执行所有已激活的 defer 调用。

这个设计,巧妙地将 defer 的信息编码在了栈帧和二进制文件中,使得 panic 流程依然能够正确地、逆序地执行所有 defer,同时保证了正常执行路径的极致性能。

下面是Dan Scales给出的一个defer性能对比结果:

我们看到:采用Open-coded defer进行优化后,defer的开销非常接近与普通的函数调用了(1.x倍)。

小结:“救赎”的完成与新的约定

defer 的性能“救赎之路”,从 Go 1.12 的 44ns,到 Go 1.13 的 32ns(栈分配 _defer 记录),再到 Go 1.14 的 3ns(Open-Coded Defer),其演进历程波澜壮阔,是 Go 团队追求极致性能与工程实用性的最佳例证。

下面是汇总后的各个Go版本的defer实现机制与开销数据:

这场“革命”之后,Dan Scales 在演讲的最后发出了强有力的呼吁,这也应该成为我们所有 Gopher 的新共识:

defers should now be used whenever it makes sense to make code clearer and more maintainable. defer should definitely not be avoided for performance reasons.
(现在,只要能让代码更清晰、更易于维护,就应该使用 defer。绝对不应该再因为性能原因而避免使用 defer。)

defer 的“原罪”已被救赎。从现在开始,请放心地使用它,去编写更优雅、更健壮的 Go 代码吧。

参考资料

  • Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case – https://go.googlesource.com/proposal/+/master/design/34481-opencoded-defers.md
  • GopherCon 2020: Implementing Faster Defers by Dan Scales – https://www.youtube.com/watch?v=DHVeUsrKcbM
  • cmd/compile: allocate some defers in stack frames – https://github.com/golang/go/issues/6980

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

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

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

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

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


想系统学习Go,构建扎实的知识体系?

我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏,内容全面升级,同步至Go 1.24。首发期有专属五折优惠,不到40元即可入手,扫码即可拥有这本300页的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