<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; 设计模式</title>
	<atom:link href="http://tonybai.com/tag/%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Thu, 09 Apr 2026 00:20:15 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>为什么 Go 社区强调避免不必要的抽象？—— 借用海德格尔哲学寻找“正确”的答案</title>
		<link>https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/</link>
		<comments>https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/#comments</comments>
		<pubDate>Fri, 16 Jan 2026 00:04:27 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Abstraction]]></category>
		<category><![CDATA[Analytictruth]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[BeingandTime]]></category>
		<category><![CDATA[CognitiveLoad]]></category>
		<category><![CDATA[Coincidence]]></category>
		<category><![CDATA[DesignPatterns]]></category>
		<category><![CDATA[Essentialtruth]]></category>
		<category><![CDATA[Function]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GoCommunity]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherConUK2025]]></category>
		<category><![CDATA[Heidegger]]></category>
		<category><![CDATA[Inappropriateabstraction]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[io.Reader]]></category>
		<category><![CDATA[JohnCinnamond]]></category>
		<category><![CDATA[Kant]]></category>
		<category><![CDATA[monad]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[Presentathand]]></category>
		<category><![CDATA[Readytohand]]></category>
		<category><![CDATA[Socialcost]]></category>
		<category><![CDATA[Synthetictruth]]></category>
		<category><![CDATA[Unnecessaryabstractions]]></category>
		<category><![CDATA[上手状态]]></category>
		<category><![CDATA[不必要的抽象]]></category>
		<category><![CDATA[不恰当的抽象]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[分析真理]]></category>
		<category><![CDATA[在手状态]]></category>
		<category><![CDATA[存在与时间]]></category>
		<category><![CDATA[巧合]]></category>
		<category><![CDATA[康德]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[本质真理]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[海德格尔]]></category>
		<category><![CDATA[社会成本]]></category>
		<category><![CDATA[综合真理]]></category>
		<category><![CDATA[认知负担]]></category>
		<category><![CDATA[设计模式]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5730</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction 大家好，我是Tony Bai。 “Go 的哲学强调避免不必要的抽象。” 这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时，往往会收到这样的反馈。这句话本身没有错，但难点在于：到底什么是“不必要”的？ 函数是抽象吗？汇编是抽象吗？如果不加定义地“避免抽象”，我们最终只能对着硅片大喊大叫。 在 GopherCon UK 2025 上，John Cinnamond 做了一场与众不同的演讲。他没有展示任何炫酷的并发模式，而是搬出了马丁·海德格尔（Martin Heidegger）和伊曼努尔·康德（Immanuel Kant），试图用哲学的视角，为我们解开关于 Go 抽象的终极困惑。 注：海德格尔与《存在与时间》 马丁·海德格尔（Martin Heidegger）是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中，深入探讨了人（此在）如何与世界互动。John Cinnamond 在演讲中引用的核心概念——“上手状态” (Ready-to-hand) 和 “在手状态” (Present-at-hand)，正是海德格尔用来描述我们与工具（如锤子）之间关系的术语。这套理论极好地解释了为什么优秀的工具（或代码抽象）应该是“透明”的，而糟糕的工具则会强行占据我们的注意力。 我们都在使用的“必要”抽象 首先，让我们承认一个事实：编程本身就是建立在无数层抽象之上的。 泛型：这是对类型的抽象。虽然 Go 曾长期拒绝它，但在技术上它是必要的，否则我们将充斥着重复代码。 接口：这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。 函数：这是对指令序列的抽象。没有它，我们只能写长长的 main 函数。 汇编语言：这是对机器码的抽象。 所以，当我们说“避免不必要的抽象”时，我们真正想表达的其实是——避免“不恰当” (Inappropriate) 的抽象。 那么，如何判断一个抽象是否“恰当”？ 何为抽象？—— [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction">本文永久链接</a> &#8211; https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction</p>
<p>大家好，我是Tony Bai。</p>
<p><strong>“Go 的哲学强调避免不必要的抽象。”</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-2.png" alt="" /></p>
<p>这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时，往往会收到这样的反馈。这句话本身没有错，但难点在于：<strong>到底什么是“不必要”的？</strong></p>
<p>函数是抽象吗？汇编是抽象吗？如果不加定义地“避免抽象”，我们最终只能对着硅片大喊大叫。</p>
<p>在 GopherCon UK 2025 上，John Cinnamond 做了<a href="https://www.youtube.com/watch?v=oP_-eHZSaqc">一场与众不同的演讲</a>。他没有展示任何炫酷的并发模式，而是搬出了马丁·海德格尔（Martin Heidegger）和伊曼努尔·康德（Immanuel Kant），试图用哲学的视角，为我们解开关于 Go 抽象的终极困惑。</p>
<blockquote>
<p><strong>注：海德格尔与《存在与时间》</strong></p>
<p>马丁·海德格尔（Martin Heidegger）是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中，深入探讨了人（此在）如何与世界互动。John Cinnamond 在演讲中引用的核心概念——<strong>“上手状态” (Ready-to-hand)</strong> 和 <strong>“在手状态” (Present-at-hand)</strong>，正是海德格尔用来描述我们与工具（如锤子）之间关系的术语。这套理论极好地解释了为什么优秀的工具（或代码抽象）应该是“透明”的，而糟糕的工具则会强行占据我们的注意力。</p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/2026/distributed-system-guide-qr.png" alt="img{512x368}" /></p>
<h2>我们都在使用的“必要”抽象</h2>
<p>首先，让我们承认一个事实：<strong>编程本身就是建立在无数层抽象之上的。</strong></p>
<ul>
<li><strong>泛型</strong>：这是对类型的抽象。虽然 Go 曾长期拒绝它，但在技术上它是必要的，否则我们将充斥着重复代码。</li>
<li><strong>接口</strong>：这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。</li>
<li><strong>函数</strong>：这是对指令序列的抽象。没有它，我们只能写长长的 main 函数。</li>
<li><strong>汇编语言</strong>：这是对机器码的抽象。</li>
</ul>
<p>所以，当我们说“避免不必要的抽象”时，我们真正想表达的其实是——<strong>避免“不恰当” (Inappropriate) 的抽象</strong>。</p>
<p>那么，如何判断一个抽象是否“恰当”？</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-3.png" alt="" /></p>
<h2>何为抽象？—— 一场有目的的“细节隐藏”</h2>
<p>在深入探讨“正确”的抽象之前，我们必须先回到最基本的定义。John Cinnamond 在演讲中给出了一个精炼而深刻的定义：</p>
<blockquote>
<p><strong>“抽象是一种表示 (Representation)，但它是一种刻意移除被表示事物某些细节的表示。”</strong></p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-4.png" alt="" /></p>
<p>让我们拆解这个定义：</p>
<ol>
<li>抽象是一种“表示”，而非事物本身<br />
它不是代码的实体，而是代码的地图或模型。例如，一辆模型汽车是真实汽车的表示，但 Gopher 吉祥物是地鼠的抽象——它刻意省略了真实地鼠的所有细节，只保留了核心特征。</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-6.png" alt="" /></p>
<ol>
<li>抽象是“有目的的”细节移除<br />
这与仅仅是“不精确”或“粗糙”不同。抽象是有意为之的，它不试图精确描绘所有方面，而是<strong>只关注某个特定维度</strong>。</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-5.png" alt="" /></p>
<ol>
<li>抽象在编程中具有动态性
<ul>
<li>不确定引用 (Indefinite Reference)：一个抽象（如 io.Reader）通常可以指代许多不同的具体实现。</li>
<li>开放引用 (Open Reference)：抽象的内容或它所指代的事物可以随着时间而改变。</li>
</ul>
</li>
</ol>
<p><strong>为什么要刻意移除细节？John 总结了几个核心动机：</strong></p>
<ul>
<li>避免重复代码：将重复的逻辑提取到抽象中。</li>
<li>统一不同的实现：允许以统一的方式处理本质上不同的数据结构（如所有实现了 Read 方法的类型）。</li>
<li>推迟细节：隐藏那些当下不重要、或开发者不关心的细节（例如，你坐火车参会，不需要知道每节车厢的编号）。</li>
<li>揭示领域概念：用抽象来更好地表达业务领域中的核心概念。</li>
<li>驾驭复杂性：这是最核心的理由——没有抽象，我们无法在大脑中一次性处理所有细节，也就无法解决复杂的问题。</li>
</ul>
<p><strong>但请记住，并非所有抽象都是一样的。John 将它们分为三类：</strong></p>
<ol>
<li>
<p>基于“它是如何工作的” (How it works)<br />
这是为了代码复用而提取的抽象。例如，你发现两处代码都在做“检查用户是否是管理员”的逻辑，于是将其提取为一个函数。这种抽象关注的是内部机制。 <em>(这类抽象通常比较脆弱，一旦实现细节变化，抽象可能就会失效。)</em></p>
</li>
<li>
<p>基于“它做了什么” (What it does)<br />
这是 Go 语言中接口（Interface）最典型的用法。例如 io.Reader，我们不关心它是文件还是网络连接，我们只关心它能“读取字节”。这是一种行为抽象。</p>
</li>
<li>
<p>基于“它是什么” (What it is)<br />
这是基于领域模型的抽象。例如一个 User 结构体，它代表了系统中的一个实体。这种抽象关注的是本质属性。</p>
</li>
</ol>
<p>在现实中，好的抽象往往是这三者的混合体，但在设计时，明确你是在抽象“行为”还是“实现”，对于判断抽象的质量至关重要。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-7.png" alt="" /></p>
<p>理解了抽象的本质，我们可能会觉得：既然抽象能驾驭复杂性，那是不是越多越好？</p>
<p>且慢。在急于评判一个抽象是否“恰当”之前，我们必须先意识到一个常被技术人员忽略的现实：<strong>抽象不仅存在于代码中，更存在于人与人的互动里。</strong> 这将我们引向了一个更现实的考量维度。</p>
<h2>抽象的代价 —— 代码是写给人看的</h2>
<p>John 提醒我们，软件开发本质上是一项<strong>社会活动 (Social Activity)</strong>。</p>
<blockquote>
<p><strong>“除非你是为了自己写着玩，否则你的代码总是写给别人看的。团队是一个微型社会，它有自己的习俗、信仰和‘传说’(Lore)。”</strong></p>
</blockquote>
<p>引入一个新的抽象，本质上是在向这个微型社会引入一种新的文化或规则。这意味着：</p>
<ol>
<li><strong>你需要支付“社会成本”</strong>：如果这个抽象与团队现有的习惯（Lore）相悖——比如在一个从未用过函数式编程的 Go 团队里强推 Monad——你将遭遇巨大的阻力。</li>
<li><strong>团队的保守性</strong>：成熟的团队往往趋于保守，改变既定习惯需要巨大的能量。你不能仅仅因为一个抽象在理论上很美就引入它，你必须证明<strong>它的收益足以覆盖它带来的社会摩擦成本</strong>。</li>
<li><strong>认知负担是共享的</strong>：一个抽象对你来说可能很清晰，但如果它让队友感到困惑，那就是在消耗团队的整体智力资源。</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-8.png" alt="" /></p>
<p>因此，当我们评判一个抽象是否“恰当”时，不能只看代码本身，还必须看它是否<strong>“合群”</strong>。这正是我们接下来要引入海德格尔哲学的现实基础。</p>
<h2>锤子哲学 —— “上手状态” vs. “在手状态”</h2>
<p>John 引用了海德格尔在《存在与时间》中的一个著名概念：<strong>Ready-to-hand (上手状态)</strong> 与 <strong>Present-at-hand (在手状态)</strong>。</p>
<ul>
<li><strong>上手状态 (Ready-to-hand)</strong>：当你熟练使用一把锤子钉钉子时，你的注意力完全在钉钉子这件事上，锤子本身在你意识中是“透明”的。你感觉不到它的存在，它只是你身体的延伸。</li>
<li><strong>在手状态 (Present-at-hand)</strong>：当锤子突然坏了（比如锤头掉了），或者你拿到一把设计奇特的陌生工具时，你的注意力被迫从“钉钉子”转移到了“锤子”本身。你开始审视它的构造、重量和用法。</li>
</ul>
<p><strong>这对代码意味着什么？</strong></p>
<ul>
<li><strong>好的抽象是“上手状态”的</strong>：比如 for 循环。作为经验丰富的开发者，你使用它时是在思考“我要遍历数据”，而不是“这个循环语法是怎么编译的”。它透明、顺手，让你专注于解决问题。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-9.png" alt="" /></p>
<ul>
<li><strong>坏的抽象是“在手状态”的</strong>：比如一个复杂的、过度设计的 ORM 或者一个晦涩的 Monad 库。当你使用它时，你的思维被迫中断，你需要停下来思考：“这个函数到底在干什么？这个参数是什么意思？”</li>
</ul>
<p>如果一个抽象让你频繁地从“解决业务问题”中抽离出来去思考“工具本身”，那么它很可能是一个<strong>坏的抽象</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-10.png" alt="" /></p>
<blockquote>
<p>注：通过学习和实践，在手状态 (Present-at-hand)的抽象可以转换为 上手状态 (Ready-to-hand)的抽象。</p>
</blockquote>
<h2>真理的检验 —— “本质真理” vs. “巧合真理”</h2>
<p>接着，John 又搬出了康德关于真理的分类，引导我们思考抽象的<strong>持久性</strong>。</p>
<ul>
<li><strong>分析真理 (Analytic Truth)</strong>：由定义决定的真理。比如“所有单身汉都没结婚”。在代码中，这就像 unnecessary abstractions are unnecessary，虽然正确但没啥用。</li>
<li><strong>综合真理 (Synthetic Truth)</strong>：由外部事实决定的真理。比如“外面在下雨”。它的真假取决于环境，随时可能变。</li>
<li><strong>本质真理 (Essential Truth)</strong>：虽然不是由定义决定，但反映了世界的本质规律。比如“物质由原子构成”。</li>
</ul>
<p><strong>这对抽象意味着什么？</strong></p>
<p>当你提取一个抽象时，问问自己：<strong>它代表的是代码的“本质真理”，还是仅仅是一个“巧合”？</strong></p>
<p>举个例子：你有一段过滤商品的代码，可以按“价格”过滤，也可以按“库存”过滤。你提取了一个 Filter(Product) bool 的抽象。</p>
<ul>
<li>如果未来所有的过滤需求（如颜色、大小）都能用这个签名解决，那么你发现了一个<strong>本质真理</strong>。这个抽象是稳固的。</li>
<li>但如果突然来了一个需求：“过滤掉重复的商品”，这个需求需要知道<strong>所有</strong>商品的状态，而不仅仅是单个商品。原本的 Filter(Product) bool 签名瞬间失效。</li>
</ul>
<p>如果你提取的抽象仅仅是因为几段代码“长得像”（巧合），而不是因为它们“本质上是一回事”，那么当需求变更时，这个抽象就会崩塌，变成一种负担。</p>
<p>由此可见，好的抽象不是被<strong>创造</strong>出来的，而是被<strong>发现</strong>（Recognized）出来的。它们是对代码中某种本质结构的捕捉。</p>
<h2>实战指南 —— 如何引入抽象？</h2>
<p>最后，John 给出了一个评估抽象是否“恰当”的五步清单：</p>
<ol>
<li>明确收益 (Benefit)：你到底是为了解决重复、隐藏细节，还是仅仅因为觉得它“很酷”？</li>
<li>考虑社会成本 (Social Cost)：编程是社会活动。这个抽象符合团队的习惯吗？引入它是否需要消耗大量的团队认知成本？（比如在 Go 里强推 Monad等函数式编程的范式）。</li>
<li>是否处于“上手状态” (Ready-to-hand)：它能融入开发者的直觉吗？还是会成为注意力的绊脚石？</li>
<li>是否本质 (Essential)：它是否捕捉到了问题的核心结构，能经得起未来的变化？</li>
<li>是否涌现 (Emergent)：它是你从现有代码中“识别”出来的模式，还是你强加给代码的枷锁？</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-11.png" alt="" /></p>
<h2>小结：保持怀疑，但别放弃好奇</h2>
<p>Go 社区的“避免不必要的抽象”文化，本质上是对<strong>认知负担</strong>的防御。我们见过太多为了抽象而抽象的烂代码。但 John 提醒我们，不要因此走向另一个极端——<strong>恐惧抽象</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-12.png" alt="" /></p>
<p>正确且必要的抽象是强大的武器，它能让我们驾驭巨大的复杂性。只要我们能像海德格尔审视锤子那样审视我们的代码，区分“上手”与“在手”，区分“本质”与“巧合”，我们就能在 Go 的简约哲学中，找到属于自己的那条“正确”道路。</p>
<p>资料链接：https://www.youtube.com/watch?v=oP_-eHZSaqc</p>
<hr />
<p><strong>你的“锤子”顺手吗？</strong></p>
<p>用海德格尔的视角审视代码，确实别有一番风味。<strong>在你现在的项目中，有哪些抽象是让你感觉“如臂使指”的（上手状态）？又有哪些抽象经常让你<br />
“出戏”，迫使你不得不去研究它内部的构造（在手状态）？</strong></p>
<p><strong>欢迎在评论区分享你的“哲学思考”！</strong> 让我们一起寻找那个最本质的代码真理。</p>
<p><strong>如果这篇文章带给你一次思维的“脑暴”，别忘了点个【赞】和【在看】，并转发给那些喜欢深究技术的伙伴！</strong></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>坚守内核，拥抱变量：我的 2025 年终复盘与 2026 展望</title>
		<link>https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook/</link>
		<comments>https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook/#comments</comments>
		<pubDate>Sat, 03 Jan 2026 23:17:15 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[2025]]></category>
		<category><![CDATA[2026]]></category>
		<category><![CDATA[AgenticSystem]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AIAgent]]></category>
		<category><![CDATA[AINativeDevelopment]]></category>
		<category><![CDATA[AI原生开发]]></category>
		<category><![CDATA[APIDesign]]></category>
		<category><![CDATA[API设计]]></category>
		<category><![CDATA[ArchitectureDesign]]></category>
		<category><![CDATA[BitwiseOperations]]></category>
		<category><![CDATA[ChannelCommunication]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[ConcurrencyScheduling]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[DatabaseDesign]]></category>
		<category><![CDATA[DDD]]></category>
		<category><![CDATA[DependencyManagement]]></category>
		<category><![CDATA[DesignPatterns]]></category>
		<category><![CDATA[DistributedSystems]]></category>
		<category><![CDATA[ErrorHandling]]></category>
		<category><![CDATA[FlakyTest]]></category>
		<category><![CDATA[Gin]]></category>
		<category><![CDATA[GMP]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GreenTeaGC]]></category>
		<category><![CDATA[http3]]></category>
		<category><![CDATA[IO]]></category>
		<category><![CDATA[IPC]]></category>
		<category><![CDATA[L4Workflow]]></category>
		<category><![CDATA[MCP]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[Mini-DB]]></category>
		<category><![CDATA[Mini-Kafka]]></category>
		<category><![CDATA[NetworkProgramming]]></category>
		<category><![CDATA[new(expr)]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[PerformanceOptimization]]></category>
		<category><![CDATA[porting]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[SharedMemory]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[Socket]]></category>
		<category><![CDATA[SoftwareEngineering]]></category>
		<category><![CDATA[SystemProgramming]]></category>
		<category><![CDATA[TUI]]></category>
		<category><![CDATA[位运算]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[信道通信]]></category>
		<category><![CDATA[共享内存]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[可观测性]]></category>
		<category><![CDATA[复盘]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[展望]]></category>
		<category><![CDATA[并发调度]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[性能优化]]></category>
		<category><![CDATA[数据库设计]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[架构设计]]></category>
		<category><![CDATA[测试之道]]></category>
		<category><![CDATA[知识星球]]></category>
		<category><![CDATA[系统编程]]></category>
		<category><![CDATA[网络编程]]></category>
		<category><![CDATA[自主智能系统]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[软件工程]]></category>
		<category><![CDATA[错误处理]]></category>
		<category><![CDATA[领域建模]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5660</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook 大家好，我是Tony Bai。 当时钟拨向 2026 年，我不禁回望刚刚过去的 2025。 在技术史上，这注定会被定义为“分水岭”的一年。如果说之前我们还在观望 AI 能画出什么样的图，生成怎样的代码，那么在 2025 年，我们真切地感受到了它对软件工程核心领地的冲击与重塑——从 Google 三巨头定义“AI Agent 元年”，到 CodeRabbit 报告揭示 AI 代码的质量隐忧，再到 Rob Pike 对那封AI “致谢信”的罕见愤怒。 在这样的洪流中，保持定力并不容易。回顾这一年，我庆幸自己做对了一件事：在变化的浪潮中，依然坚持系统性地输出“不变”的价值。 今天，在这个2026年元旦后开工的第一天，我想和大家聊聊我的 2025，以及我对 2026 的硬核规划。 2025：一场“微专栏”的内容实验 2025 年，我做了一个重要的决定：重塑公众号的内容形态。 在碎片化阅读盛行的当下，我深感很多技术痛点——如并发调度、网络协议、系统底层——是无法通过单篇千字文章讲透的。于是，我推出了“微专栏”模式：用 3-10 篇的体量，像写书一样去深度拆解一个技术专题。 这是一次冒险，但结果令人欣慰。这一年，我们通过 16 个微专栏，构建了一张从底层原理到 AI 前沿的完整技术拼图： 第一块拼图：攻克 Go 并发的“深水区” 并发是 Go 的灵魂，也是最容易出错的地方。 我们通过 《Go并发调度艺术》，跟随 Dmitry Vyukov 的视角亲历了 GMP 模型的演进；通过 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/stick-to-the-core-embrace-variables-2025-review-2026-outlook-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook">本文永久链接</a> &#8211; https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook</p>
<p>大家好，我是Tony Bai。</p>
<p>当时钟<a href="https://mp.weixin.qq.com/s/6U3cnqjCve9WIn07g0Y_Og">拨向 2026 年</a>，我不禁回望刚刚过去的 2025。</p>
<p>在技术史上，这注定会被定义为<strong>“分水岭”</strong>的一年。如果说之前我们还在观望 AI 能画出什么样的图，生成怎样的代码，那么在 2025 年，我们真切地感受到了它对软件工程核心领地的冲击与重塑——<a href="https://tonybai.com/2025/12/26/google-2025-research-breakthroughs/">从 Google 三巨头定义“AI Agent 元年”</a>，到<a href="https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report/"> CodeRabbit 报告揭示 AI 代码的质量隐忧</a>，再到 <a href="https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/">Rob Pike 对那封AI “致谢信”的罕见愤怒</a>。</p>
<p>在这样的洪流中，保持定力并不容易。回顾这一年，我庆幸自己做对了一件事：<strong>在变化的浪潮中，依然坚持系统性地输出“不变”的价值。</strong></p>
<p>今天，在这个2026年元旦后开工的第一天，我想和大家聊聊我的 2025，以及<strong>我对 2026 的硬核规划</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>2025：一场“微专栏”的内容实验</h2>
<p>2025 年，我做了一个重要的决定：<strong>重塑公众号的内容形态</strong>。</p>
<p>在碎片化阅读盛行的当下，我深感很多技术痛点——如并发调度、网络协议、系统底层——是无法通过单篇千字文章讲透的。于是，我推出了<strong>“微专栏”</strong>模式：<strong>用 3-10 篇的体量，像写书一样去深度拆解一个技术专题。</strong></p>
<p>这是一次冒险，但结果令人欣慰。这一年，我们通过 16 个微专栏，构建了一张从底层原理到 AI 前沿的完整技术拼图：</p>
<p><strong>第一块拼图：攻克 Go 并发的“深水区”</strong></p>
<p>并发是 Go 的灵魂，也是最容易出错的地方。</p>
<p>我们通过 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4036682086282166273#wechat_redirect">《Go并发调度艺术》</a></strong>，跟随 Dmitry Vyukov 的视角亲历了 GMP 模型的演进；通过 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4105816518230016005#wechat_redirect">《Go并发心智模型课》</a></strong>，完成了从“共享内存”到“信道通信”的思维转变；更为关键的是，<strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4017357519222882315#wechat_redirect">《征服Go并发测试》</a></strong> 让我们终于掌握了驯服 Flaky Test 的新武器。</p>
<p><strong>第二块拼图：夯实系统编程与工程底座</strong></p>
<p>在应用层之下，是冰山般的底层细节。</p>
<p>我们潜入内核，在 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4145488567680368641#wechat_redirect">《Go系统编程：揭秘进程控制、I/O与IPC》</a></strong> 中手写系统级工具；在 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4199064345390874624#wechat_redirect">《Go网络编程全解：从Socket到HTTP/3》</a></strong> 中打通了网络协议栈的任督二脉。</p>
<p>同时，我们补齐了工程化的关键短板：通过 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4163317975908614168#wechat_redirect">《Go Context解惑》</a></strong> 掌握了生命周期管理，通过 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4225702928272949254#wechat_redirect">《Go模块构建与依赖管理》</a></strong> 走出了依赖地狱，用 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4082448928904577033#wechat_redirect">《Go密码学101》</a></strong> 和 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4116476795552268292#wechat_redirect">《用Go解锁位运算之美》</a></strong> 强化了基本功，并用 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4256541133263962115#wechat_redirect">《Go 测试之道》</a></strong> 建立了交付信心。</p>
<p><strong>第三块拼图：架构设计与交互体验</strong></p>
<p>当 Coding 能力溢出，设计能力便决定了上限。</p>
<p>我们探讨了 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4286172038198558738#wechat_redirect">《API 设计之道：从设计模式到 Gin 工程化实现》</a></strong> 和 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4174451166274912264#wechat_redirect">《Go开发者的数据库设计之道》</a></strong>，拒绝面条代码。甚至，我们还玩了一把复古与现代结合的 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4108702531688333316#wechat_redirect">《重塑终端：Go TUI开发入门课》</a></strong>，让命令行工具也能拥有惊艳的交互。</p>
<p><strong>第四块拼图：Gopher 的 AI 破局</strong></p>
<p>这一年，我们不再旁观，而是下场实战。</p>
<p>从 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4286172038198558738#wechat_redirect">《AI应用开发第一课》</a></strong> 入门，到掌握 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4067128336651386882#wechat_redirect">《Gemini CLI：重新定义命令行AI开发》</a></strong>，再到硬核的 <strong><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4266729696274251779#wechat_redirect">《Google ADK 实战：用 Go 构建可靠的 AI Agent》</a></strong>，我们证明了 Go 在 AI 时代的无限可能。</p>
<p><strong>除了微专栏，2025 年也是我“系统化输出”的大年。</strong></p>
<p>在极客时间，<strong><a href="http://gk.link/a/12yGY">《Go语言进阶课》</a></strong> 正式上线，帮助无数 Gopher 完成了从熟练到精通的跨越。<br />
更让我惊喜的是，<strong><a href="http://gk.link/a/12EPd">《AI原生开发工作流实战》</a></strong> 在上架短短一个多月内就获得了 <strong>3600+</strong> 订阅。这说明大家已经意识到：<strong>AI 不仅仅是工具，更是一种全新的开发范式。</strong></p>
<p>与此同时，<strong><a href="https://tonybai.com/2025/08/28/go-primer-published">《Go语言第一课》纸质书</a></strong>也在这一年正式出版，为这一年的“内容实验”画上了一个厚重的句号。</p>
<p>这一系列的产出证明了：<strong>在浮躁的时代，深度、系统化的内容依然有着旺盛的生命力。</strong></p>
<h2>2025：在喧嚣中寻找信号</h2>
<p>翻看我 2025 年的<a href="https://tonybai.com/articles/">博客列表</a>，你会发现我的关注点始终在<strong>“底层原理”</strong>与<strong>“前沿变革”</strong>之间穿梭。</p>
<p><strong>关于 Go，我们不仅向前看，也向后看。</strong></p>
<p>Go 团队在这一年对底层的打磨可谓大刀阔斧。我们见证了 GC 的重大演进，<strong><a href="https://tonybai.com/2025/05/03/go-green-tea-garbage-collector/">《Go新垃圾回收器登场：Green Tea GC》</a></strong> 详细剖析了它如何通过内存感知降低 CPU 开销，<strong><a href="https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/">《深入 Go Green Tea GC》</a></strong> 则进一步揭示了其架构演进。在性能压榨上，<strong><a href="https://tonybai.com/2025/08/22/go-simd-package-preview/">《解锁CPU终极性能：Go原生SIMD包预览版初探》</a></strong> 让我们看到了 Go 在高性能计算领域的野心，尽管 <strong><a href="https://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check/">《连 Rob Pike 都感到“担忧”》</a></strong> 也提醒了我们随之而来的复杂性。</p>
<p>同时，我们也向后进行了“Go 考古”，探究了 <strong><a href="https://tonybai.com/2025/10/28/go-archaeology-error-handling/">《错误处理的“语法糖”之战》</a></strong>，以及 <strong><a href="https://tonybai.com/2025/10/02/go-archaeology-slice/">《Slice 的“隐秘角落”》</a></strong> 中扩容策略的演变。我们还深入探讨了 <strong><a href="https://tonybai.com/2025/12/16/go-1-26-foresight/">《Go 1.26 新特性前瞻》</a></strong> 中的语法糖 new(expr)，以及 <strong><a href="https://tonybai.com/2025/11/30/ice-assertion-failed-with-append/">《Go 编译器崩溃背后》</a></strong> 的语言规范修正。</p>
<p><strong>关于软件工程，我们保持清醒。</strong></p>
<p>当业界盲目推崇微服务时，我们通过 <strong><a href="https://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster/">《“6 个月，47 个微服务”：一场由“简历驱动”引发的架构灾难》</a></strong> 发出了警示；当所有人都在由 AI 生成代码时，我们解读了 <strong><a href="https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report/">《Bug 激增 1.7 倍！AI 写代码：是速度的蜜糖，还是质量的砒霜？》</a></strong>。我们探讨了 <strong><a href="https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work/">《无聊设计的终极奥义》</a></strong>，也重温了 <strong><a href="https://tonybai.com/2025/12/27/code-review-hell-in-ai-age/">《Code Review 已死？Kent Beck：当 AI 成为“副驾驶”，我们该如何审查代码？》</a></strong>。</p>
<p><strong>关于 AI，我们从旁观走向入局。</strong></p>
<p>这一年，我不再满足于仅仅介绍 AI 工具，而是开始探索 <strong>Go 与 AI 的结合点</strong>。从 <strong><a href="https://tonybai.com/2025/05/25/go-at-googleio-2025/">《Google I/O 2025 Go 语言进展》</a></strong> 看到的 AI 赋能，到 <strong><a href="https://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge/">《Cloudflare 2025 年度报告》</a></strong> 中 Go 在自动化 API 领域的统治力，再到 <strong><a href="https://tonybai.com/2025/09/10/introducing-the-mcp-registry/">《MCP协议注册中心发布》</a></strong> 带来的基础设施变革，我们看到了 Gopher 在 AI 时代的巨大机会。</p>
<h2>2026：Coding 廉价，眼光无价</h2>
<p>如果说 2025 年是 AI 辅助编程进入Agent模式（Copilot、Cursor、Claude Code、Gemini cli等）的普及年，那么 2026 年，将是 <strong>自主智能系统（Agentic System）</strong> 的爆发年。</p>
<p>在 AI 能以百倍速度生成代码的时代，单纯的 Coding 能力正在不可避免地贬值。但<strong>架构设计的能力、技术选型的眼光、以及构建复杂系统的智慧，将变得无价</strong>。</p>
<p>基于此，在 2026 年，我将在<strong>公众号（付费微专栏）</strong>和<strong>知识星球（免费畅读）</strong>双线并行，重点规划以下三大战役：</p>
<h3>战役一：AI 原生工程与 Agent 实战</h3>
<p>这不再是写几个 Prompt 的游戏，而是真正软件工程范式的变革。</p>
<ul>
<li><strong>自主智能系统 (Agentic System) 构建实战</strong>：我们将深入研究如何构建真正的 AI Agent。不仅仅是调用 API，而是设计能够感知环境、规划任务、使用工具、具有记忆并能自我修正的智能系统。</li>
<li><strong>以Claude Code为例的AI编码进阶实战</strong>：作为当前最强的 AI 编码 Agent，Claude Code 的潜力远未被挖掘。我们将探索如何用它<a href="https://time.geekbang.org/column/article/924970">实现L4级工作流</a>，即AI 作为自主软件工程师，能够独立地、端到端地完成从需求理解到部署上线的整个软件开发生命周期，实现端到端的自动应用构建。同时我们还要考虑AI使用的经济性(省token，省money)。</li>
<li><strong>AI 时代的软件工程探索</strong>：当代码主要由机器生成时，我们的 CI/CD、Code Review 以及测试策略该如何演进？这将是我们探索的重点。</li>
</ul>
<h3>战役二：架构设计与系统思维</h3>
<p>当“怎么写”变得容易，“写什么”和“怎么设计”就决定了你的上限。</p>
<ul>
<li><strong>分布式系统与架构设计微专栏</strong>：我们将跳出语言细节，探讨高可用架构、一致性难题、分布式事务等硬核话题。</li>
<li><strong>最佳实践与反模式</strong>：从微服务拆分到单体演进，从 数据表查询性能设计到领域建模（DDD），我们将沉淀出一套经得起时间考验的工程智慧。</li>
</ul>
<h3>战役三：Go 语言的深耕与重塑</h3>
<p>Go 依然是我们的基本盘，但在 2026 年，我们要换个玩法。</p>
<ul>
<li><strong>AI 时代的角色转换</strong>：Go 在 AI 基础设施（推理服务、向量检索、Agent 后端）中的核心地位愈发稳固。我们将关注 Go 如何更好地服务于 AI 负载。</li>
<li><strong>硬核实战：Porting（移植）系列</strong>：这是我今年最想做的一件事。我们将通过<strong>用 Go 复刻经典系统</strong>（如编写一个 <strong>Mini-Kafka</strong> 或 <strong>Mini-DB</strong>），来深入理解存储引擎、网络协议和分布式共识的底层原理。这是掌握系统编程最扎实的路。</li>
<li><strong>传统艺能</strong>：Go 的<strong>极致性能优化</strong>与<strong>可观测性</strong>依然是很多读者的刚需，也是Go生产事件中的重中之重。我们将继续关注 Go Runtime 的演进（如 Green Tea GC、SIMD），确保我们始终站在性能的最前沿。</li>
</ul>
<p>当然，作为系统编程的双子星之一，<strong>Rust</strong> 依然会在我的技术雷达范围内，作为我们拓宽技术视野的重要补充。</p>
<h2>小结</h2>
<p>2026 年的画卷已经展开。</p>
<p>这是一个技术人最焦虑的时代，也是最令人兴奋的时代。焦虑在于旧经验的快速折旧，兴奋在于个体生产力的无限放大。</p>
<p>新的一年，我希望通过这些<strong>深度微专栏</strong>和<strong>知识星球的陪伴</strong>，帮你建立起抵御技术通胀的护城河。</p>
<p>让我们左手握着 Go 与架构设计的<strong>工程底气</strong>，右手举起 AI Agent 的<strong>效率火把</strong>，从“代码工人”进化为“系统构建者”。</p>
<p>祝大家在 2026 年，代码无 Bug，架构有灵魂，人生有增量！</p>
<hr />
<p>扫码加入我的知识星球，2026 全年微专栏以及存量微专栏免费畅读！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<hr />
<p><strong>你的 2025 关键词</strong></p>
<p>我的 2025 是“坚守与拥抱”。<strong>回顾你的 2025，如果用一个词或一句话来总结，会是什么？对于即将到来的 2026，你最大的技术期待又是什么？</strong></p>
<p><strong>欢迎在评论区留下你的年度关键词，让我们一起记录这段不平凡的时光！</strong></p>
<p><strong>如果这篇文章给了你前行的力量，别忘了点个【赞】和【在看】，并转发给你的朋友，让我们在 2026 顶峰相见！</strong></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>逃离 Java 的“自行车棚”：Go 语言真的是那片“净土”吗？</title>
		<link>https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land/</link>
		<comments>https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land/#comments</comments>
		<pubDate>Thu, 18 Dec 2025 00:23:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BikeShedding]]></category>
		<category><![CDATA[Checkstyle]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[EngineeringEfficiency]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golangci-lint]]></category>
		<category><![CDATA[IdiomaticGo]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[linter]]></category>
		<category><![CDATA[NotInventedHere]]></category>
		<category><![CDATA[Optional]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[PullRequest]]></category>
		<category><![CDATA[TeamCulture]]></category>
		<category><![CDATA[代码审查]]></category>
		<category><![CDATA[团队文化]]></category>
		<category><![CDATA[工程效率]]></category>
		<category><![CDATA[帕金森定律]]></category>
		<category><![CDATA[开发体验]]></category>
		<category><![CDATA[技术迁移]]></category>
		<category><![CDATA[极简设计]]></category>
		<category><![CDATA[自行车棚效应]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[语法糖]]></category>
		<category><![CDATA[语言特性]]></category>
		<category><![CDATA[静态分析]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5553</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land 大家好，我是Tony Bai。 “如果每次我看到‘为什么不这么写？’这种针对完美代码的 PR 评论都能得到一分钱，我现在已经退休了。” 近日，一位在 r/golang 社区发帖的开发者发出了这样的咆哮。他受够了 Java 生态中那种无休止的、关于细枝末节的争论——也就是所谓的“自行车棚效应”(Bike Shedding)。他正在认真考虑转向 Go 语言。 但问题是，换一门语言，真的就能彻底摆脱人性的弱点，找到那份久违的“简单”与“高效”吗？ 这篇帖子迅速引发了数百条评论的热议，并形成了一次关于团队文化、工程效率以及如何在代码审查中保持理性的深度反思。 科普小贴士：什么是“自行车棚效应” (Bike Shedding)？ 这个概念源自帕金森定律。典故是：一个委员会在审批核电站计划时，对极其复杂的反应堆设计匆匆通过（因为太难懂，大家不敢轻易发言），却为了旁边自行车棚该漆成什么颜色而争论了一个小时（因为每个人都能懂，都想发表意见）。 在软件开发中，它特指团队在无关紧要的琐事（如代码风格、变量命名、语法糖）上浪费大量时间，而忽略了真正重要的系统设计和逻辑问题。 审视“自行车棚”—— 无效争论的代价 发帖人的痛苦，源于一种许多开发者都深有体会的经历：PR 被琐碎的个人偏好所劫持。 症状：代码逻辑正确、测试通过、符合规范。但审查者依然会问：“为什么不用 Optional.of(&#8230;).orElse(&#8230;) 而是用 if-else？” 潜台词：“如果是我的话，我会这么写。我要指出这一点，以显示我有在认真看代码，而且我很聪明。” 后果： 时间浪费：为了一个不影响功能的改动，需要重新提交代码、等待 CI 跑完（在企业级 Java 项目中可能长达 20-30 分钟）、等待再次审查。 士气低落：开发者感到自己的工作不被尊重，变成了为了满足审查者个人喜好而工作的“打字员”。 正如一位评论者尖锐指出的：“这不仅是语言问题，更是‘人’的问题。” 有些开发者倾向于过度工程化，为了使用设计模式而使用设计模式，而忽略了代码的实际价值。 Go 是解药吗？—— “强制统一”带来的自由 为什么发帖人认为 Go 是解药？因为 Go 的设计哲学确实在很大程度上抑制了“自行车棚”的滋生土壤。 1. 极简的语法与“唯一解” Go 语言的设计哲学是“少即是多”。它没有 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/escaping-java-bicycle-shed-is-go-the-pure-land-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land">本文永久链接</a> &#8211; https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land</p>
<p>大家好，我是Tony Bai。</p>
<p>“如果每次我看到‘为什么不这么写？’这种针对完美代码的 PR 评论都能得到一分钱，我现在已经退休了。”</p>
<p>近日，一位在 r/golang 社区发帖的开发者<a href="https://www.reddit.com/r/golang/comments/1pechqt/who_else_has_or_wants_to_move_from_java_to_go/">发出了这样的咆哮</a>。他受够了 Java 生态中那种无休止的、关于细枝末节的争论——也就是所谓的“自行车棚效应”(Bike Shedding)。他正在认真考虑转向 Go 语言。</p>
<p><strong>但问题是，换一门语言，真的就能彻底摆脱人性的弱点，找到那份久违的“简单”与“高效”吗？</strong></p>
<p>这篇帖子迅速引发了数百条评论的热议，并形成了一次关于<strong>团队文化</strong>、<strong>工程效率</strong>以及<strong>如何在代码审查中保持理性</strong>的深度反思。</p>
<blockquote>
<hr />
<p><strong>科普小贴士：什么是“自行车棚效应” (Bike Shedding)？</strong></p>
<p>这个概念源自帕金森定律。典故是：一个委员会在审批核电站计划时，对极其复杂的反应堆设计匆匆通过（因为太难懂，大家不敢轻易发言），却为了旁边<strong>自行车棚该漆成什么颜色</strong>而争论了一个小时（因为每个人都能懂，都想发表意见）。</p>
<p>在软件开发中，它特指<strong>团队在无关紧要的琐事（如代码风格、变量命名、语法糖）上浪费大量时间，而忽略了真正重要的系统设计和逻辑问题</strong>。</p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/api-design-pattern-and-implementation-qr.png" alt="img{512x368}" /></p>
<h2>审视“自行车棚”—— 无效争论的代价</h2>
<p>发帖人的痛苦，源于一种许多开发者都深有体会的经历：<strong>PR 被琐碎的个人偏好所劫持</strong>。</p>
<ul>
<li><strong>症状</strong>：代码逻辑正确、测试通过、符合规范。但审查者依然会问：“为什么不用 Optional.of(&#8230;).orElse(&#8230;) 而是用 if-else？”</li>
<li><strong>潜台词</strong>：“如果是我的话，我会这么写。我要指出这一点，以显示我有在认真看代码，而且我很聪明。”</li>
<li><strong>后果</strong>：
<ul>
<li><strong>时间浪费</strong>：为了一个不影响功能的改动，需要重新提交代码、等待 CI 跑完（在企业级 Java 项目中可能长达 20-30 分钟）、等待再次审查。</li>
<li><strong>士气低落</strong>：开发者感到自己的工作不被尊重，变成了为了满足审查者个人喜好而工作的“打字员”。</li>
</ul>
</li>
</ul>
<p>正如一位评论者尖锐指出的：“<strong>这不仅是语言问题，更是‘人’的问题。</strong>” 有些开发者倾向于过度工程化，为了使用设计模式而使用设计模式，而忽略了代码的实际价值。</p>
<h2>Go 是解药吗？—— “强制统一”带来的自由</h2>
<p>为什么发帖人认为 Go 是解药？因为 Go 的设计哲学确实在很大程度上抑制了“自行车棚”的滋生土壤。</p>
<p><strong>1. 极简的语法与“唯一解”</strong></p>
<p>Go 语言的设计哲学是“少即是多”。它没有 Optional，没有复杂的流式操作符，没有十种不同的方式来实现同一个功能。<br />
一位资深 Gopher 指出：“在 Go 中，很少有人会争论‘为什么不这么写’，因为通常<strong>只有一种</strong>地道的写法。” gofmt 更是从根本上消灭了关于格式化的争论。</p>
<p><strong>2. “Not Invented Here” vs. “Just Copy It”</strong></p>
<p>Java 生态倾向于构建通用的、高度抽象的框架。而 Go 社区更推崇“复制一点代码胜过引入一点依赖”。这种文化鼓励简单、直接的实现，而不是过早的抽象。<br />
评论区有人提到：“在 Go 中，如果你看到重复代码，你可能会选择容忍它；但在 Java 中，你会被要求抽象出一个通用的 Factory 模式。”</p>
<p><strong>3. 工具链的胜利</strong></p>
<p>Go 的工具链（lint, vet）非常强大且统一。如果一个问题可以通过静态分析发现，那就交给机器去阻止，而不是在 PR 中由人来指指点点。</p>
<h2>硬币的另一面 —— Go 社区的“新自行车棚”</h2>
<p>然而，逃离了 Java，就能彻底摆脱“自行车棚”吗？社区的声音并非一边倒。<strong>Go 并不是没有任何争论的乌托邦。</strong></p>
<ul>
<li><strong>新的争论点</strong>：虽然不再争论 Optional，但 Go 社区也有自己的“圣战”：
<ul>
<li>“为什么你要用这个第三方库？标准库不够好吗？”</li>
<li>“这个 struct 应该放在 pkg 目录下吗？”</li>
<li>“为什么你要定义这个接口？让消费者去定义！”</li>
</ul>
</li>
<li><strong>过度的“地道”追求</strong>：有时，对“Idiomatic Go”（地道 Go 代码）的追求也会演变成一种教条主义。一位评论者分享了自己的经历：仅仅因为不想在代码中看到哪怕一点点“Java 味”，审查者就拒绝了一个完美运行的 PR。</li>
</ul>
<p>由此看来，Go 虽然减少了语法的复杂性，从而减少了<em>语法层面</em>的争论空间，但它也无法消除<em>人类</em>对于微小差异的执着。</p>
<h2>给所有团队的“防杠指南”</h2>
<p>无论你使用 Java 还是 Go，如何建立一个健康的 Code Review 文化才是根本。社区贡献了许多极具价值的建议：</p>
<ol>
<li><strong>区分“阻塞”与“建议”</strong>：引入明确的前缀，如 nit: (吹毛求疵，不阻塞合并)、suggestion: (建议，可不采纳)、blocker: (必须修改)。这能清晰地传达审查者的意图。</li>
<li><strong>自己动手，丰衣足食</strong>：如果审查者对某个非功能性的风格问题非常在意，且有权限，不妨直接提交一个小改动，而不是阻碍原作者的 PR。</li>
<li><strong>规则自动化</strong>：凡是能用 Linter (如 Checkstyle, golangci-lint) 解决的问题，绝不在人工审查中讨论。<strong>让机器做坏人</strong>。</li>
<li><strong>接受“足够好”</strong>：Code Review 的目标是保证代码质量、发现 Bug 和分享知识，而不是追求“完美”。<strong>完美是完成的敌人</strong>。</li>
</ol>
<h2>小结：选择语言，更是选择文化</h2>
<p>回到标题的问题：<strong>Go 是开发者的净土或避风港吗？是，也不是。</strong></p>
<p>它确实通过强制的规范和极简的设计，消灭了许多“低级”的自行车棚，提供了一种更务实、更直接的编程体验。</p>
<p>但在任何有人的地方，争论都会寻找新的出口。如果你厌倦了 Java 的复杂，Go 绝对值得尝试；但请记得，<strong>真正的避风港不在语言里，而在一个成熟、理性、相互尊重的团队文化中。</strong></p>
<p>资料链接：https://www.reddit.com/r/golang/comments/1pechqt/who_else_has_or_wants_to_move_from_java_to_go/</p>
<hr />
<p><strong>吐槽时间</strong></p>
<p>“自行车棚效应”恐怕是每个程序员心中的痛。<strong>你在代码审查中遇到过最离谱、最让你抓狂的“自行车棚”争论是什么？是关于一个变量名，还是一个缩进？</strong></p>
<p><strong>欢迎在评论区吐吐槽，让我们一起把这些“无效卷”晒在阳光下！</strong></p>
<p><strong>如果这篇文章说出了你的心声，别忘了点个【赞】和【在看】，并转发给你的团队（也许能改变点什么）！</strong></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>“香蕉、猴子和整片丛林”：我们是否深陷于 OOP 的“优雅”陷阱？</title>
		<link>https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming/</link>
		<comments>https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming/#comments</comments>
		<pubDate>Fri, 28 Nov 2025 23:21:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AlexanderDanilov]]></category>
		<category><![CDATA[Banana]]></category>
		<category><![CDATA[CodeReuse]]></category>
		<category><![CDATA[composition]]></category>
		<category><![CDATA[Coupling]]></category>
		<category><![CDATA[DesignPatterns]]></category>
		<category><![CDATA[DIContainers]]></category>
		<category><![CDATA[Explicit]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Implicit]]></category>
		<category><![CDATA[Inheritance]]></category>
		<category><![CDATA[InstanceMethod]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[JoeArmstrong]]></category>
		<category><![CDATA[Jungle]]></category>
		<category><![CDATA[Magic]]></category>
		<category><![CDATA[Monkey]]></category>
		<category><![CDATA[OOP]]></category>
		<category><![CDATA[override]]></category>
		<category><![CDATA[Polymorphism]]></category>
		<category><![CDATA[receiver]]></category>
		<category><![CDATA[StructEmbedding]]></category>
		<category><![CDATA[代码复用]]></category>
		<category><![CDATA[优雅陷阱]]></category>
		<category><![CDATA[依赖倒置原则]]></category>
		<category><![CDATA[依赖注入]]></category>
		<category><![CDATA[复杂性]]></category>
		<category><![CDATA[多态]]></category>
		<category><![CDATA[实例方法]]></category>
		<category><![CDATA[强耦合]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[接收者]]></category>
		<category><![CDATA[显式]]></category>
		<category><![CDATA[简单]]></category>
		<category><![CDATA[组合]]></category>
		<category><![CDATA[结构体嵌入]]></category>
		<category><![CDATA[继承]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[隐式]]></category>
		<category><![CDATA[非侵入式]]></category>
		<category><![CDATA[面向对象编程]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5453</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming 大家好，我是Tony Bai。 Erlang 之父 Joe Armstrong 曾提出了一个关于面向对象编程（OOP）的、流传甚广的深刻比喻： “你想要一根香蕉，但你得到的却是一只拿着香蕉的猴子，以及整片丛林。” 这个比喻辛辣地讽刺了 OOP 中继承（Inheritance）等机制带来的强耦合与不必要的复杂性。近日，一篇由 Alexander Danilov 撰写的、题为《OOP：编程史上发生的最糟糕的事》的文章，则以一种更系统、更“檄文”式的方式，为我们详细解剖了这只“猴子”和这片“丛林”的构成。 Danilov 的文章，如同一份详细的“丛林勘探报告”，迫使我们重新审视，我们最初只是想要的那根香蕉（代码复用），是如何让我们不知不觉地，深陷于一片由类、继承和“魔法”构成的、盘根错节的“优雅”陷阱之中的。 想要香蕉，却来了只猴子 (继承的“原罪”) 故事始于一个最简单的愿望：代码复用。Danilov 在文章中展示了一个典型的场景：我们有一个 User 类，现在想创建一个 Npc（非玩家角色），它也需要 User 的 name 和 surname 字段。 在 OOP 的世界里，最“优雅”的做法就是继承。 // OOP - Inheritance (Danilov's example) class User { id: string name: string surname: string address: string friends: User[] // [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/oop-the-worst-thing-that-happened-to-programming-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming">本文永久链接</a> &#8211; https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming</p>
<p>大家好，我是Tony Bai。</p>
<p>Erlang 之父 Joe Armstrong 曾提出了一个关于面向对象编程（OOP）的、流传甚广的深刻比喻：</p>
<p><strong>“你想要一根香蕉，但你得到的却是一只拿着香蕉的猴子，以及整片丛林。”</strong></p>
<p>这个比喻辛辣地讽刺了 OOP 中继承（Inheritance）等机制带来的强耦合与不必要的复杂性。近日，一篇由 Alexander Danilov 撰写的、题为《<a href="https://alexanderdanilov.dev/en/articles/oop">OOP：编程史上发生的最糟糕的事</a>》的文章，则以一种更系统、更“檄文”式的方式，为我们详细解剖了这只“猴子”和这片“丛林”的构成。</p>
<p>Danilov 的文章，如同一份详细的“丛林勘探报告”，迫使我们重新审视，我们最初只是想要的那根香蕉（代码复用），是如何让我们不知不觉地，深陷于一片由类、继承和“魔法”构成的、盘根错节的“优雅”陷阱之中的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/google-adk-in-action-qr.png" alt="" /></p>
<h2>想要香蕉，却来了只猴子 (继承的“原罪”)</h2>
<p>故事始于一个最简单的愿望：<strong>代码复用</strong>。Danilov 在文章中展示了一个典型的场景：我们有一个 User 类，现在想创建一个 Npc（非玩家角色），它也需要 User 的 name 和 surname 字段。</p>
<p>在 OOP 的世界里，最“优雅”的做法就是<strong>继承</strong>。</p>
<pre><code class="typescript">// OOP - Inheritance (Danilov's example)
class User {
  id: string
  name: string
  surname: string
  address: string
  friends: User[]
  // ... a dozen other fields and methods ...
}

// “优雅"的陷阱：为了得到 name 和 surname (香蕉)，
// 我们被迫继承了 User 的全部 (猴子)
class Npc extends User {
  constructor(name: string, surname: string) {
    // 我们被迫为那些根本不需要的字段提供空值
    super(name, surname, "", [])
  }
}
</code></pre>
<p>我们成功地拿到了香蕉，但代价是，我们必须同时领养一只我们不想要的猴子——User 的所有其他字段和方法，如 address, friends 等。这只猴子不仅增加了我们代码的认知负荷，更在内存中占用了不必要的空间。</p>
<p>Danilov 指出，与之相对，函数式/组合式的思路则要直接得多：</p>
<pre><code class="typescript">// FP/Composition
type BaseUser = { id: string; name: string; surname: string }
type User = BaseUser &amp; { address: string; friendIds: string[] }
type Npc = BaseUser // Npc 只是 BaseUser 的一个别名
</code></pre>
<p>通过<strong>组合</strong>而非继承，我们可以像搭乐高积木一样，精确地选择自己需要的“零件”（香蕉），而不会被迫带上任何多余的“猴子”。</p>
<h2>猴子带来了它的朋友们 (方法的强耦合)</h2>
<p>Danilov 的批判并未止步于继承。他将矛头直指 OOP 的另一个核心——<strong>实例方法 (Instance Method)</strong>。他认为，一个实例方法，本质上就是一个被“绑架”了的函数，它的第一个参数被隐式地、硬编码地绑定到了一个特定的类实例 (this) 上。</p>
<p>这场“绑架”，直接导致了方法的<strong>可重用性极差</strong>。一个 User 类的 getDisplayName() 方法，无法被一个同样拥有 name 字段的 Dog 对象复用。方法与其所属的类（猴子）形成了不可分割的共生关系。</p>
<p>更糟糕的是，Danilov 还展示了 OOP 语言为了管理这种绑定关系而发明的、迷宫般复杂的<strong>重写 (Override)</strong> 规则（如 C# 中的 virtual, override, sealed），他讽刺道：“想出这个的人，显然觉得 OOP 中‘搬起石头砸自己脚’的方法还不够多。”</p>
<h2>为了管理猴群，我们建了座丛林 (设计模式与 DI 容器)</h2>
<p>当我们的代码库里充满了各种各样的猴子（类），它们之间有着复杂的亲缘关系（继承链）和社交网络（依赖关系）时，事情开始失控。于是，为了“优雅”地管理这群日益庞大的猴子，我们开始建造一座<strong>丛林</strong>。</p>
<p>Danilov 对这座“丛林”的构成进行了无情的剖析：</p>
<ul>
<li>
<p><strong>设计模式 (Design Patterns)</strong>：他认为，绝大多数 GoF 的设计模式，都并非普适的智慧，而只是在 OOP 的种种限制下，为了实现本应简单的功能而发明的、复杂的<strong>“变通方案”</strong> 或“拐杖”。例如，“装饰器模式”就是为了在无法使用继承时，动态地为对象添加功能。</p>
</li>
<li>
<p><strong>依赖注入容器 (DI Containers)</strong>：这是丛林里最“魔法”的部分。Danilov 回忆起他第一次面试 C# 时遇到的那段“童年阴影”代码，其中一个类的实例，通过静态构造函数和静态字段“自我创建”。他当时就感到困惑：“人类的大脑是如何以及为何会想出这种东西？” 后来他才明白，这只是通往 DI 容器“更深层魔法”的第一步。当一个 @Service 或 @Inject 注解就能让一个实例“凭空出现”时，你就失去了对程序启动和依赖关系最宝贵的洞察力——<strong>可预测性</strong>。当系统出错时，我们如同在伸手不见五指的丛林里，根本不知道那根有毒的香蕉，究竟是从哪棵树上掉下来的。</p>
</li>
</ul>
<h2>走出丛林 —— Go 语言的“反叛”与“重构”</h2>
<p>在这场关于“香蕉、猴子与丛林”的寓言中，Go 语言扮演了一个“破局者”的角色。Danilov 在文章的最后，也将 Go 列为值得推荐的现代语言之一，正是因为它在设计上，系统性地回应并解决了 OOP 的诸多“原罪”。</p>
<p>Go 的方式并非简单粗暴地全盘否定，而是一种深刻的<strong>“反叛”与“重构”</strong>：它保留了 OOP 中部分有价值的表象（如 . 点号调用），却在底层彻底重构了其实现哲学。</p>
<h3>没有继承，只有组合：直接砍掉“猴子”</h3>
<p>这是 Go 最彻底的“反叛”。Go <strong>完全废除了</strong>类型间的继承。取而代之的是更灵活的<strong>结构体嵌入 (Embedding)</strong>。你可以将一个 Nameable 结构体（香蕉）嵌入到 User 和 Npc 中，精确地实现复用，而不会被迫带上任何多余的“猴子”。这正是“组合优于继承”原则在语言层面的终极体现。</p>
<h3>没有类，但有方法：将“被绑架的函数”解放出来</h3>
<p>Go 确实有<strong>方法 (Method)</strong>。然而，Go 的方法与 OOP 的实例方法，在哲学上有着根本性的不同。</p>
<ul>
<li>在 OOP 中，方法是<strong>类定义的一部分</strong>，与数据紧密耦合。</li>
<li>在 Go 中，方法是通过 func (receiver T) MethodName() 的语法，<strong>“附加”</strong>到一个类型上的。数据 (struct) 和行为 (func) 在定义上是<strong>分离的</strong>。</li>
</ul>
<p>这种“分离”的设计，使得 Go 的方法更像是<strong>一个以 receiver 作为第一个参数的、被赋予了特殊“点号调用”语法糖的普通函数</strong>。</p>
<p>它巧妙地实现了“两全其美”：</p>
<ul>
<li><strong>保留了便利性</strong>：我们依然可以写出 user.GetDisplayName() 这样符合直觉的代码。</li>
<li><strong>获得了灵活性</strong>：由于底层仍是函数，它鼓励我们思考更通用的、基于接口而非具体类型的解决方案，从而避免了 OOP 方法的强耦合问题。</li>
</ul>
<h3>隐式的、非侵入式的接口：重新定义“多态”</h3>
<p>Go 的接口设计，是对传统 OOP 接口（如 Java 的 implements）的一次彻底革命。</p>
<ul>
<li>在传统 OOP 中，一个类必须在定义时就<strong>明确声明</strong>它要实现哪个接口。这是一种<strong>侵入式的、预先绑定的</strong>关系。</li>
<li>在 Go 中，接口的实现是<strong>隐式的、非侵入式的</strong>。任何类型，只要它拥有一个接口所要求的所有方法，它就<strong>自动地、在事后</strong>满足了这个接口。</li>
</ul>
<p>这种设计带来了巨大的灵活性，使得我们可以为任何（甚至是来自第三方库的）类型，定义我们自己的接口，而无需修改其源代码。这是对“依赖倒置原则”的终极实践。</p>
<h3>拒绝“魔法”，拥抱显式</h3>
<p>Danilov 所批判的 DI 容器和各种“魔法”，在 Go 的世界里几乎没有生存的土壤。</p>
<p>Go 的依赖管理就是简单的 import。一个包的 API，就是它导出的所有函数、类型和变量。一切都是<strong>显式的、可被静态分析的</strong>，没有注解驱动的“自动装配”，也就没有了那片需要“魔法”才能导航的丛林。</p>
<p>Danilov 引用了 Java 之父 James Gosling “后悔加入类”的传闻，以及 Linus Torvalds 禁止在 Linux 内核中使用 C++ 的决定，来佐证他的观点。而 Go 语言，似乎正是这些“巨人”反思的结晶。</p>
<p>Go 语言并非简单地回归到 C 语言那样的纯粹过程式编程。它更像是一位高明的外科医生，精准地解剖了 OOP 这具“巨人”的尸体，<strong>剔除了</strong>其中已经腐坏的组织（如继承），<strong>重构并解放了</strong>其依然有活力的器官（如方法和接口），最终创造出了一个更简单、更健壮、也更符合现代工程实践的新物种。</p>
<h2>小结：简单，才是终极的优雅</h2>
<p>Danilov 的文章，以一种辛辣而深刻的方式，揭示了 OOP 所承诺的“优雅”，在数十年的实践中，是如何常常演变成一个诱人的陷阱。它以“模拟现实世界”为名，引导我们构建起复杂的继承体系和对象网络，最终将我们自己困在了这片由“香蕉、猴子和丛林”组成的、难以维护的复杂性之中。</p>
<p>而 Go 语言的故事，则是一个关于“回归”的故事。它没有试图发明更聪明的“魔法”来隐藏复杂性，而是选择从根源上<strong>消除复杂性</strong>。</p>
<p>它提醒我们，真正的优雅，并非来自于那些能够驾驭复杂丛林的精巧工具，而是来自于<strong>从一开始，就选择不走进那片丛林</strong>的智慧。</p>
<p>资料链接：https://alexanderdanilov.dev/en/articles/oop</p>
<hr />
<p><strong>聊聊你的“OOP”爱恨情仇：</strong></p>
<ul>
<li>你是否也在项目中遇到过“香蕉、猴子和整片丛林”的困境？</li>
<li>你认为OOP在哪些场景下依然是“最优解”？</li>
<li>对于像Go/Rust等新一代编程语言的“反叛”与“重构”，你有哪些认同或不同的看法？</li>
</ul>
<p>欢迎在评论区留下你的思考与争鸣，让我们一起探寻更优雅的编程之道！</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go项目设计的“七宗罪”？警惕那些流行的“反模式”</title>
		<link>https://tonybai.com/2025/04/21/go-project-design-antipatterns/</link>
		<comments>https://tonybai.com/2025/04/21/go-project-design-antipatterns/#comments</comments>
		<pubDate>Sun, 20 Apr 2025 23:20:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[anti-pattern]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cmd]]></category>
		<category><![CDATA[common]]></category>
		<category><![CDATA[DAG]]></category>
		<category><![CDATA[DesignPattern]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoProverbs]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[helpers]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[internal]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[linter]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[model]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[pkg]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[shared]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[util]]></category>
		<category><![CDATA[utils]]></category>
		<category><![CDATA[依赖包]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[反模式]]></category>
		<category><![CDATA[循环导入]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[接口隔离]]></category>
		<category><![CDATA[最佳实践]]></category>
		<category><![CDATA[标准]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[简洁]]></category>
		<category><![CDATA[耦合]]></category>
		<category><![CDATA[表驱动测试]]></category>
		<category><![CDATA[规范]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[过度设计]]></category>
		<category><![CDATA[项目布局]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4596</guid>
		<description><![CDATA[本文永久链接 &#8211; 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 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/21/go-project-design-antipatterns">本文永久链接</a> &#8211; https://tonybai.com/2025/04/21/go-project-design-antipatterns</p>
<p>大家好，我是Tony Bai。</p>
<p>在软件开发这个行当里，“最佳实践”、“设计模式”、“标准规范”这些词汇总是自带光环。它们总是承诺会带来更好的代码质量、可维护性和扩展性。然而，当这些“圣经”般的原则被生搬硬套到Go语言的语境下时，有时非但不能带来预期的好处，反而可能把我们引入“歧途”，滋生出一些看似“专业”实则有害的“反模式”。</p>
<p>最近我也拜读了几篇国外开发者关于Go项目布局和设计哲学的文章，结合我自己这些年的实践和观察，我愈发觉得，Go社区中确实存在一些需要警惕的、流行的设计“反模式”。这些“反模式”很多人都或多或少的使用过，包括曾经的我自己。</p>
<p>在这篇文章中，我就总结一下我眼中的Go项目设计“七宗罪”，希望能帮助大家在实践中保持清醒，做出更符合Go精神的决策。</p>
<h2>第一宗罪：为了结构而结构——过度分层与分组</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-2.jpg" alt="" /></p>
<p><strong>表现：</strong> 项目伊始，不假思索地创建pkg/、internal/、cmd/、util/、model/、handler/、service/ 等层层嵌套的目录，美其名曰“组织清晰”、“符合标准”。</p>
<p><strong>危害：</strong><br />
*   <strong>违背简洁：</strong> Go 的核心哲学是简洁。不必要的目录层级增加了认知负担和导航成本。<br />
*   <strong>过早抽象/耦合：</strong> 在需求尚不明确时就划分 service、handler 等，可能导致错误的抽象边界和不必要的耦合。<br />
*   <strong>pkg/ 的迷思：</strong> pkg/ 是一个过时的、缺乏语义的约定，Go官方在<a href="https://tonybai.com/2014/11/04/some-changes-in-go-1-4">Go 1.4</a>时将Go项目中的pkg层次去掉了，<a href="https://go.dev/doc/modules/layout">Go官方的module布局指南</a>中也使用了更多有意义的名字代替了pkg。<br />
*   <strong>internal/ 的滥用：</strong> 它是 Go 工具链的一个特性，用于保护内部实现不被外部导入。但如果你的项目根本不作为库被外部依赖，或者需要保护的代码很少，强制使用 internal/ 只会徒增复杂性。<br />
*   <strong>cmd/ 的误用：</strong> 除非你的仓库包含多个独立的可执行文件，否则将单一的main.go放入cmd/毫无必要。</p>
<p><strong>解药：</strong> 保持扁平！从根目录开始，根据<strong>实际的功能或领域</strong>需要创建<strong>有意义的包</strong>。让结构随着项目的增长<strong>有机演化</strong>，而不是一开始就套用模板。</p>
<blockquote>
<p>注：笔者当年也是pkg的“忠实粉丝”，新创建一个项目，无论规模大小，总喜欢先将pkg目录预创建出来。现在是时候根据项目的演进和规模的增长来判断是否需要”pkg”这个有点像“namespace”的目录了，即当你有多个希望公开的库时，是否用pkg/作为一个顶层分组，这个是要基于项目的实际情况进行判断的。</p>
</blockquote>
<h2>第二宗罪：无效的“美化运动”——无价值的重构与移动</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-3.jpg" alt="" /></p>
<p><strong>表现：</strong> 为了让代码看起来“更干净”、“更符合某种设计模式”或“消除Linter警告”，在没有明确收益（修复 Bug、增加功能、提升性能、解决安全问题）的情况下，大规模地移动代码、修改变量名、调整文件结构。</p>
<p><strong>危害：</strong><br />
*   <strong>浪费时间精力：</strong> 投入大量时间做无意义的表面文章。<br />
*   <strong>引入风险：</strong> 任何修改都有引入新 Bug 的风险，没有价值的修改更是得不偿失。<br />
*   <strong>增加 Code Review 负担：</strong> 团队成员需要花费时间理解这些非功能性的变更。<br />
*   <strong>违背价值驱动：</strong> 软件工程的核心是交付价值，而不是追求代码的“艺术感”。</p>
<p><strong>解药：</strong> 坚持<strong>价值驱动</strong>的变更！在做任何结构或代码调整前，严格拷问自己：这个改动解决了什么<strong>真实的、当前存在</strong>的问题？它的收益是否能明确衡量并大于风险？</p>
<h2>第三宗罪：接口的“原罪”——过早、过度的抽象</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-4.jpg" alt="" /></p>
<p><strong>表现：</strong><br />
*   在只有一个具体实现的情况下，就为其定义接口。<br />
*   定义庞大、臃肿的接口，包含过多方法。<br />
*   为了“可测试性”而无脑地给所有东西加上接口。</p>
<p><strong>危害：</strong><br />
*   <strong>不必要的抽象：</strong> 接口是为了解耦和多态。在不需要这些时引入接口，只会增加代码量和理解成本。<br />
*   <strong>弱化抽象能力：</strong> “接口越大，抽象越弱”（来自<a href="https://go-proverbs.github.io/">Go谚语</a>）。大接口难以实现和维护，它变得模糊，难以理解哪些方法是真正必要的，也失去了其作为“契约”的精准性。<br />
*   <strong>阻碍演化：</strong> 过早定义接口可能锁定不成熟的设计，后续修改成本更高。<br />
*   <strong>测试的借口：</strong> Go拥有强大的测试工具（如<a href="https://tonybai.com/2024/01/01/go-testing-by-example">表驱动测试</a>），很多时候并不需要接口来实现可测试性。为测试而引入的接口可能扭曲生产代码的设计。</p>
<p><strong>解药：</strong><br />
*   <strong>拥抱具体：</strong> 先写具体实现。<br />
*   <strong>发现接口，而非设计接口：</strong> 只有当你<strong>确实需要</strong>多种实现（包括<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators">测试中的Mock</a>，但要谨慎对待），或者需要<strong>打破循环依赖</strong>时，才考虑提取接口。<br />
*   <strong>保持接口小巧、正交：</strong> 遵循接口隔离原则。</p>
<h2>第四宗罪：“大杂烩”的诱惑——utils/common/shared 黑洞</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-5.jpg" alt="" /></p>
<p><strong>表现：</strong> 创建一个名为 utils、common、shared 或 helpers 的包，把各种看似“通用”的函数、类型塞进去。</p>
<p><strong>危害：</strong><br />
*   <strong>职责不清：</strong> 这些包缺乏明确的领域或功能归属，成为代码的“垃圾抽屉”。<br />
*   <strong>依赖洼地：</strong> 随着项目增长，这些包往往会依赖越来越多的其他包，同时也被越来越多的包依赖，极易引发循环依赖或成为构建瓶颈。<br />
*   <strong>降低内聚性：</strong> 本应属于特定领域的功能被剥离出来，破坏了原有包的内聚性。</p>
<p><strong>解药：</strong><br />
*   <strong>就近原则：</strong> 如果一个“工具函数”只被一个包使用，就把它放在那个包里（可以是私有的）。<br />
*   <strong>功能归类：</strong> 如果一个“工具函数”被多个包使用，思考它真正属于哪个<strong>功能领域</strong>，为其创建一个<strong>有意义的</strong>新包（例如 applog 而不是 logutil）。<br />
*   <strong>思考依赖方向：</strong> 真正通用的基础库（如自定义的 string 处理、时间处理）应该处于依赖关系图的底层，不应依赖上层业务逻辑。</p>
<blockquote>
<p>注：坦白说，其他几项“罪过”或许还只是部分开发者的“偶发行为”，但这“第四宗罪”——随手创建 utils 或 common 包——恐怕是我们绝大多数人都曾犯过，甚至习以为常的“通病”。笔者也是如此:)。</p>
</blockquote>
<h2>第五宗罪：对 DRY 的“迷信”——为了“不重复”而引入不当依赖</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-6.jpg" alt="" /></p>
<p><strong>表现：</strong> 为了避免几行相似代码的重复，强行提取公共函数或类型，并为此引入新的包依赖，有时甚至导致复杂的依赖关系或循环依赖。</p>
<p><strong>危害：</strong><br />
*   <strong>错误的抽象：</strong> 有时看似重复的代码，在不同的上下文中可能有细微的差别或独立演化的需求。强行合并可能导致错误的抽象。<br />
*   <strong>不必要的耦合：</strong> 为了共享几行代码而引入整个包的依赖，增加了耦合度，可能比少量重复代码的维护成本更高。<br />
*   <strong>违背 Go 谚语：</strong> “A little copying is better than a little dependency.”（一点复制代码胜过一点点依赖）。Go 社区鼓励在权衡后接受适度的代码重复，以换取更低的耦合度和更高的独立性。</p>
<p><strong>解药：</strong><br />
*   <strong>批判性看待重复：</strong> 看到重复代码时，先思考它们是否真的是“同一件事”？它们的演化趋势是否一致？<br />
*   <strong>权衡成本：</strong> 引入依赖的成本（耦合、潜在冲突、维护负担）是否真的低于复制代码的成本？<br />
*   <strong>优先考虑简单：</strong> 在不确定时，保持简单，适度复制代码通常更安全。</p>
<blockquote>
<p>注：这种事儿，恐怕咱们自己或者团队里都遇到过不少：就为了用里面那一两个小函数，咔嚓一下，引入了一个庞大无比的依赖库。</p>
</blockquote>
<h2>第六宗罪：盲目崇拜与跟风——“伪标准”与“最佳实践”的陷阱</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-7.jpg" alt="" /></p>
<p><strong>表现：</strong><br />
*   不加批判地复制某个“明星项目”或所谓的“Go 标准项目布局”（如已被社区诟病的<a href="https://github.com/golang-standards/project-layout">golang-standards/project-layout</a>）。<br />
*   将其他语言（如 Java, C#）的复杂模式生搬硬套到 Go 项目中。<br />
*   将任何 Linter 规则或所谓的“最佳实践”奉为圭臬，不考虑具体场景。</p>
<p><strong>危害：</strong><br />
*   <strong>脱离实际：</strong> 别人的“最佳实践”是基于他们的特定问题和上下文演化而来的，未必适合你的项目。<br />
*   <strong>扼杀思考：</strong> 放弃了基于自己项目需求进行独立思考和决策的机会。<br />
*   <strong>违背Go文化：</strong> Go 推崇实用主义和具体问题具体分析，而非僵化的教条。</p>
<p><strong>解药：</strong><br />
*   <strong>保持独立思考：</strong> 理解每个模式或实践要解决的<strong>原始问题</strong>是什么，它是否在你的项目中真实存在？<br />
*   <strong>以我为主，兼收并蓄：</strong> 学习和借鉴，但最终决策要基于你自己的项目需求、团队情况和对 Go 语言的理解。<br />
*   <strong>质疑“最佳”：</strong> 没有万能的“最佳实践”，只有在特定上下文中的“较好实践”。</p>
<blockquote>
<p>注：确实，很多Go初学者（甚至一些老手，包括我自己）都曾长期困惑甚至“抱怨”：官方为何不给出一个项目布局的指导呢？这个呼声持续多年后，Go官方终于在2023年发布了一份<a href="https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project/">官方布局指南</a>。这份指南无疑是我们理解官方思路、开始设计Go项目布局的一个重要起点。</p>
</blockquote>
<h2>第七宗罪：与“引力”对抗——忽视 Go 的依赖约束</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-8.jpg" alt="" /></p>
<p><strong>表现：</strong><br />
*   设计出隐含循环依赖的架构（例如，某些复杂的 ORM 模式，或者 Service 层与 Repository 层相互调用具体类型）。<br />
*   当遇到 import cycle not allowed 错误时，不从根本上调整结构，而是通过滥用接口、全局变量或 init() 函数等“技巧”来绕过编译错误。</p>
<p><strong>危害：</strong><br />
*   <strong>与语言对抗：</strong> Go禁止循环依赖是其核心设计之一，旨在强制形成清晰的、可管理的依赖关系图 (DAG)。试图绕过它，本质上是在与语言的设计哲学对抗。<br />
*   <strong>隐藏的复杂性：</strong> 用“技巧”解决循环依赖，只是将问题扫到地毯下，使得真实的依赖关系变得模糊不清，增加了维护难度。<br />
*   <strong>错失优化机会：</strong> 循环依赖往往是代码职责不清、耦合过度的信号。解决循环依赖的过程，本身就是一次优化架构、厘清职责的好机会。</p>
<p><strong>解药：</strong><br />
*   <strong>拥抱 DAG：</strong> 理解并尊重 Go 的依赖规则，将其视为架构设计的“向导”。<br />
*   <strong>分析依赖：</strong> 当出现循环依赖时，深入分析其根源，理解是哪个环节的职责划分或耦合出了问题。<br />
*   <strong>结构性解决：</strong> 优先使用移动代码、提取新包（向上或向下）等结构性方法来打破循环。接口解耦是可用手段，但不应是首选或唯一手段。</p>
<h2>小结：回归常识，拥抱简洁</h2>
<p>Go语言的设计哲学是务实和简洁。许多所谓的“最佳实践”和“复杂模式”，在Go的世界里可能水土不服。识别并避免上述这些“反模式”，需要我们：</p>
<ul>
<li><strong>保持批判性思维：</strong> 不盲从，不跟风，时刻追问“为什么”。</li>
<li><strong>坚持价值驱动：</strong> 让每一个设计决策都服务于解决真实问题。</li>
<li><strong>深刻理解Go：</strong> 尊重其核心约束（如无循环依赖），发挥其优势（如简洁性）。</li>
<li><strong>拥抱演化：</strong> 从简单开始，让架构随着需求的明确而有机生长。</li>
</ul>
<p>希望这篇“七宗罪”的总结能给大家带来一些警示和启发。<strong>你是否也曾在项目中遇到过这些“反模式”？你认为还有哪些Go设计中需要警惕的“坑”？欢迎在评论区分享你的看法和经验！</strong></p>
<p>也别忘了点个【赞】和【在看】，让更多Gopher看到这篇“反模式”的总结！</p>
<hr />
<p>避开这些设计“反模式”是迈向Go高手的关键一步。如果你渴望更深层次地理解Go语言精髓，与顶尖Gopher交流切磋，并紧跟Go+AI前沿动态…</p>
<p>那么，我的 <strong>「Go &amp; AI 精进营」知识星球</strong> 正是你需要的！在这里，你可以沉浸式学习【Go原理/进阶/避坑】等独家深度专栏，随时向我提问获<br />
得解析，并与高活跃社区成员碰撞思想火花。</p>
<p><strong>扫码加入，开启你的Go深度学习与精进之旅！</strong></p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/21/go-project-design-antipatterns/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>认知负荷对编程语言选择和学习的影响</title>
		<link>https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study/</link>
		<comments>https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study/#comments</comments>
		<pubDate>Wed, 23 Oct 2024 22:12:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[思考控]]></category>
		<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[CLASS_PATH]]></category>
		<category><![CDATA[CognitiveLoad]]></category>
		<category><![CDATA[conan]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DesignPattern]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[gradle]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JAVA_HOME]]></category>
		<category><![CDATA[Maven]]></category>
		<category><![CDATA[MentalLoad]]></category>
		<category><![CDATA[PEP8]]></category>
		<category><![CDATA[pip]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[rustfmt]]></category>
		<category><![CDATA[rustup]]></category>
		<category><![CDATA[TypeScript]]></category>
		<category><![CDATA[vcpkg]]></category>
		<category><![CDATA[三脑理论]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[借用检查]]></category>
		<category><![CDATA[元编程]]></category>
		<category><![CDATA[内在认知]]></category>
		<category><![CDATA[内存管理]]></category>
		<category><![CDATA[函数式编程]]></category>
		<category><![CDATA[原始脑]]></category>
		<category><![CDATA[外在认知]]></category>
		<category><![CDATA[工具]]></category>
		<category><![CDATA[平台期]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[开发环境]]></category>
		<category><![CDATA[心智负担]]></category>
		<category><![CDATA[思维]]></category>
		<category><![CDATA[思考，快与慢]]></category>
		<category><![CDATA[情绪脑]]></category>
		<category><![CDATA[所有权]]></category>
		<category><![CDATA[文档]]></category>
		<category><![CDATA[模板]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[爬虫脑]]></category>
		<category><![CDATA[理性脑]]></category>
		<category><![CDATA[相关认知]]></category>
		<category><![CDATA[认知觉醒]]></category>
		<category><![CDATA[认知负荷]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[语法]]></category>
		<category><![CDATA[跨平台]]></category>
		<category><![CDATA[面向对象]]></category>
		<category><![CDATA[面向过程]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4353</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study 在《Go语言精进之路：从新手到高手的编程思想、方法和技巧》两卷书出版后，我收到了一些读者的反馈。其中一位读者提到：“为什么作者如此偏爱使用心智负担这个词？”当时我对此并未给予太多关注。然而，近期我阅读了一些关于认知心理学和脑科学的著作后，才意识到读者的反馈不仅仅是对该词频繁使用的关注，更可能暗示了用词不当的问题。 “心智负担”（Mental Load）指的是在处理多任务或日常生活安排时所需耗费的心理资源和精力，包括记忆、计划、组织以及应对各种任务所带来的精神压力。然而，在学习、思考和理解的情境中，特别是在编程语言的学习中，使用“认知负荷”（Cognitive Load）这一术语可能更为恰当。 认知负荷理论最初由澳大利亚新南威尔士大学的认知心理学家约翰·斯威勒(John Sweller)于1988年首先提出来的，旨在解释学习过程中的认知资源分配。认知负荷是指在学习、思考或解决问题时，大脑在处理信息和执行任务时所承受的负担。在选择编程语言时，认知负荷是一个至关重要的因素，指的是人们在学习和使用某种编程语言时，为理解语法、掌握工具和解决问题所需付出的心理负担和精力。 那么，在面对众多主流编程语言时，在不考虑市场需求与公司或组织强制学习的情况下，认知负荷究竟如何影响开发人员对编程语言的选择呢？在这篇文章中，我将进行一些不那么严谨，也非专业的粗略探讨，希望能够为大家带来一些启发。 1. 认知负荷在编程语言中的体现 认知负荷理论发展到今天，其总体被分为三种类型： 内在认知负荷(Intrinsic Cognitive Load) 内在认知负荷，也称为固有负荷，是由学习材料本身的复杂性所决定的，它与学习任务的本质和内容密切相关。例如，编程语言的语法规则、数据类型、内存管理和并发模型等都是内在负荷的一部分。学习这些概念的难易程度主要取决于编程语言本身的设计和复杂度。 外在认知负荷(Extraneous Cognitive Load) 外在负荷是由学习环境和教学方式引起的负担，通常是由于无关信息或低效的学习方法造成的。比如，配置开发环境、学习非必要的工具或被复杂的IDE界面困扰，都可能增加外在负荷。在编程语言学习中，清晰的文档和易于理解的教程可以显著减少外在负荷。 虽然外在负荷不是由编程语言语法本身决定的，但它会影响新手的学习体验。如果学习资源和工具太复杂或不直观，即使是简单的编程语言也会让人感到困难。 相关认知负荷(Germane Cognitive Load) 相关认知负荷是指学习过程中专门用于理解、整合和构建知识结构的认知努力。它与思维加工、模式识别、知识内化等过程有关。在编程中，相关认知负荷指的是学习者在掌握编程思想、设计模式和编程习惯时所付出的努力。例如，理解如何在实际项目中应用编程概念，如何优化代码设计，以及如何解决编程中的复杂问题，这些过程都会增加相关认知负荷。这种负担是积极的，因为它有助于深入理解和长期记忆。 下面这张图来自网络，可以帮助我们进一步理解三类认知负荷(只是出发点来自教学角度)： 由此可见，对于新手来说，学习一门编程语言时，外在认知负荷是第一道门槛，它决定了是否能坚持学习，还是选择“Hello and Bye”；内在认知负荷则是基础，是核心；相关认知负荷则是进阶挑战，决定了可以达到的高度。 接下来，我们将针对一些主流编程语言，沿着新手入门学习编程语言的认知负荷先后顺序进行粗略对比。希望这能为大家提供在编程语言选择方面的有用信息，同时帮助不同阶段的学习者针对各自的认知负荷水平做好心理准备。 2. 主流编程语言的认知负荷对比 在探讨主流编程语言的认知负荷时，我们需要从外在认知负荷、内在认知负荷以及相关认知负荷这三个维度进行深入分析。这种分析不仅能帮助我们理解不同语言的特点，更能为选择合适的编程语言提供参考依据。 注：笔者是后端程序员出身，对前端语言比如Javascript、Typescript等了解有限，因此这里将使用像Go、Rust、C++等主流后端语言作为分析和对比的参考对象。 2.1 外在认知负荷的影响 在编程语言学习的初始阶段，外在认知负荷往往是最先遇到的挑战。 Python在这方面表现出色，它简单的环境搭建流程让初学者能够快速开始编程之旅。只需安装一个解释器，新手就能立即开始编写代码。虽然在使用pip管理依赖时可能遇到一些包冲突的问题，但整体来说，在环境搭建、工具使用等外在认知负荷方面对初学者相当友好。 Go语言同样提供了令人称道的开发体验。它的工具链安装过程直观明了，跨平台支持也十分完善。特别值得一提的是，自从Go 1.11引入go modules以来，依赖管理变得更加自动化和直观。虽然对新手来说，理解版本控制可能需要一些时间。此外，Go团队也给出了Go项目布局的官方建议，为开发者进行代码组织提供了清晰的参考。 相比之下，C++的环境搭建则显得较为复杂。开发者需要安装编译器，配置IDE，这些步骤对新手来说都构成了不小的挑战。加上缺乏统一的包管理工具（尽管vcpkg和conan等工具正在改变这一现状），以及灵活但缺乏标准的项目结构，都让C++的外在认知负荷明显高于其他语言。 Rust通过其官方工具链安装工具rustup提供了相对简便的环境搭建方式。它的Cargo包管理器集成度高，使用便捷，而且项目结构的标准化程度高，这些特点都有效降低了外在认知负荷。 Java则介于两个极端之间。它需要安装JDK并配置环境变量(如JAVA_HOME、CLASS_PATH等)，这个过程对新手来说可能有些繁琐。虽然Maven和Gradle这样的依赖管理工具功能强大，但学习曲线较陡峭。不过，Java严格的项目布局规范在初期可能显得死板，但从长远来看反而有助于培养良好的工程习惯。 过了环境安装、工具使用和项目布局这些“外在认知负荷”的关卡后，语言自身的复杂性便会成为新手面前的更大的挑战。 2.2 内在认知负荷考量 谈到语言本身的复杂性，Python的设计理念“简单胜于复杂”使其成为认知负荷最低的选择之一。它的语法接近自然语言，几乎不需要特别的学习就能读懂基本的代码结构。这种简洁性使得Python特别适合编程初学者，以至于主流的儿童编程教学大多使用Python(当然一些启蒙教学使用的是scratch)。 Go语言同样以简洁著称，它的语法设计注重一致性和可读性。虽然保留了指针这样的底层特性，可能会让某些初学者感到困惑，但整体而言，Go的学习曲线相当平缓。值得注意的是，Go 1.18引入泛型后，虽然提升了语言的表达能力，但也增加了一定的复杂性。至于Go是否适合作为从零开始编程的新手，也是见仁见智。 C++的内在认知负荷则明显较高。它支持多种编程范式，包括面向过程、面向对象、模板编程等，这些范式和特性固然强大，但对初学者来说往往构成了较大的认知负担。特别是在处理多态、模板元编程等高级特性时，学习曲线会变得异常陡峭。 Rust的内在认知负荷同样不低，但事实证明其复杂性是有意义的。它的所有权系统和借用检查器虽然增加了学习难度，但这些机制对于理解系统编程的本质非常有帮助，同时提高了程序在运行时的安全性。新手在最初接触这些概念时可能会感到困惑，但掌握后会对内存安全有深刻的理解。 Java的内在认知负荷介于中等水平。它的面向对象语法虽然比Python或Go略显繁琐，但整体而言还算直观。Java的复杂性主要体现在面向对象设计模式、泛型和异常处理等特性上，这些概念需要时间来消化和掌握。 2.3 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/cognitive-load-impact-on-programming-language-choice-and-study-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study">本文永久链接</a> &#8211; https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study</p>
<p>在《<a href="https://item.jd.com/13694000.html">Go语言精进之路：从新手到高手的编程思想、方法和技巧</a>》两卷书出版后，我收到了一些读者的反馈。其中一位读者提到：“为什么作者如此偏爱使用<code>心智负担</code>这个词？”当时我对此并未给予太多关注。然而，近期我阅读了一些关于认知心理学和脑科学的著作后，才意识到读者的反馈不仅仅是对该词频繁使用的关注，更可能暗示了用词不当的问题。</p>
<p>“心智负担”（Mental Load）指的是在处理多任务或日常生活安排时所需耗费的心理资源和精力，包括记忆、计划、组织以及应对各种任务所带来的精神压力。然而，在学习、思考和理解的情境中，特别是在编程语言的学习中，使用“认知负荷”（Cognitive Load）这一术语可能更为恰当。</p>
<p>认知负荷理论最初由澳大利亚新南威尔士大学的认知心理学家约翰·斯威勒(John Sweller)于1988年首先提出来的，旨在解释学习过程中的认知资源分配。认知负荷是指在学习、思考或解决问题时，大脑在处理信息和执行任务时所承受的负担。在选择编程语言时，认知负荷是一个至关重要的因素，指的是人们在学习和使用某种编程语言时，为理解语法、掌握工具和解决问题所需付出的心理负担和精力。</p>
<p>那么，在面对众多主流编程语言时，在<strong>不考虑市场需求与公司或组织强制学习的情况</strong>下，认知负荷究竟如何影响开发人员对编程语言的选择呢？在这篇文章中，我将进行一些不那么严谨，也非专业的粗略探讨，希望能够为大家带来一些启发。</p>
<h2>1. 认知负荷在编程语言中的体现</h2>
<p>认知负荷理论发展到今天，其总体被分为三种类型：</p>
<ul>
<li>内在认知负荷(Intrinsic Cognitive Load)</li>
</ul>
<p>内在认知负荷，也称为固有负荷，是由学习材料本身的复杂性所决定的，它与学习任务的本质和内容密切相关。例如，编程语言的语法规则、<a href="https://tonybai.com/2022/12/18/go-type-system">数据类型</a>、<a href="https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience">内存管理</a>和<a href="https://tonybai.com/2015/06/23/concurrency-and-parallelism/">并发</a>模型等都是内在负荷的一部分。学习这些概念的难易程度主要取决于编程语言本身的设计和复杂度。</p>
<ul>
<li>外在认知负荷(Extraneous Cognitive Load)</li>
</ul>
<p>外在负荷是由学习环境和教学方式引起的负担，通常是由于无关信息或低效的学习方法造成的。比如，配置开发环境、学习非必要的工具或被复杂的IDE界面困扰，都可能增加外在负荷。在编程语言学习中，<a href="https://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite">清晰的文档</a>和易于理解的教程可以显著减少外在负荷。</p>
<p>虽然外在负荷不是由编程语言语法本身决定的，但它会影响新手的学习体验。如果学习资源和工具太复杂或不直观，即使是简单的编程语言也会让人感到困难。</p>
<ul>
<li>相关认知负荷(Germane Cognitive Load)</li>
</ul>
<p>相关认知负荷是指学习过程中专门用于理解、整合和构建知识结构的认知努力。它与思维加工、模式识别、知识内化等过程有关。在编程中，相关认知负荷指的是学习者在掌握编程思想、设计模式和编程习惯时所付出的努力。例如，理解如何在实际项目中应用编程概念，如何优化代码设计，以及如何解决编程中的复杂问题，这些过程都会增加相关认知负荷。这种负担是积极的，因为它有助于深入理解和长期记忆。</p>
<p>下面这张图来自网络，可以帮助我们进一步理解三类认知负荷(只是出发点来自教学角度)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/cognitive-load-impact-on-programming-language-choice-and-study-2.png" alt="" /></p>
<p>由此可见，对于新手来说，学习一门编程语言时，<strong>外在认知负荷是第一道门槛，它决定了是否能坚持学习，还是选择“Hello and Bye”；内在认知负荷则是基础，是核心；相关认知负荷则是进阶挑战，决定了可以达到的高度</strong>。</p>
<p>接下来，我们将针对一些主流编程语言，<strong>沿着新手入门学习编程语言的认知负荷先后顺序进行粗略对比</strong>。希望这能为大家提供在编程语言选择方面的有用信息，同时帮助不同阶段的学习者针对各自的认知负荷水平做好心理准备。</p>
<h2>2. 主流编程语言的认知负荷对比</h2>
<p>在探讨主流编程语言的认知负荷时，我们需要从外在认知负荷、内在认知负荷以及相关认知负荷这三个维度进行深入分析。这种分析不仅能帮助我们理解不同语言的特点，更能为选择合适的编程语言提供参考依据。</p>
<blockquote>
<p>注：笔者是后端程序员出身，对前端语言比如Javascript、Typescript等了解有限，因此这里将使用像Go、Rust、C++等主流后端语言作为分析和对比的参考对象。</p>
</blockquote>
<h3>2.1 外在认知负荷的影响</h3>
<p>在编程语言学习的初始阶段，<strong>外在认知负荷往往是最先遇到的挑战</strong>。</p>
<p>Python在这方面表现出色，它简单的环境搭建流程让初学者能够快速开始编程之旅。只需安装一个解释器，新手就能立即开始编写代码。虽然在使用pip管理依赖时可能遇到一些包冲突的问题，但整体来说，在环境搭建、工具使用等外在认知负荷方面对初学者相当友好。</p>
<p>Go语言同样提供了令人称道的开发体验。它的工具链安装过程直观明了，<a href="https://tonybai.com/2014/10/20/cross-compilation-with-golang/">跨平台支持</a>也十分完善。特别值得一提的是，自从<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11</a>引入<a href="https://tonybai.com/tag/gomodule">go modules</a>以来，依赖管理变得更加自动化和直观。虽然对新手来说，理解版本控制可能需要一些时间。此外，Go团队也给出了<a href="https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project">Go项目布局的官方建议</a>，为开发者进行代码组织提供了清晰的参考。</p>
<p>相比之下，C++的环境搭建则显得较为复杂。开发者需要安装编译器，配置IDE，这些步骤对新手来说都构成了不小的挑战。加上缺乏统一的包管理工具（尽管<a href="https://vcpkg.io/">vcpkg</a>和<a href="https://conan.io/">conan</a>等工具正在改变这一现状），以及灵活但缺乏标准的项目结构，都让C++的外在认知负荷明显高于其他语言。</p>
<p>Rust通过其官方工具链安装工具rustup提供了<a href="https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env/">相对简便的环境搭建方式</a>。它的Cargo包管理器集成度高，使用便捷，而且项目结构的标准化程度高，这些特点都有效降低了外在认知负荷。</p>
<p>Java则介于两个极端之间。它需要安装JDK并配置环境变量(如JAVA_HOME、CLASS_PATH等)，这个过程对新手来说可能有些繁琐。虽然Maven和Gradle这样的依赖管理工具功能强大，但学习曲线较陡峭。不过，Java严格的项目布局规范在初期可能显得死板，但从长远来看反而有助于培养良好的工程习惯。</p>
<p>过了环境安装、工具使用和项目布局这些“外在认知负荷”的关卡后，语言自身的复杂性便会成为新手面前的更大的挑战。</p>
<h3>2.2 内在认知负荷考量</h3>
<p>谈到语言本身的复杂性，Python的设计理念“简单胜于复杂”使其成为认知负荷最低的选择之一。它的语法接近自然语言，几乎不需要特别的学习就能读懂基本的代码结构。这种简洁性使得Python特别适合编程初学者，以至于主流的儿童编程教学大多使用Python(当然一些启蒙教学使用的是scratch)。</p>
<p>Go语言同样<strong>以简洁著称</strong>，它的语法设计注重一致性和可读性。虽然保留了指针这样的底层特性，可能会让某些初学者感到困惑，但整体而言，Go的学习曲线相当平缓。值得注意的是，<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18引入泛型</a>后，虽然提升了语言的表达能力，但也增加了一定的复杂性。至于<a href="https://tonybai.com/2024/08/22/go-as-first-language">Go是否适合作为从零开始编程的新手</a>，也是见仁见智。</p>
<p>C++的内在认知负荷则明显较高。它支持多种编程范式，包括面向过程、面向对象、模板编程等，这些范式和特性固然强大，但对初学者来说往往构成了较大的认知负担。特别是在处理多态、模板元编程等高级特性时，学习曲线会变得异常陡峭。</p>
<p>Rust的内在认知负荷同样不低，但事实证明其复杂性是有意义的。它的所有权系统和借用检查器虽然增加了学习难度，但这些机制对于理解系统编程的本质非常有帮助，同时提高了程序在运行时的安全性。新手在最初接触这些概念时可能会感到困惑，但掌握后会对内存安全有深刻的理解。</p>
<p>Java的内在认知负荷介于中等水平。它的面向对象语法虽然比Python或Go略显繁琐，但整体而言还算直观。Java的复杂性主要体现在面向对象设计模式、泛型和异常处理等特性上，这些概念需要时间来消化和掌握。</p>
<h3>2.3 相关认知负荷的深入分析</h3>
<p>在实际应用知识解决问题时，各种语言呈现出不同的特点。</p>
<p>Python的优势在于它能让学习者快速将知识付诸实践。其丰富的标准库和生态、简洁的语法使得从学习到应用的过程异常顺畅。无论是数据科学还是Web开发，Python都能让新手快速看到成果。它支持多种编程范式，并且社区的<a href="https://peps.python.org/pep-0008/">PEP 8规范</a>为代码风格提供了清晰的指导。</p>
<p>Go语言在知识应用方面同样表现出色。它的工具链完善，容易将所学付诸实践。特别是在服务器端开发领域，Go的并发模型和简洁的语法让新手能够相对轻松地构建高效的后端服务。虽然Go不像传统的面向对象语言那样依赖继承体系，但其接口机制和组合方式为代码设计提供了优雅的解决方案。</p>
<p>C++的相关认知负荷较高，主要体现在将理论知识转化为实践时面临的挑战。内存管理和性能优化这些概念需要大量实践才能真正掌握。它支持多种编程范式，这种灵活性虽然强大，但对初学者来说往往是一把双刃剑。由于缺乏统一的编码规范，新手可能在选择最佳实践时感到困惑。</p>
<p>Rust在这方面呈现出独特的特点。它的所有权系统要求开发者在实践中深入思考内存管理问题，这个过程虽然充满挑战，但却能培养扎实的系统编程思维。Rust社区提供的编码规范和工具链都很完善，有助于形成良好的编程习惯。</p>
<p>Java则以其企业级开发的特点著称。它要求开发者深入理解面向对象编程的核心概念，这个过程需要较长时间的积累。Java的设计模式体系完备，社区的编码规范成熟，这些特点有助于培养专业的工程思维，但对新手来说可能需要更多的时间和耐心。</p>
<h3>2.4 综合评估</h3>
<p>通过以上分析，我们可以看出不同语言在认知负荷方面的特点。</p>
<p>Python以其全方位的低认知负荷成为初学者的理想选择。</p>
<p>Go语言通过简洁的设计和完善的工具链在降低认知负荷方面做出了显著成效。</p>
<p>Java虽然相对繁琐，但其成熟的生态系统和规范的开发流程为长期发展提供了良好基础。</p>
<p>Rust和C++的学习曲线较陡，但它们在系统编程和性能优化方面的深度让投入的学习成本变得有价值。</p>
<p>在理解了编程语言的认知负荷特点后，我们不妨再从心理学的角度，特别是借助三脑理论的视角，来探讨初学者是如何在面对不同编程语言时做出选择的。</p>
<h2>3. 初学者的编程语言学习决策过程</h2>
<p><a href="https://en.wikipedia.org/wiki/Triune_brain">三脑理论(Triune Brain Theory)</a>由Paul D. MacLean于1970年提出的理论假说，该理论将人脑分为三个层次，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/cognitive-load-impact-on-programming-language-choice-and-study-3.png" alt="" /><br />
<center>来自维基百科</center></p>
<ul>
<li>爬虫脑（Reptilian Brain）：也称原始脑，负责基本生存反应，包括对威胁的快速反应和本能行为。</li>
<li>情绪脑（Limbic System）：处理情绪和动机，影响记忆形成和社交行为。</li>
<li>理性脑（Neocortex）：负责高级认知功能，如逻辑思考、语言处理和复杂决策。</li>
</ul>
<blockquote>
<p>注：三脑理论提出较早，如今有新的理论认为三脑理论毫无依据。不过这里我们假定这个理论是正确和适用的。</p>
</blockquote>
<p>三脑理论影响初学者的编程学习决策的过程是怎样的呢？这个过程往往涉及本能反应(爬虫脑主导)、情感体验（情绪脑主导）和理性思考(理性脑主导)三个层面的互动。我们继续往下看。</p>
<h3>3.1 初学阶段的决策历程</h3>
<p>在首次接触编程语言时，学习者的反应往往是多层次的。本能层面的反应最为直接，面对像C++这样认知负荷较高的语言时，很多人会本能地产生畏惧感。这种反应不是简单的怯懦，而是大脑对复杂性的自然防御机制。相反，Python这类认知负荷较低的语言则较少触发这种应激反应，使得学习者能够保持相对轻松的心态。</p>
<p>情感层面的体验则更为复杂。当成功运行第一个程序时，无论使用什么语言，都会带来成就感。但随着学习的深入，不同语言带来的情感体验会产生分化。举个例子，我在早期学习Java时，仅仅是配置环境变量这样的基础工作就带来了挫折感，这种负面情绪很容易影响学习的积极性。而Rust虽然入门门槛较低，但一旦进入到所有权系统的学习，很多人会因为频繁的编译错误而感到沮丧。</p>
<p>理性思考则是决策过程中最后但也是最重要的环节。这包括对语言应用领域的评估、职业发展前景的考虑，以及个人学习时间和精力投入的权衡。这个阶段的决策通常更加慎重，也更具有长期性。</p>
<h3>3.2 深入学习阶段的转变</h3>
<p>随着学习的深入，最初的决策依据往往会发生改变。原本令人望而生畏的特性可能转变为吸引力的来源。这种转变在Rust的学习过程中特别明显，当开发者逐渐理解了所有权系统的价值，<strong>最初的困惑可能转化为对语言设计的欣赏</strong>。</p>
<p>在这个阶段，情感体验也往往变得更加丰富。<strong>克服困难带来的成就感可能超越了简单的编程快感</strong>，这也解释了为什么一些看似“难学”的语言反而能够培养出更加忠实的用户群体。Rust连续多年在最受欢迎编程语言榜单上位居前列，很大程度上就<strong>源于这种深层的技术认同感</strong>。</p>
<p>理性思考在这个阶段会更加全面，不再局限于语言本身的特性，而是扩展到整个技术生态系统的考量。开发者会更多地思考语言的性能特点、社区活跃度、工具链完善程度等因素。</p>
<h3>3.3 认知负荷与学习效果</h3>
<p>从短期来看，低认知负荷的语言确实能够提供更平缓的学习曲线，让入门过程更加顺畅。Python和Go在这方面的优势明显，它们能让学习者快速进入实践阶段，建立信心。但这种便利性有时也会带来一个意想不到的问题：<strong>学习者可能在掌握了基础语法后陷入平台期</strong>，难以实现质的突破。这也是<strong>为什么经常有读者询问如何才能在Go语言编程中更进一步</strong>。</p>
<p>相比之下，高认知负荷的语言虽然入门较难，但往往能够培养更深入的编程思维。比如Rust的所有权系统，虽然增加了学习难度，但这种设计迫使开发者深入思考内存管理的问题，从而建立更扎实的系统编程基础。C++的模板元编程虽然复杂，但掌握后能够大大提升代码的抽象能力和复用效率。</p>
<p>不过，我们也要警惕过高的认知负荷带来的风险。如果学习过程中的挫折感持续累积，很容易导致半途而废。每年<a href="https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust">入门一次Rust</a>的真实案例也屡见不鲜。这就要求我们在选择编程语言时，既要考虑个人的学习能力和时间投入，也要权衡职业发展的需求，找到一个适合自己的平衡点。</p>
<h2>4. 小结</h2>
<p>在探讨了认知负荷对编程语言学习的影响后，我们可以得出一些粗浅的见解：编程语言的学习绝非简单的语法掌握过程，而是一个涉及多个认知维度的复杂历程。从开发环境的搭建到语言特性的理解，从基础概念的掌握到工程实践的应用，每个阶段都会给学习者带来不同程度的认知压力。理解这些认知负荷的本质，有助于我们做出更明智的编程语言学习的选择。</p>
<p>对于编程新手来说，像Python和Go这样在各个维度都尽量降低认知负荷的语言，无疑是入门的理想选择。但我们也要认识到，较高的认知负荷未必就是缺点。就像Rust和C++这样的语言，它们的学习曲线虽然陡峭，但这种”困难”往往蕴含着宝贵的学习机会。通过克服这些认知挑战，开发者能够建立起更深入的系统编程认知，形成更扎实的技术功底。</p>
<p>选择合适的编程语言，某种程度上就像选择一位长期相处的伙伴。这个选择不仅要考虑语言本身的特点，还要权衡个人的学习能力、职业规划和时间投入。<strong>认知负荷理论为我们提供了一个有价值的分析框架</strong>，但最终的选择还是要回归到个人的实际需求和发展目标。正如没有完美的编程语言一样，也没有放之四海而皆准的学习路径。找到适合自己的平衡点，或许才是最务实的学习策略。</p>
<p>最后，在<a href="https://tonybai.com/2024/10/14/programming-in-ai-era/">人工智能编码辅助技术飞速发展</a>的今天，开放的学习心态和持续学习的能力，可能比选择某个特定的编程语言更为重要。毕竟唯一不变的可能就是变化本身。</p>
<h2>5. 参考资料</h2>
<ul>
<li>《<a href="https://book.douban.com/subject/10785583/">思考，快与慢</a>》- https://book.douban.com/subject/10785583/</li>
<li>《<a href="https://book.douban.com/subject/35193035/">认知觉醒</a>》 &#8211; https://book.douban.com/subject/35193035/</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>致敬：程序员成长路上的良师与经典著作</title>
		<link>https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works/</link>
		<comments>https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works/#comments</comments>
		<pubDate>Tue, 10 Sep 2024 13:14:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agile]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[awk]]></category>
		<category><![CDATA[BjarneStroustrup]]></category>
		<category><![CDATA[BrianKernighan]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C++Primer]]></category>
		<category><![CDATA[CodeCompleted]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[C程序设计语言]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[DennisRitchie]]></category>
		<category><![CDATA[DesignPatterns]]></category>
		<category><![CDATA[DonaldKnuth]]></category>
		<category><![CDATA[EffectiveCpp]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopl]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[IP]]></category>
		<category><![CDATA[IPC]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JoshuaBloch]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[MIT]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[RobertSedgewick]]></category>
		<category><![CDATA[ScottMeyers]]></category>
		<category><![CDATA[SOLID]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[TheCProgrammingLanguage]]></category>
		<category><![CDATA[TheGoProgrammingLanguage]]></category>
		<category><![CDATA[ThinkingInCpp]]></category>
		<category><![CDATA[ThinkingInJava]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[WRichardStevens]]></category>
		<category><![CDATA[人月神话]]></category>
		<category><![CDATA[代码大全]]></category>
		<category><![CDATA[图灵奖]]></category>
		<category><![CDATA[套接字]]></category>
		<category><![CDATA[对象模型]]></category>
		<category><![CDATA[操作系统概念]]></category>
		<category><![CDATA[敏捷软件开发]]></category>
		<category><![CDATA[教师节]]></category>
		<category><![CDATA[数据库系统概念]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[算法导论]]></category>
		<category><![CDATA[编译原理]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[计算机]]></category>
		<category><![CDATA[计算机程序设计艺术]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[谭浩强]]></category>
		<category><![CDATA[龙书]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4273</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works 早上送孩子去幼儿园的路上，收到一个小伙伴的微信： 我这才意识到今天是教师节！为人师，自觉还不够格！但在这个特殊的日子，作为IT行业从业人员，我想向那些在计算机科学和编程领域给予我们启迪的“老师们”致敬。这些老师可能不是传统意义上站在讲台前的教育者，但他们通过自己的著作、思想和贡献，通过他们的智慧结晶，为我们指明了方向，为无数程序员的成长之路点亮了明灯。 这里我列举的作者与其著作也都是我个人从大学开始至今在计算机编程学习和实践之路上受到深刻影响的重要参考资料。这些书籍不仅丰富了我的知识，也激发了我对编程的热情和探索精神。每一位作者的独特视角和深入浅出的讲解，都让我在理解复杂概念时受益匪浅。希望也能引起大家的共鸣。 注：计算机领域巨匠甚多，笔者见识有限，不能一一列举，这里仅列出我亲自读过且对我影响深远的作者及其代表作品，至于像唐纳德·克努斯和他的巨著《计算机程序设计艺术》等，由于我并未拜读过，这里也就没有列出。 注：书中的图书封面图片可能并非该书最新版的封面，而是笔者购买时的版本的封面图片。 2. 编程语言 2.1 C语言/Go语言领域 2.1.1 Dennis Ritchie 大一的时候学校开设了C语言编程课，指定谭浩强老师的《C程序设计（第二版）》作为随课教材，当时我特意到大学书店花了银子买了本，并奉为皋臬。 直到我看到清华出版的影印版《C程序设计语言(第二版)》，才发现自己天真了，这本才是真正的“圣经”！ Dennis Ritchie，被誉为”C语言之父”，1983年图灵奖得主(与Ken Thompson同年获得)。他不仅创造了C语言，还与Ken Thompson一起开发了UNIX操作系统。刚刚过去的9月9日是其诞辰纪念日，MIT CSAIL在X上发文纪念了这位计算机先驱和现代编程语言奠基人： 他与Brian Kernighan合著的《The C Programming Language》被亲切地称为“K&#38;R C”，是学习C语言的必读经典，书籍不厚，它以简洁明了的语言介绍了C语言的核心概念(遵循当时的ANSI C89/C90标准)，影响了几代程序员。 2.1.2 Brian Kernighan 说完K&#38;R中的R，我们再来说K。K指的是Brian Kernighan，他也是Bell实验室UNIX开发团队的重要成员，是C语言的主要推广者之一，他也是AWK语言中的最后的那个K。和Dennis Ritchie等动不动就是语言之父不同，Kernighan以写作风格闻名。他的写作风格清晰易懂，使复杂的概念变得平易近人，并以一种易于理解和应用的方式呈现给读者。这使得与Dennis Ritchie合著的《C程序设计语言》不仅是C语言语言特性的权威指南，更是编程语言类书籍技术写作的典范，之后很多编程语言类的书籍都参考Kernighan的风格，至少也会先从一个“Hello, World”开始全书的讲解。 其与P.J.Plauger合著的《The Elements of Programming Style》也是程序员眼中的经典。 2015年，已经70高龄的Kernighan又和Go团队的Alan Donovan合著了Go语言编程书籍领域公认的圣经《The Go Programming Language》。这本书与K&#38;R C的风格很相似，作者们以清晰简洁的语言，系统且全面地介绍了Go的语法特性和编程理念，并通过大量的实例展示了Go在实际项目中的应用。书中不仅覆盖了基础知识，还深入探讨了并发编程、unsafe编程等Go高级主题。 2.2 C++ 2.2.1 Bjarne Stroustrup Bjarne Stroustrup是C++语言之父，他从1979年开始，在C语言的基础上添加了面向对象编程等特性，从而创造了C++这门强大而灵活的通用编程语言。C++经过ISO标准化后，他也是C++标准委员会的创始成员，并从那时起一直是一名活跃成员。如今，他还担任负责处理语言扩展提案的小组（进化工作组）的主席。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works">本文永久链接</a> &#8211; https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works</p>
<p>早上送孩子去幼儿园的路上，收到一个小伙伴的微信：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-2.png" alt="" /></p>
<p>我这才意识到今天是教师节！为人师，自觉还不够格！但在这个特殊的日子，作为IT行业从业人员，我想向那些在计算机科学和编程领域给予我们启迪的“老师们”致敬。这些老师可能不是传统意义上站在讲台前的教育者，但他们通过自己的著作、思想和贡献，通过他们的智慧结晶，为我们指明了方向，为无数程序员的成长之路点亮了明灯。</p>
<p>这里我列举的作者与其著作也都是我个人从大学开始至今在计算机编程学习和实践之路上受到深刻影响的重要参考资料。这些书籍不仅丰富了我的知识，也激发了我对编程的热情和探索精神。每一位作者的独特视角和深入浅出的讲解，都让我在理解复杂概念时受益匪浅。希望也能引起大家的共鸣。</p>
<blockquote>
<p>注：计算机领域巨匠甚多，笔者见识有限，不能一一列举，这里仅列出我亲自读过且对我影响深远的作者及其代表作品，至于像唐纳德·克努斯和他的巨著《<a href="https://book.douban.com/series/12331">计算机程序设计艺术</a>》等，由于我并未拜读过，这里也就没有列出。</p>
<p>注：书中的图书封面图片可能并非该书最新版的封面，而是笔者购买时的版本的封面图片。</p>
</blockquote>
<h2>2. 编程语言</h2>
<h3>2.1 C语言/Go语言领域</h3>
<h4>2.1.1 <a href="http://www.cs.bell-labs.com/~dmr">Dennis Ritchie</a></h4>
<p>大一的时候学校开设了C语言编程课，指定谭浩强老师的《<a href="https://book.douban.com/subject/1040868/">C程序设计（第二版）</a>》作为随课教材，当时我特意到大学书店花了银子买了本，并奉为皋臬。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-3.jpg" alt="" /></p>
<p>直到我看到清华出版的影印版《<a href="https://book.douban.com/subject/1230004/">C程序设计语言(第二版)</a>》，才发现自己天真了，这本才是真正的“圣经”！</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-4.png" alt="" /></p>
<p>Dennis Ritchie，被誉为”C语言之父”，1983年图灵奖得主(与Ken Thompson同年获得)。他不仅创造了C语言，还与Ken Thompson一起开发了UNIX操作系统。刚刚过去的9月9日是其诞辰纪念日，<a href="https://www.csail.mit.edu">MIT CSAIL</a>在X上发文纪念了这位计算机先驱和现代编程语言奠基人：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-5.png" alt="" /></p>
<p>他与Brian Kernighan合著的《<a href="http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html">The C Programming Language</a>》被亲切地称为“K&amp;R C”，是学习C语言的必读经典，书籍不厚，它以简洁明了的语言介绍了C语言的核心概念(遵循当时的ANSI C89/C90标准)，影响了几代程序员。</p>
<h4>2.1.2 <a href="https://www.cs.princeton.edu/~bwk/">Brian Kernighan</a></h4>
<p>说完K&amp;R中的R，我们再来说K。K指的是Brian Kernighan，他也是Bell实验室UNIX开发团队的重要成员，是C语言的主要推广者之一，他也是<a href="https://en.wikipedia.org/wiki/AWK">AWK语言</a>中的最后的那个K。和Dennis Ritchie等动不动就是语言之父不同，Kernighan以写作风格闻名。他的写作风格清晰易懂，使复杂的概念变得平易近人，并以一种易于理解和应用的方式呈现给读者。这使得与Dennis Ritchie合著的《C程序设计语言》不仅是C语言语言特性的权威指南，更是<strong>编程语言类书籍技术写作的典范</strong>，之后很多编程语言类的书籍都参考Kernighan的风格，至少也会先从一个“Hello, World”开始全书的讲解。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-7.jpeg" alt="" /></p>
<p>其与<a href="https://en.wikipedia.org/wiki/P._J._Plauger">P.J.Plauger</a>合著的《<a href="https://book.douban.com/subject/1470267/">The Elements of Programming Style</a>》也是程序员眼中的经典。</p>
<p>2015年，已经70高龄的Kernighan又和Go团队的Alan Donovan合著了Go语言编程书籍领域公认的圣经《<a href="https://www.gopl.io/">The Go Programming Language</a>》。这本书与K&amp;R C的风格很相似，作者们以清晰简洁的语言，系统且全面地介绍了Go的语法特性和编程理念，并通过大量的实例展示了Go在实际项目中的应用。书中不仅覆盖了基础知识，还深入探讨了并发编程、unsafe编程等Go高级主题。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-6.png" alt="" /></p>
<h3>2.2 C++</h3>
<h4>2.2.1 <a href="https://www.stroustrup.com/">Bjarne Stroustrup</a></h4>
<p>Bjarne Stroustrup是C++语言之父，他从1979年开始，在C语言的基础上添加了面向对象编程等特性，从而创造了C++这门强大而灵活的通用编程语言。C++经过ISO标准化后，他也是C++标准委员会的创始成员，并从那时起一直是一名活跃成员。如今，他还担任负责处理语言扩展提案的小组（进化工作组）的主席。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-8.jpeg" alt="" /></p>
<p>Bjarne Stroustrup的著作也是我入门和深入C++的必读经典，其中《C++程序设计语言》被认为是C++语言的”圣经”。Stroustrup以语言之父的口吻在书中详细介绍了C++的语言特性、抽象机制、标准库与设计理念。它不仅是一本语言参考，更是理解C++哲学的重要资源。</p>
<p>我是从高教影印版的《<a href="https://book.douban.com/subject/1231576/">The C++ Programming Language (Special Edition)</a>》开始看这本书的，与当时手里的钱能老师所著的《<a href="https://book.douban.com/subject/1041241/">C++程序设计教程</a>》相比，我感觉Stroustrup的The C++ Programming Language简直是在讲述一门新语言。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-9.jpg" alt="" /></p>
<p>Stroustrup的另外一本书《The Design and Evolution of C++》是C++进阶的必读之作，国内版译为《<a href="https://book.douban.com/subject/1096216/">C++语言的设计与演化</a>》，这本书可以理解为Stroustrup设计C++背后的心路历程以及设计决策与语言机制：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-10.jpg" alt="" /></p>
<p>Stroustrup的书虽好，但读起来有些难度，对初学者可能不那么友好，尤其是The C++ Programming Language，更像是一本C++语言的spec，缺少了像Kernighan那种春风化雨的阅读体验，所以我个人更喜欢下面这位C++大佬的作品。</p>
<blockquote>
<p>注：Stroustrup这些年持续更新其作品，甚至还推出了《<a href="https://book.douban.com/subject/25720141/">A Tour of C++</a>》这样的更易读的小册子。</p>
</blockquote>
<h4>2.2.2 <a href="https://en.wikipedia.org/wiki/Stanley_B._Lippman">Stanley B. Lippman</a></h4>
<p>Stanley B. Lippman是Stroustrup的同事，早年和Stroustrup一起在Bell实验室开发C++编译器，2001年，Lippman加入微软，成为Visual C++的架构师。他最为人所称道的是他的“一厚一薄”两本C++经典著作。</p>
<p>我们先说这本厚的，它就是C++大部头：<a href="https://book.douban.com/subject/10505113/">《C++ Primer》</a>，这本书分为C++基础、C++标准库、类设计者的工具和高级主题四个部分，非常适合C++初学者，同样其高级主题对于有经验的C++熟手也有很高的价值。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-11.jpg" alt="" /></p>
<p>Lippman的另外一本薄书名为<a href="https://book.douban.com/subject/1484262/">《Inside the C++ Object Model》</a>，最初国内中译版《<a href="https://book.douban.com/subject/1091086/">深度探索C++对象模型</a>》由宝岛知名技术作家<a href="https://zh.wikipedia.org/zh-hans/%E4%BE%AF%E4%BF%8A%E5%82%91_(%E4%BD%9C%E5%AE%B6)">侯捷</a>翻译，如今的很多新一代程序员可能已经不知道侯捷老师了，他如今依然活跃在C++高级培训的舞台上。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-12.jpg" alt="" /></p>
<p>这本书属于C++进阶书籍，Lippman从C++编译器实现者的角度对C++的对象模型、继承和多态的实现机制(比如虚函数表、动态绑定等)等做了深入浅出的讲解，是C++走向高级阶段的必读之作。</p>
<p>不幸的是，Lippman已于2022年仙逝，我们再也看不到他亲自更新C++ Primer了。</p>
<h4>2.2.3 <a href="https://en.wikipedia.org/wiki/Scott_Meyers">Scott Meyers</a></h4>
<p>如果你学过C++，但没有看过Effective C++系列，那我可以肯定你不是C++高手，Scott Meyers的《Effective C++》系列书籍是C++程序员通往高手境界的必读书籍：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-13.png" alt="" /></p>
<p>这套C++丛书的特色就是以一条条C++准则为单元，每一条都扼要说明了一个可让你写出更好的C++程序代码的方法，并以特别设计过的例子详加讨论，这非常适合程序员的胃口。</p>
<h3>2.3 Java</h3>
<p>我在工作初期<a href="https://tonybai.com/2004/10/10/java-basics/">曾经系统学过Java</a>，那时<a href="https://tonybai.com/2004/11/19/java5-research-part1/">Java刚刚发布5.0</a>，Spring也是方兴未艾。现在看来，没有Spring的Java是那么的纯粹！</p>
<p>学习纯Java，两本书足矣！下面我们就分别来看看这两本书和他们的作者。</p>
<h4>2.3.1 <a href="https://www.bruceeckel.com/">Bruce Eckel</a></h4>
<p>Bruce Eckel是著名的C++和Java作家，以其深入浅出的写作风格闻名。我没有将Eckel列到C++范畴，一是因为C++大神太多，二则是因为他的<a href="https://book.douban.com/subject/2130190/">Thinking in Java</a>似乎比他的<a href="https://book.douban.com/subject/1459728/">Thinking in C++</a>影响力更大。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-14.jpg" alt="" /></p>
<p>这本书《Java编程思想》被誉为学习Java最全面的资源之一。Eckel以其特有的方式，深入浅出地解释了Java的核心概念和高级特性。书中的例子丰富而实用，帮助读者真正理解和掌握Java编程，并这本书只讲纯Java语法，并不涉及任何框架。读过的朋友，还记得书中那句“Everything is an object”吗！</p>
<h4>2.3.2 <a href="https://en.wikipedia.org/wiki/Joshua_Bloch">Joshua Bloch</a></h4>
<p>和Bruce Eckel是一个作家和培训师不同，Joshua Bloch领导了许多Java平台功能的设计和实现，包括Java Collections Framework、java.math包和断言机制等，对Java语言和库的发展做出了重要贡献。他曾在Sun Microsystems担任杰出工程师。2004年他离开Sun，成为Google首席Java架构师。</p>
<p>和Bloch为Java实现做出的贡献相比，他的书籍在Java界更是“家喻户晓”，他曾自己或与其他人合著过多本Java书籍，包括Java Puzzlers、Java Concurrency In Practice以及Effective Java。而最后的《<a href="https://book.douban.com/subject/27047716/">Effective Java</a>》更是成为了Java程序员几乎人手一本的神作：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-15.jpg" alt="" /></p>
<p>这本书提供了编写高质量Java代码的最佳实践。Bloch基于自己丰富的经验，提出了许多实用的建议，涵盖了从基本的编程习惯到高级主题如并发和序列化，其中每条建议都值得大家细致琢磨品味。这本书帮助无数Java程序员提升了代码质量和效率。</p>
<h2>3. 算法与数据结构</h2>
<p>程序员，永远绕不开算法与数据结构。在算法与数据结构领域，Donald E. Knuth无疑是祖师爷级别的，他写的多卷本大部头的“计算机程序设计艺术”被多少人买回后顶礼膜拜，却不曾拆封拜读:)。</p>
<p>更多人和我一样，喜欢更为实用的，能看懂的书籍资料。</p>
<h3>3.1 <a href="https://en.wikipedia.org/wiki/Robert_Sedgewick_(computer_scientist)">Robert Sedgewick</a></h3>
<p>首先我们来看Sedgewick和Wayne合著的作品：《<a href="https://book.douban.com/subject/19952400/">算法（第4版）</a>》。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-16.jpg" alt="" /></p>
<p>Robert Sedgewick是Donald E. Knuth的学生，名门之后，从1985年开始一直担任普林斯顿大学计算机科学系教授，曾任该系主任。很多耳熟能详的数据结构和算法都是Sedgewick发明的，比如红黑树、三元搜索树等。他基于课程讲义编写的这本“算法”，以清晰的讲解和丰富的Java实现而闻名。该书不仅介绍了经典数据结构和算法，还着重讨论了算法在实际问题中的应用。书中包含了大量的图示和代码，使得复杂的算法概念变得易于理解。这本书适合从入门到进阶的各个阶段的读者，是算法学习的必备参考。不过你不要想一下吃透这本书，很多算法非常深奥，可以将其作为案头的参考书，常看常新。</p>
<p>Sedgewick曾出版过多本算法书籍，有C实现的，有C++实现的，大家可以根据自己需要选择不同的实现版本。</p>
<h3>3.2 <a href="https://en.wikipedia.org/wiki/Thomas_H._Cormen">Thomas H. Cormen</a>和<a href="https://en.wikipedia.org/wiki/Charles_E._Leiserson">Charles E. Leiserson</a>等</h3>
<p>提到算法，就不能不提到另外一部大部头的经典著作《<a href="https://book.douban.com/subject/1152912/">算法导论</a>》</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-17.jpg" alt="" /></p>
<p>这部作品的英文版有上千页，可谓是算法领域的“百科全书”，这本书由 达特茅斯学院计算机科学系教授Thomas H. Cormen、麻省理工学院计算机科学与电气工程系教授Charles E. Leiserson等四人共同完成。这本书既全面又严谨，因此啃起来非常有难度，我在大学时期就买了该书的高教出版社的影印版，至今过去了十余年，我也没有完成全书的阅读:(。</p>
<p>在国内数据结构领域不得不说的另外一本教材是清华大学出版社出版的、由严蔚敏和吴伟民两位老师合著的《数据结构（C语言版）》，因很多高效将其作为考研指定教材，因此这本书的市占率很高，大家可以结合前面两个外版教材一起学习，效果可能更佳。下图是当年我购买时的版本样式：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-18.png" alt="" /></p>
<h2>4. 软件工程与编程思想</h2>
<p>从大学毕业，入职工作后，软件工程知识必不可少，下面这些经典著作可以帮助大家快速融入工程领域。</p>
<h3>4.1 Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides</h3>
<p>这四位博士都是国际公认的面向对象软件领域的专家。他们在1994年合著的开创性的书籍<a href="https://book.douban.com/subject/1436745/">《设计模式：可复用面向对象软件的基础》</a>成为了开发人员在工程领域的必读之作，其影响力之广泛在整个IT领域都能排在TOP10。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-19.jpg" alt="" /></p>
<p>这本书定义并系统化了软件设计中的常见模式，为面向对象设计提供了一套通用词汇和最佳实践。书中详细描述了23种设计模式，并通过实例说明了它们的应用场景。这本书不仅影响了无数程序员的设计思想，也为软件工程领域提供了宝贵的参考。这四位博士的工作对软件设计模式的研究和应用产生了深远的影响。</p>
<h3>4.2 <a href="https://en.wikipedia.org/wiki/Steve_McConnell">Steve McConnell</a></h3>
<p>Steve McConnell是软件工程实践领域的权威专家，他的著作有不少，包括《Code Complete》、《Rapid Development 》和《Software Estimation》等，都对提高代码质量和开发效率有着重要影响。而其中影响力最大的莫过于《<a href="https://book.douban.com/subject/1477390/">代码大全（第2版）</a>》：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-20.jpg" alt="" /></p>
<p>这是一本软件构建实践的百科全书，它涵盖了从变量命名到软件架构的各个方面。McConnell以丰富的经验和洞察力，提供了大量实用的编程技巧和最佳实践。这本书不仅适合新手学习，也是有经验的程序员提升技能的重要资源。并且，书中所讲的各种技巧和实践几乎与编程语言无关，无论你擅长哪种语言，都能从中获益！</p>
<h3>4.3 <a href="https://en.wikipedia.org/wiki/Robert_C._Martin">Robert C. Martin（Uncle Bob）</a></h3>
<p>Robert C. Martin，昵称”Uncle Bob”，是敏捷开发运动的重要推动者，也是软件工艺的倡导者。他的著作颇多，包括敏捷软件开发、敏捷整洁之道、代码整洁之道、匠艺整洁之道等。最近刚刚上市的《<a href="https://book.douban.com/subject/36974785/">函数式设计</a>》也出自Bob大叔之手。</p>
<p>在他的诸多作品中，《<a href="https://book.douban.com/subject/1140457/">敏捷软件开发：原则、模式与实践</a>》对我的影响更为深刻。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-21.jpg" alt="" /></p>
<p>在这本书中，Martin详细阐述了敏捷开发的核心原则(SOLID原则)，并通过大量的案例研究和设计模式，展示了如何在实践中应用这些原则。这本书不仅介绍了技术层面的最佳实践，还深入探讨了敏捷开发对团队协作和项目管理的影响。</p>
<h3>4.4 <a href="https://en.wikipedia.org/wiki/Andy_Hunt_(author)">Andrew Hunt</a>和<a href="https://en.wikipedia.org/wiki/Dave_Thomas_(programmer)">David Thomas</a></h3>
<p>Hunt和Thomas是两位经验丰富的软件开发者，他们的著作强调了持续学习和改进在程序员职业生涯中的重要性。他们共同开创了Pragmatic Programmer的概念，并通过其大作：《<a href="https://book.douban.com/subject/1152111/">程序员修炼之道：从小工到专家</a>》为开发人员讲述具体实践的方法：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-22.jpg" alt="" /></p>
<p>这本书强调了在软件开发中保持务实态度的重要性。作者们通过一系列小贴士和练习，涵盖了从个人责任到知识投资等多个方面，帮助程序员不断提升自己的技能和职业素养。</p>
<h3>4.5 <a href="https://en.wikipedia.org/wiki/Fred_Brooks">Frederick P. Brooks Jr.</a></h3>
<p>谈到软件工程，我们不能忘记一个人，他就是Frederick P. Brooks Jr.。Brooks是一位美国计算机架构师、软件工程师和计算机科学家，以管理IBMSystem/360系列大型机和OS/360的开发而闻名。他在其开创性著作《<a href="https://book.douban.com/subject/1102259/">人月神话</a>》中坦率地写下了这些开发和项目管理经历，对后续的软件工程领域产生了深远的影响：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-23.jpg" alt="" /></p>
<p>这本软件工程的经典之作挑战了许多关于软件开发的传统观念。Brooks通过自己在IBM的经历，深入探讨了大型软件项目管理中的各种问题。尽管首次出版已经过去多年，但书中关于团队沟通、项目规划和概念完整性等方面的见解至今仍然适用，是每个软件项目管理者入门必读的著作。</p>
<h2>5. 计算机系统</h2>
<p>最后，我们看一下计算机系统领域，我将系统编程、网络编程、编译器、数据库、操作系统统统放到这个领域一起说明了，排名不分先后:)。</p>
<h3>5.1 <a href="https://en.wikipedia.org/wiki/Randal_Bryant">Randal E.Bryant</a></h3>
<p>Randal Bryant是一位美国计算机科学家和学者，因其在形式验证数字硬件和软件方面的研究而闻名。Bryant自1984年以来一直在卡内基梅隆大学任教。2004年至2014年，他担任卡内基梅隆大学计算机科学学院(SCS)院长。他长期从事本科生和研究生计算机系统方面课程教学近40年。他和David O&#8217;Hallaron教授一起在卡内基梅隆大学开设了15-213课程“计算机系统导论”，其《<a href="https://book.douban.com/subject/26912767/">深入理解计算机系统</a>》便是以这门课的讲义为基础撰写而成的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-24.jpg" alt="" /></p>
<p>这本书涵盖了计算机系统的多个层面，包括硬件、操作系统、编程语言和网络等，使读者对计算机的整体架构有深入的理解。对于计算机专业入门的学生而言，这本书是必读的教材，国内尚没有类似的教材能望其项背！当年如果早早能看到这本教材该多好啊！</p>
<h3>5.2 <a href="http://www.kohala.com/start/">W. Richard Stevens</a></h3>
<p>Richard Stevens是UNIX和网络编程领域的权威专家，也是我顶礼膜拜的大神，他的著作对系统级编程产生了深远的影响。在我工作后的若干年内，Stevens的作品是我理解Unix/Linux系统编程的必备参考，并全部购买收藏，随时翻阅。更神奇的是，他的每一部作品都是上乘之作，看下面的豆瓣评分：</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-25.png" alt="" /></p>
<p>-《UNIX环境高级编程》</p>
<p>这本书被誉为UNIX编程的”圣经”。Stevens深入浅出地解释了UNIX系统调用和库函数的使用，涵盖了文件I/O、进程控制、信号处理、线程等关键主题。这本书不仅是学习UNIX/Linux系统编程的必备参考，也为理解操作系统内部工作原理提供了宝贵的见解。</p>
<p>-《UNIX网络编程》（卷1：套接字联网API，卷2：进程间通信）</p>
<p>相对于Unix环境高级编程的全面和总括，这套书深入具体领域，重点覆盖了UNIX环境下的网络编程和进程间通信技术。第一卷重点讲解了TCP/IP协议族和套接字编程，第二卷则专注于UNIX系统上的各种IPC（进程间通信）机制。这套书不仅提供了详细的技术讲解，还包含了大量的实例代码，是网络编程学习和实践的必备参考。</p>
<p>-《TCP/IP详解》系列</p>
<p>这套书深入浅出地解释了TCP/IP协议族的工作原理，从协议的基本概念到复杂的实现细节，为读者呈现了一幅完整的TCP/IP知识图谱。这套书不仅适合网络程序员阅读，也是理解现代互联网技术基础的重要资源。</p>
<p>对于Stevens的这些书，虽然年代已久，但对如今的后端/系统程序员依然有极大的参考价值，建议大家必读。</p>
<h3>5.3 <a href="https://en.wikipedia.org/wiki/Alfred_Aho">Alfred V. Aho</a>, Monica S. Lam, Ravi Sethi和Jeffrey D. Ullman</h3>
<p>以Alfred V. Aho为代表的这几位作者都是编译器理论和实现的权威专家，他们的著作被誉为编译原理领域的”圣经”。Alfred V. Aho同时也是AWK语言中的那个”A”，他还著有《<a href="https://book.douban.com/subject/2208525/">计算机算法的设计与分析</a>》。当然“龙书”是其在学术领域著作的最卓越代表，学编译原理的同学建议人手一本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-26.jpg" alt="" /></p>
<p>这本书以其全面性和深度在编译器领域独树一帜。从词法分析、语法分析到代码优化，书中详细讲解了编译器设计的各个环节。虽然以理论为主，但书中也包含了大量的实例和练习，帮助读者将理论付诸实践。这本书不仅是编译器开发者的必读之作，对理解程序语言的设计和实现也有重要帮助。国内各大开设编译原理课程的重点高校也都将其作为第一教材。国内一些高校也编写了一些自己的教材，但与这本“龙书”相比，level还是差距很大。</p>
<h3>5.4 <a href="https://en.wikipedia.org/wiki/Abraham_Silberschatz">Abraham Silberschatz</a></h3>
<p>Avi Silberschatz是一位以色列计算机科学家和研究员，曾在bell实验室工作过，他因在计算机科学领域撰写了许多有影响力的著作而闻名，尤其是操作系统和数据库系统方面。其作品<a href="https://book.douban.com/subject/35501216/">《数据库系统概念》</a>和<a href="https://book.douban.com/subject/30297919/">《操作系统概念》</a>被全世界的高校计算机专业所采用。</p>
<p>-《数据库系统概念》</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-27.jpg" alt="" /></p>
<p>本书由Abraham Silberschatz、 Henry F. Korth和S. Sudarshan合著，这三位作者都是数据库系统领域的专家，他们的著作被广泛用作大学教材和专业参考。这本书全面介绍了数据库系统的基本概念、设计原理和实现技术。从关系代数到事务处理，从查询优化到分布式数据库，书中涵盖了传统和现代数据库技术的各个方面。无论你是在校数据库专业的学生，还是从事数据库核心系统开发的工程师，亦或是数据库应用开发的程序员，本书都极具参考价值，可放置在案头随时查看。</p>
<p>-《操作系统概念》</p>
<p><img src="https://tonybai.com/wp-content/uploads/programmer-mentors-and-their-classic-works-28.jpg" alt="" /></p>
<p>本书由Abraham Silberschatz, Peter B. Galvin 和Greg Gagne合著，这几位作者都是操作系统理论和实践的专家，他们的著作在学术界和工业界都有广泛影响。</p>
<p>这本书以其全面性和深度成为了操作系统学习的重要参考。从进程管理到分布式系统，从内存管理到安全性，书中详细讨论了操作系统的各个方面。作者们不仅介绍了理论知识，还通过案例研究展示了这些概念在实际系统中的应用。这本书适合从入门到进阶的各个阶段的读者，是理解现代计算机操作系统工作原理的关键参考材料。</p>
<h2>6. 小结</h2>
<p>在教师节这个神圣的日子中，我们回顾了这些在计算机科学和编程领域做出杰出贡献的”老师们”。他们的智慧和洞见，通过这些经典著作，影响了几代程序员的成长，更是对我的程序员生涯提供了莫大的帮助。</p>
<p>这些大师们不仅仅传授了技术知识，更重要的是，他们塑造了我们思考问题和解决问题的方式。从C语言到Go，从算法到软件工程，从操作系统、编译原理到网络编程等，这些著作涵盖了计算机科学的方方面面，构建了现代软件开发的知识体系。</p>
<p>作为程序员，我们应该心怀感激，因为我们站在了这些巨人的肩膀上。同时，我们也要记住，学习是一个终身的过程。技术在不断进步，新的挑战不断出现，但这些经典著作中蕴含的智慧将永远指引我们前进的方向。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>聊聊Go与依赖注入</title>
		<link>https://tonybai.com/2023/09/28/dependency-injection-with-go/</link>
		<comments>https://tonybai.com/2023/09/28/dependency-injection-with-go/#comments</comments>
		<pubDate>Wed, 27 Sep 2023 21:54:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[buildtag]]></category>
		<category><![CDATA[DesignPattern]]></category>
		<category><![CDATA[dig]]></category>
		<category><![CDATA[DIP]]></category>
		<category><![CDATA[FaceBook]]></category>
		<category><![CDATA[fx]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[IoC]]></category>
		<category><![CDATA[meta]]></category>
		<category><![CDATA[RobertMartin]]></category>
		<category><![CDATA[ServiceLocator]]></category>
		<category><![CDATA[SOLID]]></category>
		<category><![CDATA[uber]]></category>
		<category><![CDATA[wire]]></category>
		<category><![CDATA[代码生成]]></category>
		<category><![CDATA[依赖倒置]]></category>
		<category><![CDATA[依赖注入]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[控制反转]]></category>
		<category><![CDATA[敏捷软件开发]]></category>
		<category><![CDATA[松耦合]]></category>
		<category><![CDATA[编译时]]></category>
		<category><![CDATA[设计原则]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[运行时]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4000</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/09/28/dependency-injection-with-go 如果你读过Robert C. Martin的《敏捷软件开发：原则、模式与实践》(书的封皮见下图)，那么你一定知道经典的SOLID设计原则中的“D”：依赖倒置原则（Dependency Inversion Principle, DIP）。 依赖倒置原则是面向对象设计中的基本原则之一，它阐述了高层模块和低层模块的依赖关系应该倒置(如下图)，也就是: 高层模块不应该依赖低层模块，二者都应该依赖其抽象 抽象不应该依赖细节，细节应该依赖抽象 依赖倒置原则实际上就是对控制反转(Inversion of Control，IoC)这一概念的阐述，而依赖注入(Dependency Injection)是实现控制反转的一种机制。所以可以说，依赖倒置原则是设计级的指导思想，它提出了正确的依赖关系；而依赖注入是实现级的具体设计模式，它将组件的依赖关系控制权移到了外部，实现了组件之间的解耦，是对依赖倒置原则的一种实现手段。 依赖注入可以帮助你开发出松耦合的代码，松耦合使代码更易于维护。 在《Go语言包设计指南》一文中，我们提到过：在Go中，耦合发生在包这一层次。而在Go代码层面最低的耦合是接口耦合。在Go中，接口的实现是隐式的，即a包实现b包中定义的接口时是不需要显式导入b包的，我们可以在c包中完成对a包与b包的组装，这样c包依赖a包和b包，但a包与b包之间没有任何耦合。那么负责组装a包与b包的c包能否在代码层面消除掉对a和b的依赖呢？这个就很难了。不过我们可以使用依赖注入技术来消除在代码层面手动基于依赖进行初始化或创建时的复杂性，在中大型的程序中，依赖注入的优点更能得到体现。 在这篇文章中，我们就来聊聊Go中依赖注入可以解决的问题，并初步认识一下两个在Go社区认可度较高的Go依赖注入框架。 1. 手动注入 我们先建立一个符合DIP原则的例子，其依赖关系如下图： 这里有三个“模块”，从高到低分别为Service、BussinessLogic和DatabaseAccess。Service是一个接口，其实现ServiceImpl依赖BussinessLogic接口。Business是BussinessLogic的实现，它还依赖DatabaseAccess接口。Database则是DatabaseAccess接口的实现。 围绕这一示例，我们分别用手动组装和依赖注入框架演示一下如何实现注入，先来看一下手动组装与注入。 下面是示例的项目结构布局： ./manual └── demo/ ├── Makefile ├── business/ │   └── business.go ├── database/ │   └── database.go ├── go.mod ├── main.go └── service/ └── service.go manual/demo目录下的service、business和database包下面包含了导出的接口与其具体实现的定义。这里将这些包的代码列出来，这些代码在后续应用依赖注入工具的示例中也是保持不变的： // dependency-injection-examples/manual/demo/service/service.go package service import "demo/business" [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/dependency-injection-with-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/09/28/dependency-injection-with-go">本文永久链接</a> &#8211; https://tonybai.com/2023/09/28/dependency-injection-with-go</p>
<p>如果你读过<a href="http://cleancoder.com/">Robert C. Martin</a>的<a href="https://book.douban.com/subject/1140457/">《敏捷软件开发：原则、模式与实践》</a>(书的封皮见下图)，那么你一定知道经典的<a href="http://cleancoder.com/files/solid.md">SOLID设计原则</a>中的“D”：依赖倒置原则（Dependency Inversion Principle, DIP）。</p>
<p><img src="https://tonybai.com/wp-content/uploads/dependency-injection-with-go-2.jpg" alt="" /></p>
<p>依赖倒置原则是面向对象设计中的基本原则之一，它阐述了高层模块和低层模块的依赖关系应该倒置(如下图)，也就是:</p>
<ul>
<li>高层模块不应该依赖低层模块，二者都应该依赖其抽象</li>
<li>抽象不应该依赖细节，细节应该依赖抽象</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/dependency-injection-with-go-3.png" alt="" /></p>
<p>依赖倒置原则实际上就是对控制反转(Inversion of Control，IoC)这一概念的阐述，而<a href="http://en.wikipedia.org/wiki/Dependency_injection">依赖注入(Dependency Injection)</a>是实现控制反转的一种机制。所以可以说，依赖倒置原则是设计级的指导思想，它提出了正确的依赖关系；而依赖注入是实现级的具体设计模式，它将组件的依赖关系控制权移到了外部，实现了组件之间的解耦，是对依赖倒置原则的一种实现手段。</p>
<p>依赖注入可以帮助你开发出松耦合的代码，<strong>松耦合使代码更易于维护</strong>。</p>
<p>在<a href="https://tonybai.com/2023/06/18/go-package-design-guide">《Go语言包设计指南》</a>一文中，我们提到过：在Go中，耦合发生在包这一层次。而在Go代码层面最低的耦合是<strong>接口耦合</strong>。在Go中，接口的实现是隐式的，即a包实现b包中定义的接口时是不需要显式导入b包的，我们可以在c包中完成对a包与b包的组装，这样c包依赖a包和b包，但a包与b包之间没有任何耦合。那么负责组装a包与b包的c包能否在代码层面消除掉对a和b的依赖呢？这个就很难了。不过我们可以使用<strong>依赖注入</strong>技术来消除在代码层面手动基于依赖进行初始化或创建时的复杂性，在中大型的程序中，依赖注入的优点更能得到体现。</p>
<p>在这篇文章中，我们就来聊聊Go中依赖注入可以解决的问题，并初步认识一下两个在Go社区认可度较高的Go依赖注入框架。</p>
<h2>1. 手动注入</h2>
<p>我们先建立一个符合DIP原则的例子，其依赖关系如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/dependency-injection-with-go-4.png" alt="" /></p>
<p>这里有三个“模块”，从高到低分别为Service、BussinessLogic和DatabaseAccess。Service是一个接口，其实现ServiceImpl依赖BussinessLogic接口。Business是BussinessLogic的实现，它还依赖DatabaseAccess接口。Database则是DatabaseAccess接口的实现。</p>
<p>围绕这一示例，我们分别用手动组装和依赖注入框架演示一下如何实现注入，先来看一下手动组装与注入。</p>
<p>下面是示例的项目结构布局：</p>
<pre><code>./manual
└── demo/
    ├── Makefile
    ├── business/
    │   └── business.go
    ├── database/
    │   └── database.go
    ├── go.mod
    ├── main.go
    └── service/
        └── service.go
</code></pre>
<p>manual/demo目录下的service、business和database包下面包含了导出的接口与其具体实现的定义。这里将这些包的代码列出来，这些代码在后续应用依赖注入工具的示例中也是保持不变的：</p>
<pre><code>// dependency-injection-examples/manual/demo/service/service.go

package service

import "demo/business"

// Service interface
type Service interface {
    HandleRequest() string
}

// ServiceImpl struct
type ServiceImpl struct {
    logic business.BusinessLogic
}

// Constructor
func NewService(logic business.BusinessLogic) *ServiceImpl {
    return &amp;ServiceImpl{logic: logic}
}

// Implement HandleRequest()
func (s ServiceImpl) HandleRequest() string {
    return "Handled request: " + s.logic.ProcessData()
}

// dependency-injection-examples/manual/demo/business/business.go

package business

import (
    "demo/database"
)

// BusinessLogic interface
type BusinessLogic interface {
    ProcessData() string
}

// Business struct
type Business struct {
    db database.DatabaseAccess
}

// Constructor
func NewBusiness(db database.DatabaseAccess) *Business {
    return &amp;Business{db: db}
}

// Implement ProcessData()
func (b Business) ProcessData() string {
    return "Business logic processed " + b.db.GetData()
}

// dependency-injection-examples/manual/demo/database/database.go

package database

// DatabaseAccess interface
type DatabaseAccess interface {
    GetData() string
}

// Database struct
type Database struct{}

func NewDatabase() *Database {
    return &amp;Database{}
}

// Implement GetData()
func (db Database) GetData() string {
    return "Data from database"
}
</code></pre>
<p>service.Service是直面client的接口。于是在main函数中，我们实例化一个Service的实现并传给Client，后者调用Service的HandleRequest方法触发全流程。service.NewService的调用依赖一个实现了business.BusinessLogic接口的实例，我们在调用NewService之前还需要调用business.NewBusiness创建一个实现了business.BusinessLogic接口的实例；business.NewBusiness的调用依赖一个实现了database.DatabaseAccess接口的实例，我们在调用NewBusiness之前需要调用database.NewDatabase创建一个实现了database.DatabaseAccess接口的实例。</p>
<p>这就是手工组装的现实：<strong>我们要记住“模块”间的依赖关系，并手动创建对应实例以满足这种依赖</strong>。下面是main函数的代码：</p>
<pre><code>// dependency-injection-examples/manual/demo/main.go

package main

import (
    "demo/business"
    "demo/database"
    "demo/service"
    "fmt"
)

// Client struct
type Client struct {
    service service.Service
}

// Constructor
func NewClient(service service.Service) *Client {
    return &amp;Client{service: service}
}

// Call service
func (c Client) MakeRequest() string {
    return "Client request: " + c.service.HandleRequest()
}

func main() {
    // make dependency injection manually
    db := database.NewDatabase()
    busi := business.NewBusiness(db)
    svc := service.NewService(busi)
    client := NewClient(svc)

    fmt.Println(client.MakeRequest())
}
</code></pre>
<p>编译运行上述示例的结果如下：</p>
<pre><code>$cd dependency-injection-examples/manual/demo
$make
$./demo
Client request: Handled request: Business logic processed Data from database
</code></pre>
<p>这种为了满足依赖而进行的手工实例创建的行为，在一些小型或演示型程序中还可以自诩为straightforward，但在拥有上百个包的大型程序中，这种为了组装而进行的创建行为就会因多点发生、依赖众多而显现出“复杂性”和难于维护。为了保持代码的松耦合还要降低组装创建行为的复杂度，依赖注入工具被引入，并且<strong>往往代码库越庞大，引入DI的好处就越发明显</strong>。松耦合带来的好处并不总是立竿见影，但随着时间的推移，随着代码库复杂性的增加，这些好处就会变得显而易见。</p>
<blockquote>
<p>注：大家不要进入这样的误区：“采用依赖注入工具的代码就一定是符合DIP原则的松耦合的代码”。至少在Go中，不符合DIP原则的代码(比如没有建立接口抽象)也可以使用依赖注入工具来进行依赖的创建和模块间的组装。</p>
</blockquote>
<p>Go社区（尤其是一些大厂）提供了一些Go依赖注入工具，比如：<a href="https://github.com/google/wire">Google wire</a>、<a href="https://github.com/uber-go/fx">uber Fx</a>、<a href="https://github.com/facebookarchive/inject">facebook inject</a>等。这些工具大致可分为两类，一类是<strong>利用代码生成技术的编译期依赖注入</strong>，另一类则是<strong>利用反射技术的运行时依赖注入</strong>。</p>
<p>下面我们分别以编译器依赖注入的Google wire和运行时依赖注入的uber fx为例来看看如何通过依赖注入工具来完成依赖模块的组装(assembly)。</p>
<blockquote>
<p>注：facebook的inject已经public archived；google wire目前的开发也不是很active，wire团队给出的理由是要保持wire足够简单并认为从v0.3.0开始，wire已经是功能特性完备的了，目前不接受新feature，仅接受bug报告和修复的补丁pr。只有uber的fx还处于非常积极的开发状态，uber宣称fx是经过uber生产验证的：uber几乎所有的Go服务都是建立在Fx基础之上的。</p>
</blockquote>
<h2>2. google/wire：编译期的依赖注入</h2>
<p><a href="https://go.dev/blog/wire">wire</a>是由<a href="https://gocloud.dev/">Google Go Cloud开发包团队</a>于2018年下旬开源的Go编译期依赖注入工具，与uber fx、facebook的inject等使用反射在运行时注入不同的是，wire灵感来自Java的<a href="https://google.github.io/dagger/">Dagger 2</a>，使用的是代码生成技术，而不是反射或服务定位器(service locator)技术。</p>
<p>相较于运行时依赖注入，编译期间注入的最大好处就是<strong>生成的依赖注入和组装的代码是对你可见的</strong>，没有任何背后的“魔法”。这便于在编译期捕捉到注入过程的错误，也便于代码的调试。</p>
<p>此外，wire团队认为编译期注入可以避免依赖膨胀。Wire生成的代码只会导入所需的依赖项，因此，你的二进制文件不会有未使用的导入。运行时依赖项注入在运行时之前无法识别未使用的依赖项。</p>
<p>下面我们就用wire注入来改造一下上面的示例。</p>
<blockquote>
<p>注：安装wire命令为go install github.com/google/wire/cmd/wire@latest 。</p>
</blockquote>
<p>相对于manual那个示例，我们在main包下面增加一个新文件wire.go：</p>
<pre><code>// dependency-injection-examples/wire/demo/wire.go

//go:build wireinject
// +build wireinject

package main

// wire.go

import (
    "demo/business"
    "demo/database"
    "demo/service"

    "github.com/google/wire"
)

func InitializeService() service.Service {
    wire.Build(service.NewService,
        wire.Bind(new(service.Service), new(*service.ServiceImpl)),
        business.NewBusiness,
        wire.Bind(new(business.BusinessLogic), new(*business.Business)),
        database.NewDatabase,
        wire.Bind(new(database.DatabaseAccess), new(*database.Database)),
    )
    return nil
}
</code></pre>
<p>我们看到wire.go中提供了一个InitializeService函数，用于为main函数中的Client实例提供一个service.Service接口的具体实现。但是在这个函数中我们并没有像manual中那样手工调用NewService等来创建实例，我们仅仅是将各个“模块”Service、BussinessLogic以及DatabaseAccess的实例的创建函数传给了wire.Build函数。另外我们看到wire.go这个源文件使用了build tag，这个文件仅仅是用于代码生成，并不会参与到最终的代码编译过程中，这也是InitializeService函数的返回值随意设置为nil的原因，这个nil在代码生成过程中会被忽略并替换掉。</p>
<blockquote>
<p>注：为什么要使用wire.Bind？我们示例中的各个模块的NewXXX函数接受的参数都为接口类型，返回的都是具体的类型实例，这符合Go的惯例。但如果不使用wire.Bind，wire将无法知道NewXXX依赖的接口类型参数该如何创建！通过wire.Bind告诉wire某个接口类型参数，比如service.Service，可由创建如&#42;service.ServiceImpl的类型替代。关于<a href="https://github.com/google/wire/blob/main/docs/guide.md#binding-interfaces">Binding Interfaces的具体介绍</a>，可以参考wire官方文档。</p>
</blockquote>
<p>接下来，我们就可以通过wire命令生成代码，完成注入过程：</p>
<pre><code>$cd dependency-injection-examples/wire/demo
$wire
wire: demo: wrote /Users/tonybai/Go/src/github.com/bigwhite/experiments/dependency-injection-examples/wire/demo/wire_gen.go
</code></pre>
<p>wire工具基于wire.go生成了wire_gen.go文件，在该示例中，wire_gen.go的内容如下：</p>
<pre><code>// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
    "demo/business"
    "demo/database"
    "demo/service"
)

// Injectors from wire.go:

func InitializeService() service.Service {
    databaseDatabase := database.NewDatabase()
    businessBusiness := business.NewBusiness(databaseDatabase)
    serviceImpl := service.NewService(businessBusiness)
    return serviceImpl
}
</code></pre>
<p>看一下wire生成的代码，和我们在manual中手动组装的代码基本是一样的。基于这份代码，我们调整一下main函数，主要是去掉手动组装的过程，改为直接调用InitializeService：</p>
<pre><code>// dependency-injection-examples/wire/demo/main.go

func main() {
    // make dependency injection by code generated by wire
    svc := InitializeService()
    client := NewClient(svc)
    fmt.Println(client.MakeRequest())
}
</code></pre>
<p>运行一下wire注入这个demo，其结果与manual demo是一致的：</p>
<pre><code>$cd dependency-injection-examples/wire/demo
$make
$./demo
Client request: Handled request: Business logic processed Data from database
</code></pre>
<p>关于wire，这里仅是作了“浅尝辄止”的介绍。要想深入了解wire的功能特性，可以阅读<a href="https://github.com/google/wire/blob/main/_tutorial/README.md">Wire tutorial</a>和<a href="https://github.com/google/wire/blob/main/docs/guide.md">Wire User Guide</a>。</p>
<p>接下来，我们再来看看如何使用uber/fx来实现依赖注入。</p>
<h2>3. uber/fx：运行时的依赖注入</h2>
<p>如果我没记错的话，uber应该是先开源的<a href="https://github.com/uber-go/dig">dig</a>，再有的<a href="https://github.com/uber-go/fx">fx</a>。dig是基于反射的依赖注入工具包，而fx则是由dig支撑的依赖注入框架。对应普通Go开发者而言，直接使用fx就对了。</p>
<p>下面是使用fx实现上面示例依赖注入的代码，我们只需要改造一下main.go：</p>
<pre><code>// dependency-injection-examples/fx/demo/main.go

func main() {
    app := fx.New(
        fx.Provide(
            fx.Annotate(
                service.NewService,
                fx.As(new(service.Service)),
            ),
        ),
        fx.Provide(
            fx.Annotate(
                business.NewBusiness,
                fx.As(new(business.BusinessLogic)),
            ),
        ),
        fx.Provide(
            fx.Annotate(
                database.NewDatabase,
                fx.As(new(database.DatabaseAccess)),
            ),
        ),

        fx.Invoke(func(svc service.Service) {
            client := NewClient(svc)
            fmt.Println(client.MakeRequest())
        }),
        fx.NopLogger, // no fx log output
    )

    app.Run()
}
</code></pre>
<p>我们在main函数中，使用fx.Provide注册了所有依赖类型的实例的构造方法(NewXXX)，然后将我们要执行的代码放入一个匿名函数，并传给fx.Invoke。当我们运行程序时，fx会在内存中构建对象调用依赖图，并使用Provide中注册的类型实例的构造方法构造实例，完成依赖注入和代码组装，然后运行传给Invoke的函数。</p>
<p>在向fx.Provide传递NewXXX时，我们使用了fx.Annotate，其目的与在wire示例中使用wire.Bind一样，即将一个类型实例转换为接口类型，以满足参数为接口类型的NewXXX的依赖所需。关于<a href="https://uber-go.github.io/fx/annotate.html#annotating-a-function">fx.Annotate的详细说明</a>，可参考fx的官方文档。</p>
<p>上述使用fx示例还有两处要提及一下，一个是使用fx.NopLogger关闭fx框架自身的日志输出；另外一个则是上述示例run起来后并不会自动退出，只有当按下ctrl+c后，程序才会因收到系统退出信号而退出！</p>
<p>对比fx和wire，你可能也发现了这样一点：fx将很多工作放到了“背后隐蔽处”，如果你不了解fx框架的运行机理，你很难使用好fx框架；而wire生成的代码就是编译到程序中的代码，没有额外的“魔法”。</p>
<p>当然fx不仅提供了Provide、Annotate、Invoke，其他一些功能特性大家可以自行到<a href="https://uber-go.github.io/fx/intro.html">官方文档</a>阅读并理解使用。</p>
<h2>4. 小结</h2>
<p>依赖注入常用来解决软件模块之间高度耦合的问题。传统的程序设计中，一个模块直接new或者静态调用另一个模块，这使得模块之间产生了强耦合。依赖注入将模块创建和注入的控制权移交给外部，由外部动态地将某个实现类实例注入到需要它的模块中。这样实现了模块之间的松耦合。</p>
<p>如果你来自Java等面向对象编程语言的群体，你对依赖注入肯定不陌生。</p>
<p>但是在Go社区，我觉得<strong>依赖注入并非惯用法</strong>。Go社区很多人崇尚“You often don&#8217;t need frameworks in Go”这样的信条。但凡引入一个框架，都会带来学习和理解上的额外负担，Go依赖注入框架亦是如此。</p>
<p>究竟是否使用依赖注入，完全取决于你在开发过程中的权衡和取舍。</p>
<p>如果你决定使用依赖注入，wire和fx都是你可选择的框架。就目前情况来看，fx是目前开发最active、历经生产考验最多的Go依赖注入框架，不过要想用好fx，必须深入理解fx的运行机制和底层原理，这又会带来一定的学习负担。</p>
<p>本文涉及的Go源码，可以在<a href="https://github.com/bigwhite/experiments/tree/master/dependency-injection-examples">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://book.douban.com/subject/30932387/">《Dependency Injection：Principles, Practices, and Patterns》</a> &#8211; https://book.douban.com/subject/30932387/</li>
<li><a href="https://go.dev/blog/wire">Compile-time Dependency Injection With Go Cloud&#8217;s Wire</a> &#8211; https://go.dev/blog/wire</li>
<li><a href="https://github.com/google/wire/blob/main/_tutorial/README.md">Wire tutorial</a> &#8211; https://github.com/google/wire/blob/main/_tutorial/README.md</li>
<li><a href="https://github.com/google/wire/blob/main/docs/guide.md">Wire User Guide</a> &#8211; https://github.com/google/wire/blob/main/docs/guide.md</li>
<li><a href="https://martinfowler.com/articles/injection.html">Inversion of Control Containers and the Dependency Injection pattern</a> &#8211; https://martinfowler.com/articles/injection.html</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/09/28/dependency-injection-with-go/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>gRPC客户端的那些事儿</title>
		<link>https://tonybai.com/2021/09/17/those-things-about-grpc-client/</link>
		<comments>https://tonybai.com/2021/09/17/those-things-about-grpc-client/#comments</comments>
		<pubDate>Fri, 17 Sep 2021 14:31:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[ghz]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[go1.17]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goreman]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[hey]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[lb]]></category>
		<category><![CDATA[loadbalancer]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[nacos]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[Naming]]></category>
		<category><![CDATA[Procfile]]></category>
		<category><![CDATA[protobuf]]></category>
		<category><![CDATA[protoc]]></category>
		<category><![CDATA[replace]]></category>
		<category><![CDATA[resolver]]></category>
		<category><![CDATA[RESTAPI]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[scheme]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-discovery]]></category>
		<category><![CDATA[service-register]]></category>
		<category><![CDATA[stream]]></category>
		<category><![CDATA[weight]]></category>
		<category><![CDATA[客户端]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[服务]]></category>
		<category><![CDATA[服务发现]]></category>
		<category><![CDATA[服务注册]]></category>
		<category><![CDATA[权重]]></category>
		<category><![CDATA[流式RPC]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[通信模式]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3293</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2021/09/17/those-things-about-grpc-client 在云原生与微服务主导架构模式的时代，内部服务间交互所采用的通信协议选型无非就是两类：HTTP API(RESTful API)和RPC。在如今的硬件配置与网络条件下，现代RPC实现的性能一般都是好于HTTP API的。我们以json over http与gRPC(insecure)作比较，分别使用ghz和hey压测gRPC和json over http的实现，gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。实测gPRC使用的protobuf的编解码性能更是最快的json编解码的2-3倍，是Go标准库json包编解码性能的10倍以上(具体数据见本文附录)。 对于性能敏感并且内部通信协议较少变动的系统来说，内部服务使用RPC可能是多数人的选择。而gRPC虽然不是性能最好的RPC实现，但作为有谷歌大厂背书且是CNCF唯一的RPC项目，gRPC自然得到了开发人员最广泛的关注与使用。 本文也来说说gRPC，不过我们更多关注一下gRPC的客户端，我们来看看使用gRPC客户端时都会考虑的那些事情（本文所有代码基于gRPC v1.40.0版本，Go 1.17版本)。 1. 默认的gRPC的客户端 gRPC支持四种通信模式，它们是（以下四张图截自《gRPC: Up and Running》一书）： 简单RPC(Simple RPC)：最简单的，也是最常用的gRPC通信模式，简单来说就是一请求一应答 服务端流RPC(Server-streaming RPC)：一请求，多应答 客户端流RPC(Client-streaming RPC)：多请求，一应答 双向流RPC(Bidirectional-Streaming RPC)：多请求，多应答 我们以最常用的Simple RPC(也称Unary RPC)为例来看一下如何实现一个gRPC版的helloworld。 我们无需自己从头来编写helloworld.proto并生成相应的gRPC代码，gRPC官方提供了一个helloworld的例子，我们仅需对其略微改造一下即可。 helloworld例子的IDL文件helloworld.proto如下： // https://github.com/grpc/grpc-go/tree/master/examples/helloworld/helloworld/helloworld.proto syntax = "proto3"; option go_package = "google.golang.org/grpc/examples/helloworld/helloworld"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2021/09/17/those-things-about-grpc-client">本文永久链接</a> &#8211; https://tonybai.com/2021/09/17/those-things-about-grpc-client</p>
<p>在云原生与微服务主导架构模式的时代，内部服务间交互所采用的通信协议选型无非就是两类：HTTP API(RESTful API)和RPC。在如今的硬件配置与网络条件下，现代RPC实现的性能一般都是好于HTTP API的。我们以json over http与<a href="https://grpc.io">gRPC</a>(insecure)作比较，分别使用<a href="https://github.com/bojand/ghz">ghz</a>和<a href="https://github.com/rakyll/hey">hey</a>压测gRPC和json over http的实现，gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。实测gPRC使用的protobuf的编解码性能更是最快的json编解码的2-3倍，是Go标准库json包编解码性能的10倍以上(具体数据见本文附录)。</p>
<p>对于性能敏感并且内部通信协议较少变动的系统来说，内部服务使用RPC可能是多数人的选择。而gRPC虽然不是性能最好的RPC实现，但作为有谷歌大厂背书且是<a href="https://www.cncf.io/projects/">CNCF唯一的RPC项目</a>，gRPC自然得到了开发人员最广泛的关注与使用。</p>
<p>本文也来说说gRPC，不过我们更多关注一下gRPC的客户端，我们来看看使用gRPC客户端时都会考虑的那些事情（本文所有代码基于gRPC v1.40.0版本，<a href="https://mp.weixin.qq.com/s/y_pC6GYeZnKuHG8ycNy6rg">Go 1.17版本</a>)。</p>
<h3>1. 默认的gRPC的客户端</h3>
<p>gRPC支持四种通信模式，它们是（以下四张图截自<a href="https://book.douban.com/subject/34796013/">《gRPC: Up and Running》</a>一书）：</p>
<ul>
<li>简单RPC(Simple RPC)：最简单的，也是最常用的gRPC通信模式，简单来说就是<strong>一请求一应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-2.png" alt="" /></p>
<ul>
<li>服务端流RPC(Server-streaming RPC)：<strong>一请求，多应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-3.png" alt="" /></p>
<ul>
<li>客户端流RPC(Client-streaming RPC)：<strong>多请求，一应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-4.png" alt="" /></p>
<ul>
<li>双向流RPC(Bidirectional-Streaming RPC)：<strong>多请求，多应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-5.png" alt="" /></p>
<p>我们以最常用的Simple RPC(也称Unary RPC)为例来看一下如何实现一个gRPC版的helloworld。</p>
<p>我们无需自己从头来编写helloworld.proto并生成相应的gRPC代码，<a href="https://github.com/grpc/grpc-go/tree/master/examples/helloworld">gRPC官方提供了一个helloworld的例子</a>，我们仅需对其略微改造一下即可。</p>
<p>helloworld例子的IDL文件helloworld.proto如下：</p>
<pre><code>// https://github.com/grpc/grpc-go/tree/master/examples/helloworld/helloworld/helloworld.proto

syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
</code></pre>
<p>对.proto文件的规范讲解大家可以参考<a href="https://grpc.io/docs/what-is-grpc/core-concepts/">grpc官方文档</a>，这里不赘述。显然上面这个IDL是极致简单的。这里定义了一个service：Greeter，它仅包含一个方法SayHello，并且这个方法的参数与返回值都是一个仅包含一个string字段的结构体。</p>
<p>我们无需手工执行protoc命令来基于该.proto文件生成对应的Greeter service的实现以及HelloRequest、HelloReply的protobuf编解码实现，因为gRPC在example下已经放置了生成后的Go源文件，我们直接引用即可。这里要注意，最新的<a href="https://github.com/grpc/grpc-go">grpc-go项目仓库</a>采用了多module的管理模式，examples作为一个独立的go module而存在，因此我们需要将其单独作为一个module导入到其使用者的项目中。以gRPC客户端greeter_client为例，它的go.mod要这样来写：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_client/go.mod
module github.com/bigwhite/grpc-client/demo1

go 1.17

require (
    google.golang.org/grpc v1.40.0
    google.golang.org/grpc/examples v1.40.0
)

require (
    github.com/golang/protobuf v1.4.3 // indirect
    golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
    golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
    golang.org/x/text v0.3.3 // indirect
    google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect
    google.golang.org/protobuf v1.25.0 // indirect
)

replace google.golang.org/grpc v1.40.0 =&gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go

replace google.golang.org/grpc/examples v1.40.0 =&gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go/examples
</code></pre>
<blockquote>
<p>注：grpc-go项目的标签(tag)似乎打的有问题，由于没有打grpc/examples/v1.40.0标签，go命令在grpc-go的v1.40.0标签中找不到examples，因此上面的go.mod中使用了一个replace trick(example module的v1.40.0版本是假的哦)，将examples module指向本地的代码。</p>
</blockquote>
<p>gRPC通信的两端我们也稍作改造。原greeter_client仅发送一个请求便退出，这里我们将其改为每隔2s发送请求（便于后续观察），如下面代码所示：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_client/main.go
... ...
func main() {
    // Set up a connection to the server.
    ctx, cf1 := context.WithTimeout(context.Background(), time.Second*3)
    defer cf1()
    conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) &gt; 1 {
        name = os.Args[1]
    }

    for i := 0; ; i++ {
        ctx, _ := context.WithTimeout(context.Background(), time.Second)
        r, err := c.SayHello(ctx, &amp;pb.HelloRequest{Name: fmt.Sprintf("%s-%d", name, i+1)})
        if err != nil {
            log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.GetMessage())
        time.Sleep(2 * time.Second)
    }
}
</code></pre>
<p>greeter_server加了一个命令行选项-port并支持<a href="https://www.imooc.com/read/87/article/2473">gRPC server的优雅退出</a>：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_server/main.go
... ...

var port int

func init() {
    flag.IntVar(&amp;port, "port", 50051, "listen port")
}

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &amp;server{})

    go func() {
        if err := s.Serve(lis); err != nil {
            log.Fatalf("failed to serve: %v", err)
        }
    }()

    var c = make(chan os.Signal)
    signal.Notify(c, os.Interrupt, os.Kill)
    &lt;-c
    s.Stop()
    fmt.Println("exit")
}
</code></pre>
<p>搞定go.mod以及对client和server进行改造ok后，我们就可以来构建和运行greeter_client和greeter_server了：</p>
<pre><code>编译和启动server：

$cd grpc-client/demo1/greeter_server
$make
$./demo1-server -port 50051
2021/09/11 12:10:33 Received: world-1
2021/09/11 12:10:35 Received: world-2
2021/09/11 12:10:37 Received: world-3
... ...

编译和启动client：
$cd grpc-client/demo1/greeter_client
$make
$./demo1-client
2021/09/11 12:10:33 Greeting: Hello world-1
2021/09/11 12:10:35 Greeting: Hello world-2
2021/09/11 12:10:37 Greeting: Hello world-3
... ...
</code></pre>
<p>我们看到：greeter_client和greeter_server启动后可以正常的通信！我们重点看一下greeter_client。</p>
<p>greeter_client在Dial服务端时传给DialContext的target参数是一个静态的服务地址：</p>
<pre><code>const (
      address     = "localhost:50051"
)
</code></pre>
<p>这个形式的target经过google.golang.org/grpc/internal/grpcutil.ParseTarget的解析后返回一个值为nil的resolver.Target。于是gRPC采用默认的scheme：”passthrough”(github.com/grpc/grpc-go/resolver/resolver.go)，默认的”passthrough” scheme下，gRPC将使用内置的passthrough resolver(google.golang.org/grpc/internal/resolver/passthrough)。默认的这个passthrough resolver是如何设置要连接的service地址的呢？下面是passthrough resolver的代码摘录：</p>
<pre><code>// github.com/grpc/grpc-go/internal/resolver/passthrough/passthrough.go

func (r *passthroughResolver) start() {
    r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}})
}
</code></pre>
<p>我们看到它将target.Endpoint，即localhost:50051直接传给了ClientConnection(上面代码的r.cc)，后者将向这个地址建立tcp连接。这正应了该resolver的名字：<strong>passthrough</strong>。</p>
<p>上面greeter_client连接的仅仅是service的一个实例(instance)，如果我们同时启动了该service的三个实例，比如使用<a href="https://github.com/mattn/goreman">goreman</a>通过加载脚本文件来启动多个service实例：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_server/Procfile

# Use goreman to run `go get github.com/mattn/goreman`
demo1-server1: ./demo1-server -port 50051
demo1-server2: ./demo1-server -port 50052
demo1-server3: ./demo1-server -port 50053

同时启动多实例：

$goreman start
15:22:12 demo1-server3 | Starting demo1-server3 on port 5200
15:22:12 demo1-server2 | Starting demo1-server2 on port 5100
15:22:12 demo1-server1 | Starting demo1-server1 on port 5000
</code></pre>
<p>那么我们应该如何告诉greeter_client去连接这三个实例呢？是否可以将address改为下面这样就可以了呢：</p>
<pre><code>const (
    address     = "localhost:50051,localhost:50052,localhost:50053"
    defaultName = "world"
)
</code></pre>
<p>我们来改改试试，修改后重新编译greeter_client，启动greeter_client，我们看到下面结果：</p>
<pre><code>$./demo1-client
2021/09/11 15:26:32 did not connect: context deadline exceeded
</code></pre>
<p>greeter_client连接server超时！也就是说像上面这样简单的传入多个实例的地址是不行的！那问题来了！我们该怎么让greeter_client去连接一个service的多个实例呢？我们继续向下看。</p>
<h3>2. 连接一个Service的多个实例(instance)</h3>
<p>grpc.Dial/grpc.DialContext的参数target可不仅仅是service实例的服务地址这么简单，<strong>它的实参(argument)形式决定了gRPC client将采用哪一个resolver来确定service实例的地址集合</strong>。</p>
<p>下面我们以一个返回service实例地址静态集合(即service的实例数量固定且服务地址固定)的StaticResolver为例，来看如何让gRPC client连接一个Service的多个实例。</p>
<h4>1) StaticResolver</h4>
<p>我们首先来设计一下传给grpc.DialContext的target形式。<a href="https://github.com/grpc/grpc/blob/master/doc/naming.md">关于gRPC naming resolution，gRPC有专门文档说明</a>。在这里，我们也创建一个新的scheme：static，多个service instance的服务地址通过逗号分隔的字符串传入，如下面代码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/main.go

const (
      address = "static:///localhost:50051,localhost:50052,localhost:50053"
)
</code></pre>
<p>当address被作为target的实参传入grpc.DialContext后，它会被grpcutil.ParseTarget解析为一个resolver.Target结构体，该结构体包含三个字段：</p>
<pre><code>// github.com/grpc/grpc-go/resolver/resolver.go
type Target struct {
    Scheme    string
    Authority string
    Endpoint  string
}
</code></pre>
<p>其中Scheme为”static”，Authority为空，Endpoint为”localhost:50051,localhost:50052,localhost:50053&#8243;。</p>
<p>接下来，gRPC会根据Target.Scheme的值到resolver包中的builder map中查找是否有对应的Resolver Builder实例。到目前为止gRPC内置的的resolver Builder都无法匹配该Scheme值。是时候自定义一个StaticResolver的Builder了！</p>
<p>grpc的resolve包定义了一个Builder实例需要实现的接口：</p>
<pre><code>// github.com/grpc/grpc-go/resolver/resolver.go 

// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
    // Build creates a new resolver for the given target.
    //
    // gRPC dial calls Build synchronously, and fails if the returned error is
    // not nil.
    Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
    // Scheme returns the scheme supported by this resolver.
    // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
    Scheme() string
}
</code></pre>
<p>Scheme方法返回这个Builder对应的scheme，而Build方法则是真正用于构建Resolver实例的方法，我们来看一下StaticBuilder的实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/builder.go

func init() {
    resolver.Register(&amp;StaticBuilder{}) //在init函数中将StaticBuilder实例注册到resolver包的Resolver map中
}

type StaticBuilder struct{}

func (sb *StaticBuilder) Build(target resolver.Target, cc resolver.ClientConn,
    opts resolver.BuildOptions) (resolver.Resolver, error) {

    // 解析target.Endpoint (例如：localhost:50051,localhost:50052,localhost:50053)
    endpoints := strings.Split(target.Endpoint, ",")

    r := &amp;StaticResolver{
        endpoints: endpoints,
        cc:        cc,
    }
    r.ResolveNow(resolver.ResolveNowOptions{})
    return r, nil
}

func (sb *StaticBuilder) Scheme() string {
    return "static" // 返回StaticBuilder对应的scheme字符串
}
</code></pre>
<p>在这个StaticBuilder实现中，init函数在包初始化是就将一个StaticBuilder实例注册到resolver包的Resolver map中。这样gRPC在Dial时就能通过target中的scheme找到该builder。Build方法是StaticBuilder的关键，在这个方法中，它首先解析传入的target.Endpoint，得到三个service instance的服务地址并存到新创建的StaticResolver实例中，并调用StaticResolver实例的ResolveNow方法确定即将连接的service instance集合。</p>
<p>和Builder一样，grpc的resolver包也定义了每个resolver需要实现的Resolver接口：</p>
<pre><code>// github.com/grpc/grpc-go/resolver/resolver.go 

// Resolver watches for the updates on the specified target.
// Updates include address updates and service config updates.
type Resolver interface {
    // ResolveNow will be called by gRPC to try to resolve the target name
    // again. It's just a hint, resolver can ignore this if it's not necessary.
    //
    // It could be called multiple times concurrently.
    ResolveNow(ResolveNowOptions)
    // Close closes the resolver.
    Close()
}
</code></pre>
<p>从这个接口注释我们也能看出，Resolver的实现负责监视(watch)服务测的地址与配置变化，并将变化更新给grpc的ClientConn。我们来看看我们的StaticResolver的实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/resolver.go

type StaticResolver struct {
    endpoints []string
    cc        resolver.ClientConn
    sync.Mutex
}

func (r *StaticResolver) ResolveNow(opts resolver.ResolveNowOptions) {
    r.Lock()
    r.doResolve()
    r.Unlock()
}

func (r *StaticResolver) Close() {
}

func (r *StaticResolver) doResolve() {
    var addrs []resolver.Address
    for i, addr := range r.endpoints {
        addrs = append(addrs, resolver.Address{
            Addr:       addr,
            ServerName: fmt.Sprintf("instance-%d", i+1),
        })
    }

    newState := resolver.State{
        Addresses: addrs,
    }

    r.cc.UpdateState(newState)
}
</code></pre>
<blockquote>
<p>注：resolver.Resolver接口的注释要求ResolveNow方法是要支持并发安全的，所以这里我们通过sync.Mutex来实现同步。</p>
</blockquote>
<p>由于服务侧的服务地址数量与信息都是不变的，因此这里并没有watch和update的过程，而只是在实现了ResolveNow(并在Builder中的Build方法中调用），在ResolveNow中将service instance的地址集合更新给ClientConnection(r.cc)。</p>
<p>接下来我们来编译与运行一下demo2的client与server：</p>
<pre><code>$cd grpc-client/demo2/greeter_server
$make
$goreman start
22:58:21 demo2-server1 | Starting demo2-server1 on port 5000
22:58:21 demo2-server2 | Starting demo2-server2 on port 5100
22:58:21 demo2-server3 | Starting demo2-server3 on port 5200

$cd grpc-client/demo2/greeter_client
$make
$./demo2-client
</code></pre>
<p>执行一段时间后，你会在server端的日志中发现一个问题，如下日志所示：</p>
<pre><code>22:57:16 demo2-server1 | 2021/09/11 22:57:16 Received: world-1
22:57:18 demo2-server1 | 2021/09/11 22:57:18 Received: world-2
22:57:20 demo2-server1 | 2021/09/11 22:57:20 Received: world-3
22:57:22 demo2-server1 | 2021/09/11 22:57:22 Received: world-4
22:57:24 demo2-server1 | 2021/09/11 22:57:24 Received: world-5
22:57:26 demo2-server1 | 2021/09/11 22:57:26 Received: world-6
22:57:28 demo2-server1 | 2021/09/11 22:57:28 Received: world-7
22:57:30 demo2-server1 | 2021/09/11 22:57:30 Received: world-8
22:57:32 demo2-server1 | 2021/09/11 22:57:32 Received: world-9
</code></pre>
<p>我们的Service instance集合中明明有三个地址，为何只有server1收到了rpc请求，其他两个server都处于空闲状态呢？这是客户端的负载均衡策略在作祟！默认情况下，grpc会为客户端选择内置的“pick_first”负载均衡策略，即在service instance集合中选择第一个intance进行请求。在这个例子中，在pick_first策略的作用下，grpc总是会选择demo2-server1发起rpc请求。</p>
<p>如果要将请求发到各个server上，我们可以将负载均衡策略改为另外一个内置的策略：round_robin，就像下面代码这样：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/main.go

conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithBalancerName("round_robin"))
</code></pre>
<p>重新编译运行greeter_client后，在server测我们就可以看到rpc请求被轮询地发到了每个server instance上了。</p>
<h4>2) Resolver原理</h4>
<p>我们再来用一幅图来梳理一下Builder以及Resolver的工作原理：</p>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-6.png" alt="" /></p>
<p>图中的SchemeResolver泛指实现了某一特定scheme的resolver。如图所示，service instance集合resolve过程的步骤大致如下：</p>
<ul>
<li>
<ol>
<li>SchemeBuilder将自身实例注册到resolver包的map中；</li>
</ol>
</li>
<li>
<ol>
<li>grpc.Dial/DialContext时使用特定形式的target参数</li>
</ol>
</li>
<li>
<ol>
<li>对target解析后，根据target.Scheme到resolver包的map中查找Scheme对应的Buider；</li>
</ol>
</li>
<li>
<ol>
<li>调用Buider的Build方法</li>
</ol>
</li>
<li>
<ol>
<li>Build方法构建出SchemeResolver实例；</li>
</ol>
</li>
<li>
<ol>
<li>后续由SchemeResolver实例监视service instance变更状态并在有变更的时候更新ClientConnection。</li>
</ol>
</li>
</ul>
<h4>3) NacosResolver</h4>
<p>在生产环境中，考虑到服务的高可用、可伸缩等，我们很少使用固定地址、固定数量的服务实例集合，更多是通过服务注册和发现机制自动实现服务实例集合的更新。这里我们再来实现一个基于<a href="https://nacos.io/zh-cn/">nacos</a>的NacosResolver，实现服务实例变更时grpc Client的自动调整(注：nacos的本地单节点安装方案见文本附录)，让示例具实战意义^_^。</p>
<p>由于有了上面关于Resolver原理的描述，这里简化了一些描述。</p>
<p>首先和StaticResolver一样，我们也来设计一下target的形式。nacos有namespace, group的概念，因此我们将target设计为如下形式：</p>
<pre><code>nacos://[authority]/host:port/namespace/group/serviceName
</code></pre>
<p>具体到我们的greeter_client中，其address为：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/main.go

const (
      address = "nacos:///localhost:8848/public/group-a/demo3-service" //no authority
)
</code></pre>
<p>接下来我们来看NacosBuilder：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/builder.go

func (nb *NacosBuilder) Build(target resolver.Target,
    cc resolver.ClientConn,
    opts resolver.BuildOptions) (resolver.Resolver, error) {

    // use info in target to access naming service
    // parse the target.endpoint
    // target.Endpoint - localhost:8848/public/DEFAULT_GROUP/serviceName, the addr of naming service :nacos endpoint
    sl := strings.Split(target.Endpoint, "/")
    nacosAddr := sl[0]
    namespace := sl[1]
    group := sl[2]
    serviceName := sl[3]
    sl1 := strings.Split(nacosAddr, ":")
    host := sl1[0]
    port := sl1[1]
    namingClient, err := initNamingClient(host, port, namespace, group)
    if err != nil {
        return nil, err
    }

    r := &amp;NacosResolver{
        namingClient: namingClient,
        cc:           cc,
        namespace:    namespace,
        group:        group,
        serviceName:  serviceName,
    }

    // initialize the cc's states
    r.ResolveNow(resolver.ResolveNowOptions{})

    // subscribe and watch
    r.watch()
    return r, nil
}

func (nb *NacosBuilder) Scheme() string {
    return "nacos"
}
</code></pre>
<p>NacosBuilder的Build方法流程也StaticBuilder并无二致，首先我们也是解析传入的target的Endpoint，即”localhost:8848/public/group-a/demo3-service”，并将解析后的各段信息存入新创建的NacosResolver实例中备用。NacosResolver还需要一个信息，那就是与nacos的连接，这里用initNamingClient创建一个nacos client端实例(调用<a href="https://github.com/nacos-group/nacos-sdk-go">nacos提供的go sdk</a>)。</p>
<p>接下来我们调用NacosResolver的ResolveNow获取一次nacos上demo3-service的服务实例列表并初始化ClientConn，最后我们调用NacosResolver的watch方法来订阅并监视demo3-service的实例变化。下面是NacosResolver的部分实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/resolver.go

func (r *NacosResolver) doResolve(opts resolver.ResolveNowOptions) {
    instances, err := r.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
    })
    if err != nil {
        fmt.Println(err)
        return
    }

    if len(instances) == 0 {
        fmt.Printf("service %s has zero instance\n", r.serviceName)
        return
    }

    // update cc.States
    var addrs []resolver.Address
    for i, inst := range instances {
        if (!inst.Enable) || (inst.Weight == 0) {
            continue
        }

        addrs = append(addrs, resolver.Address{
            Addr:       fmt.Sprintf("%s:%d", inst.Ip, inst.Port),
            ServerName: fmt.Sprintf("instance-%d", i+1),
        })
    }

    if len(addrs) == 0 {
        fmt.Printf("service %s has zero valid instance\n", r.serviceName)
    }

    newState := resolver.State{
        Addresses: addrs,
    }

    r.Lock()
    r.cc.UpdateState(newState)
    r.Unlock()
}

func (r *NacosResolver) ResolveNow(opts resolver.ResolveNowOptions) {
    r.doResolve(opts)
}

func (r *NacosResolver) Close() {
    r.namingClient.Unsubscribe(&amp;vo.SubscribeParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
    })
}

func (r *NacosResolver) watch() {
    r.namingClient.Subscribe(&amp;vo.SubscribeParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
        SubscribeCallback: func(services []model.SubscribeService, err error) {
            fmt.Printf("subcallback: %#v\n", services)
            r.doResolve(resolver.ResolveNowOptions{})
        },
    })
}
</code></pre>
<p>这里的一个重要实现是ResolveNow和watch都调用的doResolve方法，该方法通过nacos-go sdk中的SelectAllInstances获取demo-service3的所有实例，并将得到的enabled(=true)和权重(weight)不为0的合法实例集合更新给ClientConn(r.cc.UpdateState)。</p>
<p>在NacosResolver的watch方法中，我们通过nacos-go sdk中的Subscribe方法订阅demo3-service并提供了一个回调函数。这样每当demo3-service的实例发生变化时，该回调会被调用。在该回调中我们可以基于传回的最新的service实例集合（services []model.SubscribeService）来更新ClientConn，但在这里我们复用了doResolve方法，即又去nacos获取一次demo-service3的实例。</p>
<p>编译运行demo3下greeter_server：</p>
<pre><code>$cd grpc-client/demo3/greeter_server
$make
$goreman start
06:06:02 demo3-server3 | Starting demo3-server3 on port 5200
06:06:02 demo3-server1 | Starting demo3-server1 on port 5000
06:06:02 demo3-server2 | Starting demo3-server2 on port 5100
06:06:02 demo3-server3 | 2021-09-12T06:06:02.913+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50053&gt;   cacheDir:&lt;/tmp/nacos/cache/50053&gt;
06:06:02 demo3-server2 | 2021-09-12T06:06:02.913+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50052&gt;   cacheDir:&lt;/tmp/nacos/cache/50052&gt;
06:06:02 demo3-server1 | 2021-09-12T06:06:02.913+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50051&gt;   cacheDir:&lt;/tmp/nacos/cache/50051&gt;
</code></pre>
<p>运行greeter_server后，我们在nacos dashboard上会看到demo-service3的所有实例信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-7.png" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-8.png" alt="" /></p>
<p>编译运行demo3下greeter_client：</p>
<pre><code>$cd grpc-client/demo3/greeter_client
$make
$./demo3-client
2021-09-12T06:08:25.551+0800    INFO    nacos_client/nacos_client.go:87 logDir:&lt;/Users/tonybai/go/src/github.com/bigwhite/experiments/grpc-client/demo3/greeter_client/log&gt;   cacheDir:&lt;/Users/tonybai/go/src/github.com/bigwhite/experiments/grpc-client/demo3/greeter_client/cache&gt;
2021/09/12 06:08:25 Greeting: Hello world-1
2021/09/12 06:08:27 Greeting: Hello world-2
2021/09/12 06:08:29 Greeting: Hello world-3
2021/09/12 06:08:31 Greeting: Hello world-4
2021/09/12 06:08:33 Greeting: Hello world-5
2021/09/12 06:08:35 Greeting: Hello world-6
... ...
</code></pre>
<p>由于采用了round robin负载策略，greeter_server侧每个server(权重都为1)都会平等的收到rpc请求：</p>
<pre><code>06:06:36 demo3-server1 | 2021/09/12 06:06:36 Received: world-1
06:06:38 demo3-server3 | 2021/09/12 06:06:38 Received: world-2
06:06:40 demo3-server2 | 2021/09/12 06:06:40 Received: world-3
06:06:42 demo3-server1 | 2021/09/12 06:06:42 Received: world-4
06:06:44 demo3-server3 | 2021/09/12 06:06:44 Received: world-5
06:06:46 demo3-server2 | 2021/09/12 06:06:46 Received: world-6
... ...
</code></pre>
<p>这时我们可以通过nacos dashboard调整demo3-service的实例权重或下线某个实例，比如下线service instance-2(端口50052)，之后我们会看到greeter_client回调函数执行，之后greeter_server侧将只有实例1和实例3收到rpc请求。重新上线service instance-2后，一切会恢复正常。</p>
<h3>3. 自定义客户端balancer</h3>
<p>现实中服务端的实例所部署的主机(虚拟机/容器)算力可能不同，如果所有实例都使用相同权重1，那么肯定是不科学且存在算力浪费。但grpc-go内置的balancer实现有限，不能满足我们需求，我们就需要自定义一个可以满足我们需求的balancer了。</p>
<p>这里我们以自定义一个Weighted Round Robin(wrr) Balancer为例，看看自定义balancer的步骤（我们参考grpc-go中内置<a href="https://github.com/grpc/grpc-go/tree/master/balancer/roundrobin">round_robin的实现</a>）。</p>
<p>和resolver包相似，balancer也是通过一个Builder(创建模式)来实例化的，并且balancer的Balancer接口与resolver.Balancer差不多：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/balancer.go 

// Builder creates a balancer.
type Builder interface {
    // Build creates a new balancer with the ClientConn.
    Build(cc ClientConn, opts BuildOptions) Balancer
    // Name returns the name of balancers built by this builder.
    // It will be used to pick balancers (for example in service config).
    Name() string
}
</code></pre>
<p>通过Builder.Build方法我们构建一个Balancer接口的实现，Balancer接口定义如下：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/balancer.go 

type Balancer interface {
    // UpdateClientConnState is called by gRPC when the state of the ClientConn
    // changes.  If the error returned is ErrBadResolverState, the ClientConn
    // will begin calling ResolveNow on the active name resolver with
    // exponential backoff until a subsequent call to UpdateClientConnState
    // returns a nil error.  Any other errors are currently ignored.
    UpdateClientConnState(ClientConnState) error
    // ResolverError is called by gRPC when the name resolver reports an error.
    ResolverError(error)
    // UpdateSubConnState is called by gRPC when the state of a SubConn
    // changes.
    UpdateSubConnState(SubConn, SubConnState)
    // Close closes the balancer. The balancer is not required to call
    // ClientConn.RemoveSubConn for its existing SubConns.
    Close()
}
</code></pre>
<p>可以看到，Balancer要比Resolver要复杂很多。gRPC的核心开发者们也看到了这一点，于是他们提供了一个可简化自定义Balancer创建的包：google.golang.org/grpc/balancer/base。gRPC内置的round_robin Balancer也是基于base包实现的。</p>
<p>base包提供了NewBalancerBuilder可以快速返回一个balancer.Builder的实现：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/base/base.go 

// NewBalancerBuilder returns a base balancer builder configured by the provided config.
func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder {
    return &amp;baseBuilder{
        name:          name,
        pickerBuilder: pb,
        config:        config,
    }
}
</code></pre>
<p>我们看到，这个函数接收一个参数：pb，它的类型是PikcerBuilder，这个接口类型则比较简单：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/base/base.go 

// PickerBuilder creates balancer.Picker.
type PickerBuilder interface {
    // Build returns a picker that will be used by gRPC to pick a SubConn.
    Build(info PickerBuildInfo) balancer.Picker
}
</code></pre>
<p>我们仅需要提供一个PickerBuilder的实现以及一个balancer.Picker的实现即可，而Picker则是仅有一个方法的接口类型：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/balancer.go 

type Picker interface {
    Pick(info PickInfo) (PickResult, error)
}
</code></pre>
<p>嵌套的有些多，我们用下面这幅图来直观看一下balancer的创建和使用流程：</p>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-9.png" alt="" /></p>
<p>再简述一下大致流程：</p>
<ul>
<li>首先要注册一个名为”my_weighted_round_robin”的balancer Builder:wrrBuilder，该Builder由base包的NewBalancerBuilder构建；</li>
<li>base包的NewBalancerBuilder函数需要传入一个PickerBuilder实现，于是我们需要自定义一个返回Picker接口实现的PickerBuilder。</li>
<li>grpc.Dial调用时传入一个WithBalancerName(“my_weighted_round_robin”)，grpc通过balancer Name从已注册的balancer builder中选出我们实现的wrrBuilder，并调用wrrBuilder创建Picker：wrrPicker。</li>
<li>在grpc实施rpc调用SayHello时，wrrPicker的Pick方法会被调用，选出一个Connection，并在该connection上发送rpc请求。</li>
</ul>
<p>由于用到的权重值，我们的resolver实现需要做一些变动，主要是在doResolve方法时将service instance的权重(weight)通过Attribute设置到ClientConnection中：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo4/greeter_client/resolver.go

func (r *NacosResolver) doResolve(opts resolver.ResolveNowOptions) {
    instances, err := r.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
    })
    if err != nil {
        fmt.Println(err)
        return
    }

    if len(instances) == 0 {
        fmt.Printf("service %s has zero instance\n", r.serviceName)
        return
    }

    // update cc.States
    var addrs []resolver.Address
    for i, inst := range instances {
        if (!inst.Enable) || (inst.Weight == 0) {
            continue
        }

        addr := resolver.Address{
            Addr:       fmt.Sprintf("%s:%d", inst.Ip, inst.Port),
            ServerName: fmt.Sprintf("instance-%d", i+1),
        }
        addr.Attributes = addr.Attributes.WithValues("weight", int(inst.Weight)) //考虑权重并纳入cc的状态中
        addrs = append(addrs, addr)
    }

    if len(addrs) == 0 {
        fmt.Printf("service %s has zero valid instance\n", r.serviceName)
    }

    newState := resolver.State{
        Addresses: addrs,
    }

    r.Lock()
    r.cc.UpdateState(newState)
    r.Unlock()
}
</code></pre>
<p>接下来我们重点看看greeter_client中wrrPickerBuilder与wrrPicker的实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo4/greeter_client/balancer.go

type wrrPickerBuilder struct{}

func (*wrrPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
    if len(info.ReadySCs) == 0 {
        return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
    }

    var scs []balancer.SubConn
    // 提取已经就绪的connection的权重信息，作为Picker实例的输入
    for subConn, addr := range info.ReadySCs {
        weight := addr.Address.Attributes.Value("weight").(int)
        if weight &lt;= 0 {
            weight = 1
        }
        for i := 0; i &lt; weight; i++ {
            scs = append(scs, subConn)
        }
    }

    return &amp;wrrPicker{
        subConns: scs,
        // Start at a random index, as the same RR balancer rebuilds a new
        // picker when SubConn states change, and we don't want to apply excess
        // load to the first server in the list.
        next: rand.Intn(len(scs)),
    }
}

type wrrPicker struct {
    // subConns is the snapshot of the roundrobin balancer when this picker was
    // created. The slice is immutable. Each Get() will do a round robin
    // selection from it and return the selected SubConn.
    subConns []balancer.SubConn

    mu   sync.Mutex
    next int
}

// 选出一个Connection
func (p *wrrPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
    p.mu.Lock()
    sc := p.subConns[p.next]
    p.next = (p.next + 1) % len(p.subConns)
    p.mu.Unlock()
    return balancer.PickResult{SubConn: sc}, nil
}
</code></pre>
<p>这是一个简单的Weighted Round Robin实现，加权算法十分简单，如果一个conn的权重为n，那么就在加权结果集中加入n个conn，这样在后续Pick时不需要考虑加权的问题，只需向普通Round Robin那样逐个Pick出来即可。</p>
<p>运行demo4 greeter_server后，我们在nacos将instance-1的权重改为5，我们后续就会看到如下输出：</p>
<pre><code>$goreman start
09:20:18 demo4-server3 | Starting demo4-server3 on port 5200
09:20:18 demo4-server2 | Starting demo4-server2 on port 5100
09:20:18 demo4-server1 | Starting demo4-server1 on port 5000
09:20:18 demo4-server2 | 2021-09-12T09:20:18.633+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50052&gt;   cacheDir:&lt;/tmp/nacos/cache/50052&gt;
09:20:18 demo4-server1 | 2021-09-12T09:20:18.633+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50051&gt;   cacheDir:&lt;/tmp/nacos/cache/50051&gt;
09:20:18 demo4-server3 | 2021-09-12T09:20:18.633+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50053&gt;   cacheDir:&lt;/tmp/nacos/cache/50053&gt;
09:20:23 demo4-server2 | 2021/09/12 09:20:23 Received: world-1
09:20:25 demo4-server3 | 2021/09/12 09:20:25 Received: world-2
09:20:27 demo4-server1 | 2021/09/12 09:20:27 Received: world-3
09:20:29 demo4-server2 | 2021/09/12 09:20:29 Received: world-4
09:20:31 demo4-server3 | 2021/09/12 09:20:31 Received: world-5
09:20:33 demo4-server1 | 2021/09/12 09:20:33 Received: world-6
09:20:35 demo4-server2 | 2021/09/12 09:20:35 Received: world-7
09:20:37 demo4-server3 | 2021/09/12 09:20:37 Received: world-8
09:20:39 demo4-server1 | 2021/09/12 09:20:39 Received: world-9
09:20:41 demo4-server2 | 2021/09/12 09:20:41 Received: world-10
09:20:43 demo4-server1 | 2021/09/12 09:20:43 Received: world-11
09:20:45 demo4-server2 | 2021/09/12 09:20:45 Received: world-12
09:20:47 demo4-server3 | 2021/09/12 09:20:47 Received: world-13
//这里将权重改为5后
09:20:49 demo4-server1 | 2021/09/12 09:20:49 Received: world-14
09:20:51 demo4-server1 | 2021/09/12 09:20:51 Received: world-15
09:20:53 demo4-server1 | 2021/09/12 09:20:53 Received: world-16
09:20:55 demo4-server1 | 2021/09/12 09:20:55 Received: world-17
09:20:57 demo4-server1 | 2021/09/12 09:20:57 Received: world-18
09:20:59 demo4-server2 | 2021/09/12 09:20:59 Received: world-19
09:21:01 demo4-server3 | 2021/09/12 09:21:01 Received: world-20
09:21:03 demo4-server1 | 2021/09/12 09:21:03 Received: world-21
</code></pre>
<p>注意：每次nacos的service instance发生变化后，balancer都会重新build一个新Picker实例，后续会使用新Picker实例在其Connection集合中Pick出一个conn。</p>
<h3>4. 小结</h3>
<p>在本文中我们了解了gRPC的四种通信模式。我们重点关注了在最常用的simple RPC(unary RPC)模式下gRPC Client侧需要考虑的事情，包括：</p>
<ul>
<li>如何实现一个helloworld的一对一的通信</li>
<li>如何实现一个自定义的Resolver以实现一个client到一个静态服务实例集合的通信</li>
<li>如何实现一个自定义的Resolver以实现一个client到一个动态服务实例集合的通信</li>
<li>如何自定义客户端Balancer</li>
</ul>
<p>本文代码仅做示例使用，并未考虑太多异常处理。</p>
<p>本文涉及的所有代码可以从<a href="https://github.com/bigwhite/experiments/tree/master/grpc-client">这里下载</a>：https://github.com/bigwhite/experiments/tree/master/grpc-client</p>
<h3>5. 参考资料</h3>
<ul>
<li>gRPC Name Resolution &#8211; https://github.com/grpc/grpc/blob/master/doc/naming.md</li>
<li>Load Balancing in gRPC &#8211; https://github.com/grpc/grpc/blob/master/doc/load-balancing.md</li>
<li>基于 gRPC的服务发现与负载均衡（基础篇）- https://pandaychen.github.io/2019/07/11/GRPC-SERVICE-DISCOVERY/</li>
<li>比较 gRPC服务和HTTP API &#8211; https://docs.microsoft.com/zh-cn/aspnet/core/grpc/comparison</li>
</ul>
<h3>6. 附录</h3>
<h4>1) json vs. protobuf编解码性能基准测试结果</h4>
<p>测试源码位于<a href="https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/codec">这里</a>：https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/codec</p>
<p>我们使用了Go标准库json编解码、字节开源的<a href="https://github.com/bytedance/sonic">sonic json编解码包</a>以及<a href="https://tonybai.com/2020/03/16/build-high-performance-object-storage-with-minio-part1-prototype/">minio</a>开源的<a href="https://github.com/minio/simdjson-go">simdjson-go</a>高性能json解析库与protobuf作对比的结果如下：</p>
<pre><code>$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/codec
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkSimdJsonUnmarshal-8           43304         28177 ns/op      113209 B/op         19 allocs/op
BenchmarkJsonUnmarshal-8              153214          7187 ns/op        1024 B/op          6 allocs/op
BenchmarkJsonMarshal-8                601590          2057 ns/op        2688 B/op          2 allocs/op
BenchmarkSonicJsonUnmarshal-8        1394211           861.1 ns/op      2342 B/op          2 allocs/op
BenchmarkSonicJsonMarshal-8          1592898           765.2 ns/op      2239 B/op          4 allocs/op
BenchmarkProtobufUnmarshal-8         3823441           317.0 ns/op      1208 B/op          3 allocs/op
BenchmarkProtobufMarshal-8           4461583           274.8 ns/op      1152 B/op          1 allocs/op
PASS
ok      github.com/bigwhite/codec   10.901s
</code></pre>
<p>benchmark测试结果印证了protobuf的编解码性能要远高于json编解码。但是在benchmark结果中，一个结果让我很意外，那就是号称高性能的simdjson-go的数据难看到离谱。谁知道为什么吗？simd指令没生效？字节开源的sonic的确性能很好，与pb也就2-3倍的差距，没有数量级的差距。</p>
<h4>2) gRPC(insecure) vs. json over http</h4>
<p>测试源码位于<a href="https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/protocol">这里</a>：https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/protocol</p>
<p>使用ghz对gRPC实现的server进行压测结果如下：</p>
<pre><code>$ghz --insecure -n 100000 -c 500 --proto publish.proto --call proto.PublishService.Publish -D data.json localhost:10000

Summary:
  Count:    100000
  Total:    1.67 s
  Slowest:    48.49 ms
  Fastest:    0.13 ms
  Average:    6.34 ms
  Requests/sec:    59924.34

Response time histogram:
  0.133  [1]     |
  4.968  [40143] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  9.803  [47335] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  14.639 [11306] |∎∎∎∎∎∎∎∎∎∎
  19.474 [510]   |
  24.309 [84]    |
  29.144 [89]    |
  33.980 [29]    |
  38.815 [3]     |
  43.650 [8]     |
  48.485 [492]   |

Latency distribution:
  10 % in 3.07 ms
  25 % in 4.12 ms
  50 % in 5.49 ms
  75 % in 7.94 ms
  90 % in 10.24 ms
  95 % in 11.28 ms
  99 % in 15.52 ms

Status code distribution:
  [OK]   100000 responses
</code></pre>
<p>使用hey对使用<a href="https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp">fasthttp</a>与sonic实现的http server进行压测结果如下：</p>
<pre><code>$hey -n 100000 -c 500  -m POST -D ./data.json http://127.0.0.1:10001/

Summary:
  Total:    2.0012 secs
  Slowest:    0.1028 secs
  Fastest:    0.0001 secs
  Average:    0.0038 secs
  Requests/sec:    49969.9234

Response time histogram:
  0.000 [1]     |
  0.010 [96287] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.021 [2639]  |■
  0.031 [261]   |
  0.041 [136]   |
  0.051 [146]   |
  0.062 [128]   |
  0.072 [43]    |
  0.082 [24]    |
  0.093 [10]    |
  0.103 [4]     |

Latency distribution:
  10% in 0.0013 secs
  25% in 0.0020 secs
  50% in 0.0031 secs
  75% in 0.0040 secs
  90% in 0.0062 secs
  95% in 0.0089 secs
  99% in 0.0179 secs

Details (average, fastest, slowest):
  DNS+dialup:    0.0000 secs, 0.0001 secs, 0.1028 secs
  DNS-lookup:    0.0000 secs, 0.0000 secs, 0.0000 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0202 secs
  resp wait:    0.0031 secs, 0.0000 secs, 0.0972 secs
  resp read:    0.0005 secs, 0.0000 secs, 0.0575 secs

Status code distribution:
  [200]    99679 responses
</code></pre>
<p>我们看到：gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。</p>
<h4>3) nacos docker安装</h4>
<p>单机容器版nacos安装步骤如下：</p>
<pre><code>$git clone https://github.com/nacos-group/nacos-docker.git
$cd nacos-docker
$docker-compose -f example/standalone-derby.yaml up
</code></pre>
<p>nacos相关容器启动成功后，可以打开浏览器访问http://localhost:8848/nacos，打开nacos仪表盘登录页面，输入nacos/nacos即可进入nacos web操作界面。</p>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：</p>
<ul>
<li>Go技术书籍的书摘和读书体会系列</li>
<li>Go与eBPF系列</li>
</ul>
<p>欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/202103/gopher-tribe-zsxq-card.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订<br />
阅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/09/17/those-things-about-grpc-client/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>以单件方式创建和获取数据库实例</title>
		<link>https://tonybai.com/2021/02/09/create-and-get-db-access-instance-through-singleton/</link>
		<comments>https://tonybai.com/2021/02/09/create-and-get-db-access-instance-through-singleton/#comments</comments>
		<pubDate>Tue, 09 Feb 2021 02:47:47 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[goland]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gorm]]></category>
		<category><![CDATA[JetBrains]]></category>
		<category><![CDATA[singleton]]></category>
		<category><![CDATA[sync.Once]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[单件]]></category>
		<category><![CDATA[单例]]></category>
		<category><![CDATA[数据库]]></category>
		<category><![CDATA[设计模式]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3092</guid>
		<description><![CDATA[在屡次的Go用户调查中，使用Go语言进行Web服务/API开发都占据了Go语言用途调查结果的头部位置。下面是知名Go IDE goland的母公司JetBrains最新发布的Go当前状态报告(2021.2.3)中的截图： 开发Web或API服务，难免会与数据库打交道。如今创建数据库实例并访库的技术已经是很成熟了，于是就有了下面这样的程序结构： 上面这个图片中，Web服务中的每个要与数据库进行数据交互的包都是自己创建并使用数据库实例，这显然是一种糟糕的设计，它不仅让每个包都耦合外部的第三方数据包，每个包还担负起管理数据库连接的责任，并且在Web服务的整个项目中，还会存在多处获取数据库连接配置、打开关闭数据库等的重复代码。一旦数据库访问代码发生变化，这些包就都得修改一遍。 那么如何优化呢？一个很自然的想法：将创建数据库实例以及对数据库实例的获取封装到一个包中，其他包无需再关心数据库实例的创建与释放，直接获取和使用实例即可，如下面示意图： 从这段描述来看，这显然是单件(singleton，亦翻译为单例)这个“创建型”模式的应用场景。在这里我们给出一个用Go实现的以单件方式创建和获取数据库实例的demo。 Go语言标准库提供了sync.Once类型，这让Go实现单件模式变得天然简单了。为了模拟上述场景，我们先来描述一下demo项目的结构： database-singleton ├── Makefile ├── cmd │   └── main │   └── main.go ├── conf │   └── database.conf ├── go.mod ├── go.sum └── pkg ├── config │   └── config.go ├── db │   └── db.go ├── model │   └── employee.go ├── reader │   └── reader.go └── updater └── updater.go 在database-singleton这个repo中： [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/create-and-get-db-access-instance-through-singleton-0.png" alt="img{512x368}" /></p>
<p>在屡次的Go用户调查中，使用Go语言进行Web服务/API开发都占据了Go语言用途调查结果的头部位置。下面是知名Go IDE <a href="https://blog.jetbrains.com/go/">goland</a>的母公司JetBrains最新发布的<a href="https://blog.jetbrains.com/go/2021/02/03/the-state-of-go/">Go当前状态报告(2021.2.3)</a>中的截图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/create-and-get-db-access-instance-through-singleton-3.png" alt="img{512x368}" /></p>
<p>开发Web或API服务，难免会与数据库打交道。如今创建数据库实例并访库的技术已经是很成熟了，于是就有了下面这样的程序结构：</p>
<p><img src="https://tonybai.com/wp-content/uploads/create-and-get-db-access-instance-through-singleton-1.png" alt="img{512x368}" /></p>
<p>上面这个图片中，Web服务中的每个要与数据库进行数据交互的包都是自己创建并使用数据库实例，这显然是一种糟糕的设计，它不仅让每个包都耦合外部的第三方数据包，每个包还担负起管理数据库连接的责任，并且在Web服务的整个项目中，还会存在多处获取数据库连接配置、打开关闭数据库等的重复代码。一旦数据库访问代码发生变化，这些包就都得修改一遍。</p>
<p>那么如何优化呢？一个很自然的想法：将创建数据库实例以及对数据库实例的获取封装到一个包中，其他包无需再关心数据库实例的创建与释放，直接获取和使用实例即可，如下面示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/create-and-get-db-access-instance-through-singleton-2.png" alt="img{512x368}" /></p>
<p>从这段描述来看，这显然是单件(singleton，亦翻译为单例)这个“创建型”模式的应用场景。在这里我们给出一个用Go实现的以单件方式创建和获取数据库实例的demo。</p>
<p>Go语言标准库提供了sync.Once类型，这让Go实现单件模式变得天然简单了。为了模拟上述场景，我们先来描述一下demo项目的结构：</p>
<pre><code>database-singleton
├── Makefile
├── cmd
│   └── main
│       └── main.go
├── conf
│   └── database.conf
├── go.mod
├── go.sum
└── pkg
    ├── config
    │   └── config.go
    ├── db
    │   └── db.go
    ├── model
    │   └── employee.go
    ├── reader
    │   └── reader.go
    └── updater
        └── updater.go
</code></pre>
<p>在database-singleton这个repo中：</p>
<ul>
<li>pkg/db就是我们将数据库实例的创建和获取封装到单件中的实现；</li>
<li>pkg/reader和pkg/updater则模拟了两个通过单件获取数据库实例并分别读取和更新数据库的包；</li>
<li>pkg/config是数据库连接配置的读取包。关于go程序的配置读取方案的一些方案，可以参考我的<a href="https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/">《写Go代码时遇到的那些问题[第1期]》</a>一文。</li>
</ul>
<p>我们从cmd/main/main.go中，可以看到整个程序的运行结构：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/database-singleton/cmd/main/main.go
package main

import (
    "log"
    "os"
    "os/signal"
    "sync"
    "syscall"

    "github.com/bigwhite/testdboper/pkg/config"
    "github.com/bigwhite/testdboper/pkg/db"
    "github.com/bigwhite/testdboper/pkg/reader"
    "github.com/bigwhite/testdboper/pkg/updater"
)

func init() {
    err := config.Init()
    if err != nil {
        panic(err)
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    var quit = make(chan struct{})

    // do some init from db
    _ = db.DB()

    go func() {
        updater.Run(quit)
        wg.Done()
    }()
    go func() {
        reader.Run(quit)
        wg.Done()
    }()

    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)

    _ = &lt;-c
    close(quit)
    log.Printf("recv exit signal...")
    wg.Wait()
    log.Printf("program exit ok")
}
</code></pre>
<p>简单解释一下上面main.go中的代码：</p>
<ul>
<li>在init函数中，我们读取了用于整个程序的配置信息，主要是数据库的连接信息(ip、port、user、password等)；</li>
<li>我们启动了两个独立的goroutine，分别运行reader和updater两个模拟数据库读写场景的包；</li>
<li>我们使用quit channel通知两个goroutine退出，并使用sync.WaitGroup来等待两个goroutine的结束(关于goroutine的并发模式的详解，可以参考我的专栏文章<a href="https://www.imooc.com/read/87/article/2430">《Go并发模型和常见并发模式》</a>；</li>
<li>我们使用signal.Notify监听系统信号，并在收到系统信号后做出响应（关于signal包的使用，请参见我的专栏文章<a href="https://www.imooc.com/read/87/article/2473">《小心被kill！不要忽略对系统信号的处理》</a>）。</li>
</ul>
<p>在main函数代码中，我们看到了如下调用：</p>
<pre><code>    // do some init from db
    _ = db.DB()
</code></pre>
<p>这是在初始化的时候通过单件获取访问数据库的对象实例，但这个不是必须的，只有在初始化需要从数据库读取一些信息时才会用到。</p>
<p>接下来，我们就来看看创建数据库访问实例的单件是如何实现的：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/database-singleton/pkg/db/db.go
package db

import (
    "fmt"
    "sync"
    "time"

    "github.com/bigwhite/testdboper/pkg/config"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

var once sync.Once

type database struct {
    instance    *gorm.DB
    maxIdle     int
    maxOpen     int
    maxLifetime time.Duration
}

type Option func(db *database)

var db *database

func WithMaxIdle(maxIdle int) Option {
    return func(d *database) {
        d.maxIdle = maxIdle
    }
}
func WithMaxOpen(maxOpen int) Option {
    return func(d *database) {
        d.maxOpen = maxOpen
    }
}

func DB(opts ...Option) *gorm.DB {
    once.Do(func() {
        db = new(database)
        for _, f := range opts {
            f(db)
        }

        dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&amp;parseTime=True&amp;loc=Local",
            config.Config.Database.User,
            config.Config.Database.Password,
            config.Config.Database.IP,
            config.Config.Database.Port,
            config.Config.Database.DB)
        var err error
        db.instance, err = gorm.Open("mysql", dsn) // database: *gorm.DB
        if err != nil {
            panic(err)
        }

        sqlDB := db.instance.DB()
        if err != nil {
            panic(err)
        }

        if db.maxIdle != 0 {
            sqlDB.SetMaxIdleConns(db.maxIdle)
        }

        if db.maxLifetime != 0 {
            sqlDB.SetConnMaxLifetime(db.maxLifetime)
        }

        if db.maxOpen != 0 {
            sqlDB.SetMaxOpenConns(db.maxOpen)
        }

    })
    return db.instance
}

</code></pre>
<ul>
<li>首先，上述代码使用sync.Once对象辅助实现单件模式，传给once.Do方法的函数在整个程序生命周期中执行且只执行一次。我们就是在这个函数中创建的数据库访问实例；</li>
<li>这里我们使用gorm库承担访问数据库的任务，因此所谓的实例，即gorm.DB类型的指针；</li>
<li>gorm.DB类型是并发安全的，我们无需考虑单件返回的实例的并发访问问题；</li>
<li>gorm.DB底层使用的是标准库database/sql维护的<a href="https://gorm.io/docs/connecting_to_the_database.html#Connection-Pool">连接池</a>，因此一旦gorm.DB实例建立成功，对连接的维护也全部交由它去处理，我们在业务层无需考虑保活和断连后重连问题；</li>
<li>这里我们没有将获取单件的函数DB设计为不带参数的函数，而是将其设计为携带可变参数列表的函数，这主要是考虑在初次调用DB函数时，可以对底层的连接池进行设置(MaxIdleConn、MaxLifetime、MaxOpenConn)。其他情况使用时，无需传入任何参数；当然由于返回的是gorm.DB的指针，因此外层也是可以基于该指针自行设置连接池的，但在业务层动态更改连接池属性似乎并不可取；</li>
<li>谈到可变参数函数，这里使用了功能选项(functional option)的设计，更多关于Go语言变长参数的妙用，可以参考我的专栏文章<a href="https://www.imooc.com/read/87/article/2424">《变长参数函数的妙用》</a>。</li>
</ul>
<p>接下来，我们再看看reader和updater对单件函数db.DB的使用，以reader为例：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/database-singleton/pkg/reader/reader.go
package reader

import (
    "log"
    "time"

    "github.com/bigwhite/testdboper/pkg/db"
    "github.com/bigwhite/testdboper/pkg/model"
)

func dumpEmployee() {
    var rs []model.Employee // rs: record slice
    d := db.DB()
    d.Find(&amp;rs)
    log.Println(rs)
}

func Run(quit &lt;-chan struct{}) {
    tk := time.NewTicker(5 * time.Second)
    for {
        select {
        case &lt;-tk.C:
            dumpEmployee()

        case &lt;-quit:
            return
        }
    }
}
</code></pre>
<p>我们看到，reader的Run函数通过定时器每隔5s读取数据库表employee的内容，并输出。dumpEmployee函数通过db.DB非常容易的获取到访问数据库的实例，再也无需自行管理数据库的打开和关闭操作了。</p>
<p>最后，我们说一下DB连接的释放。我们在上面的代码中并没有看到显式的db连接的释放，因此在这样的程序中，始终都需要访问和操作数据库。释放db连接的时候，也是程序退出的时候，当进程退出，与db之间的连接会自动释放，因此无需再显式释放。</p>
<blockquote>
<p>注：对于mysql而言，我们可以通过下面命令查看数据库的当前连接数：</p>
</blockquote>
<pre><code>mysql&gt; show status like  'Threads%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_cached    | 2     |
| Threads_connected | 3     |
| Threads_created   | 5     |
| Threads_running   | 2     |
+-------------------+-------+
4 rows in set (0.00 sec)
</code></pre>
<p>以上示例代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/database-singleton">这里</a> https://github.com/bigwhite/experiments/tree/master/database-singleton 下载 。</p>
<h3>附录</h3>
<ul>
<li>mysql安装设置(on ubuntu)</li>
</ul>
<pre><code>// ubuntu 18.04, mysql 5.7.33

安装mysql：

$apt-get install mysql-server mysql-client

查看mysql安装成功与否：

$ps -ef|grep mysql
mysql    23965     1  0 22:55 ?        00:00:00 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid

设置root密码：

$cat /etc/mysql/debian.cnf

# Automatically generated for Debian scripts. DO NOT TOUCH!
[client]
host     = localhost
user     = debian-sys-maint
password = xxxxxxxxxx
socket   = /var/run/mysqld/mysqld.sock

使用debian-sys-maint/xxxxxxxxxx 登录数据库：

$mysql -u debian-sys-maint -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.7.33-0ubuntu0.18.04.1 (Ubuntu)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql&gt; use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql&gt; update user set authentication_string=PASSWORD("root123") where user='root';
Query OK, 1 row affected, 1 warning (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 1

mysql&gt; update user set plugin="mysql_native_password";
Query OK, 1 row affected (0.00 sec)
Rows matched: 4  Changed: 1  Warnings: 0

mysql&gt; flush privileges;
Query OK, 0 rows affected (0.01 sec)

root密码生效：

重启mysql服务后，root密码才能生效。

$systemctl restart mysql.service
</code></pre>
<ul>
<li>demo1数据和employee表的创建</li>
</ul>
<pre><code>&gt;create database demo1;
&gt; CREATE TABLE `employee` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `age` int NOT NULL,
  `gender` varchar(8) NOT NULL,
  `birthday` char(14) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
</code></pre>
<hr />
<p>“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦。</p>
<p><img src="http://image.tonybai.com/img/202011/go-column-pgo-with-qr-and-text.png" alt="" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/02/09/create-and-get-db-access-instance-through-singleton/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
