<?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/%e6%95%b0%e7%bb%84/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 08 Jun 2026 23:32:23 +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>我的 Gopher “长期主义”：从《Go语言第一课》新书说起</title>
		<link>https://tonybai.com/2025/08/28/go-primer-published/</link>
		<comments>https://tonybai.com/2025/08/28/go-primer-published/#comments</comments>
		<pubDate>Thu, 28 Aug 2025 00:26:06 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[Claude]]></category>
		<category><![CDATA[Copilot]]></category>
		<category><![CDATA[deepseek]]></category>
		<category><![CDATA[gemini]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[gopherchina]]></category>
		<category><![CDATA[gopherdaily]]></category>
		<category><![CDATA[goprimer]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Prompt]]></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=5090</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/28/go-primer-published 大家好，我是Tony Bai。 前不久，在知乎上看到一个关于 Go 社区的帖子，其中一条评论让我感慨良多： “GopherChina 都没了，国内还有几人坚持？Tony Bai好像还在更新” 短短一句话，道尽了社区的变迁与坚持的不易。这句来自读者的回答，让我内心欣慰，也让我有机会停下来，审视自己在这条路上走了多远，以及为什么还要继续走下去。 答案或许很简单，就是三个字：长期主义。 我的个人博客 tonybai.com，从 2004 年断断续续更新至今，已经走过了二十个年头。而我在 Go 语言这条路上的“长期主义”，则始于 2011 年。那时，Go 尚处襁褓，在国内几乎无人问津。我凭借着一股直觉和热爱，一头扎了进去，成为了国内最早一批的 Go 语言探索者。 十余年来，这份坚持从未间断。从早期的博客分享，到后来出版的《Go语言精进之路》；从 GopherChina 大会的讲台，到几乎每日更新的 GopherDaily，我一直在尽我所能地为社区贡献。 这份坚持也延续到了今年。从年初开始，我在公众号上陆续推出了多个“微专栏”系列，深入探讨 Go 源码与实践的细节；与此同时，我的新课程《Go语言进阶课》也已在极客时间上线，希望能带领大家向更深层次迈进。 布道，其实是一件极具价值的事情——传递自己的观点，影响一群人，做成一件事。 今天，我的这份“长期主义”清单上，又将增添新的一项。我想借此机会，向一直支持我的朋友们，正式宣布一个喜讯。 官宣喜讯：历时一年半，2.4w 人订阅的《Go语言第一课》成书！ 四年前，我在极客时间开设了专栏《Go语言第一课》。令我欣慰的是，这个专栏得到了广大 Gopher 的认可和喜爱。截至今日，它已经影响了超过 2.4 万名订阅者(截至2025.8)，在编程语言类专栏里取得了相当不错的成绩。 为了让这份经过市场检验的优质内容，能以一种更经典、更触手可及的方式，帮助更多人踏入 Go 语言的大门，我与人民邮电出版社异步图书合作，历时一年多的精心打磨，终于将它变成了纸质书 &#8212; 我的第二本“小黄书”： 我必须强调，这本书并非专栏的简单复制。在近一年多的时间里，我倾注了大量心血，进行了一次彻底的精修与增补： 内容与时俱进：全书内容与最新的 Go 1.24 版本 同步(注：交稿时的最新版本为Go 1.24)，确保知识的前沿性与准确性。 知识体系更完整：我特别补充和深化了专栏中因篇幅所限未能详尽展开的关键内容，如指针类型的深入探讨、测试的最佳实践、以及泛型的全面讲解，使其作为一本入门读物更加系统和完备。 全面精炼与优化：基于三年来数万读者的宝贵反馈，我对全书的结构、文字表述、示例代码和图示进行了地毯式的优化，力求为读者提供“保姆级”的丝滑阅读体验。 为了让大家更直观地感受这本书是如何从“道”到“术”，构建一个完整而系统的知识体系的，我在这里分享本书的核心目录结构： [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/28/go-primer-published">本文永久链接</a> &#8211; https://tonybai.com/2025/08/28/go-primer-published</p>
<p>大家好，我是Tony Bai。</p>
<p>前不久，在知乎上看到一个关于 Go 社区的帖子，其中一条评论让我感慨良多：</p>
<blockquote>
<p><strong>“GopherChina 都没了，国内还有几人坚持？Tony Bai好像还在更新”</strong></p>
</blockquote>
<p>短短一句话，道尽了社区的变迁与坚持的不易。这句来自读者的回答，让我内心欣慰，也让我有机会停下来，审视自己在这条路上走了多远，以及为什么还要继续走下去。</p>
<p>答案或许很简单，就是三个字：<strong>长期主义</strong>。</p>
<p>我的<a href="https://tonybai.com">个人博客</a> tonybai.com，从 2004 年断断续续更新至今，已经走过了二十个年头。而我在 Go 语言这条路上的“长期主义”，则始于 2011 年。那时，Go 尚处襁褓，在国内几乎无人问津。我凭借着一股直觉和热爱，一头扎了进去，成为了国内最早一批的 Go 语言探索者。</p>
<p>十余年来，这份坚持从未间断。从早期的博客分享，到后来出版的《<a href="https://mmbiz.qpic.cn/sz_mmbiz_png/cH6WzfQ94mZuLxtuibVj3icgr9KZoD1KpX4dDNRvgRMo7F5cYSBdXIgicaDMOcHhLjH3Mx8mwBwEC4hL1ich5ZqZgA/640?wx_fmt=png&amp;from=appmsg">Go语言精进之路</a>》；从 <a href="">GopherChina 大会的讲台</a>，到几乎每日更新的 <a href="https://gopherdaily.tonybai.com">GopherDaily</a>，我一直在尽我所能地为社区贡献。</p>
<p>这份坚持也延续到了今年。从年初开始，我在公众号上陆续推出了<strong>多个“<a href="https://mp.weixin.qq.com/mp/homepage?__biz=MzIyNzM0MDk0Mg==&amp;hid=1&amp;sn=1867f7de470e9aed0881960a77be2aa9">微专栏</a>”系列</strong>，深入探讨 Go 源码与实践的细节；与此同时，我的新课程<strong>《<a href="https://mp.weixin.qq.com/s/GWGWTfCRCsOJ_4Pk-pxpHA">Go语言进阶课</a>》</strong>也已在极客时间上线，希望能带领大家向更深层次迈进。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-4.png" alt="" /></p>
<p>布道，其实是一件极具价值的事情——<strong>传递自己的观点，影响一群人，做成一件事。</strong></p>
<p>今天，我的这份“长期主义”清单上，又将增添新的一项。我想借此机会，向一直支持我的朋友们，正式宣布一个喜讯。</p>
<h2>官宣喜讯：历时一年半，2.4w 人订阅的《Go语言第一课》成书！</h2>
<p>四年前，我在极客时间开设了专栏<strong>《Go语言第一课》</strong>。令我欣慰的是，这个专栏得到了广大 Gopher 的认可和喜爱。截至今日，它已经影响了超过 <strong>2.4 万名订阅者</strong>(截至2025.8)，在编程语言类专栏里取得了相当不错的成绩。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-5.png" alt="" /></p>
<p>为了让这份经过市场检验的优质内容，能以一种更经典、更触手可及的方式，帮助更多人踏入 Go 语言的大门，我与人民邮电出版社异步图书合作，历时一年多的精心打磨，终于将它变成了纸质书 &#8212; 我的第二本“小黄书”：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-6.jpg" alt="" /></p>
<p>我必须强调，这本书<strong>并非专栏的简单复制</strong>。在近一年多的时间里，我倾注了大量心血，进行了一次彻底的精修与增补：</p>
<ul>
<li><strong>内容与时俱进</strong>：全书内容与最新的 <a href="https://tonybai.com/2025/02/16/some-changes-in-go-1-24">Go 1.24 版本</a> 同步(注：交稿时的最新版本为Go 1.24)，确保知识的前沿性与准确性。</li>
<li><strong>知识体系更完整</strong>：我特别补充和深化了专栏中因篇幅所限未能详尽展开的关键内容，如<strong>指针类型的深入探讨、测试的最佳实践、以及泛型的全面讲解</strong>，使其作为一本入门读物更加系统和完备。</li>
<li><strong>全面精炼与优化</strong>：基于三年来数万读者的宝贵反馈，我对全书的结构、文字表述、示例代码和图示进行了地毯式的优化，力求为读者提供“保姆级”的丝滑阅读体验。</li>
</ul>
<p>为了让大家更直观地感受这本书是如何从“道”到“术”，构建一个完整而系统的知识体系的，我在这里分享本书的核心目录结构：</p>
<hr />
<p><strong>《Go语言第一课》核心目录概览</strong></p>
<ul>
<li>
<p><strong>第一部分：建立宏观认知 (打好地基)</strong></p>
<ul>
<li><strong>第1章 Go的那些事儿</strong> (追本溯源，深入理解Go的诞生背景、演进历史与核心设计哲学：简单、显式、组合、并发、面向工程)</li>
</ul>
</li>
<li>
<p><strong>第二部分：基础与工程化 (工欲善其事)</strong></p>
<ul>
<li><strong>第2章</strong> 建立Go开发环境</li>
<li><strong>第3章</strong> 第一个Go程序</li>
<li><strong>第4章</strong> Go包、模块与代码组织结构</li>
<li><strong>第5章</strong> Go的依赖管理 (从演化到Go module实战)</li>
</ul>
</li>
<li>
<p><strong>第三部分：核心语法精讲 (深入肌理)</strong></p>
<ul>
<li><strong>第6章</strong> 变量与类型</li>
<li><strong>第7章</strong> 基本数据类型</li>
<li><strong>第8章</strong> 常量 (深入理解无类型常量等创新)</li>
<li><strong>第9章</strong> 复合数据类型 (数组、切片、map、结构体)</li>
<li><strong>第10章 指针类型</strong> (新增与深化章节，彻底搞懂Go指针)</li>
<li><strong>第11章</strong> 控制结构</li>
</ul>
</li>
<li>
<p><strong>第四部分：Go编程思想与范式 (提升境界)</strong></p>
<ul>
<li><strong>第12章</strong> 函数 (一等公民、defer的妙用与代价)</li>
<li><strong>第13章</strong> 错误处理 (Go独特的错误处理哲学与实践)</li>
<li><strong>第14章</strong> 方法 (深入理解Receiver的选择原则)</li>
<li><strong>第15章</strong> 接口类型 (小接口、组合思想与底层实现)</li>
</ul>
</li>
<li>
<p><strong>第五部分：Go核心竞争力 (决胜未来)</strong></p>
<ul>
<li><strong>第16章 并发编程</strong> (Goroutine、Channel与CSP并发模型)</li>
<li><strong>第17章 泛型</strong> (与Go 1.24同步，从设计演化到语法实践)</li>
<li><strong>第18章 测试</strong> (表驱动测试、示例测试、性能基准测试等最佳实践)</li>
</ul>
</li>
</ul>
<hr />
<p>从这份目录中大家可以看到，本书的路径设计清晰：<strong>从建立对 Go 的整体认知和哲学认同开始，到掌握扎实的工程基础，再到深入语言的核心语法与编程范式，最终聚焦于并发、泛型和测试这三大核心竞争力。</strong> 这是一条为初学者量身打造的、平滑而陡峭的学习曲线，旨在帮助你不仅学会 Go，更能学好 Go。</p>
<p>当然这份精益求精的背后，离不开<strong>人民邮电出版社异步图书编辑老师们</strong>的辛勤付出。在长达一年的审校过程中，他们以极高的专业素养和一丝不苟的态度，对书稿的每一处细节进行推敲和打磨。从章节结构的优化，到遣词造句的斟酌，再到每一个标点符号的校对，都倾注了大量心血。</p>
<p>下面这张布满批注的审稿截图，只是责任编辑秦健老师无数次打磨与推敲的一个缩影。正是因为有了这样认真负责的合作伙伴，这本书才能以更好的面貌呈现给大家。在此，向编辑老师们致以我最诚挚的谢意！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-3.png" alt="" /></p>
<p>简单来说，这本书凝结了我十余年的 Go 语言实战经验和布道心血，旨在为所有初学者提供一条清晰、高效的 Go 语言入门路径，不仅能快速上手，更能从一开始就建立起扎实的工程思维，为后续的进阶和实战打下坚实的基础。</p>
<h2>灵魂拷问：AI 时代，我们为什么还需要一本入门书？</h2>
<p>官宣完毕，我想和你探讨一个更深层次的问题。</p>
<p>在 ChatGPT、Claude、Gemini、DeepSeek、Copilot 等 AI 工具已经能“秒答”任何技术问题的今天，我们为什么还需要静下心来，系统地去阅读一本厚重的、入门级的纸质书？</p>
<p>这是一个极其现实的挑战。作为一名同样深度使用 AI 的工程师，我的答案是：<strong>越是在这个时代，我们越需要一本好的入门书。</strong></p>
<h3>1. AI 提供“答案”，书籍构建“体系”</h3>
<p>AI 的强大之处，在于它能针对你提出的具体问题，迅速给出一个看起来可行的“答案”（代码片段）。它能高效地帮你解决“术”层面的问题。</p>
<p>但一本好的入门书，为你构建的是一张捕鱼的“<strong>网</strong>”——一个结构化、系统化的<strong>知识体系</strong>。它从语言的“前世今生”与设计哲学讲起，为你建立宏观认知；然后层层递进，系统讲解语法、并发、泛型等核心知识点。</p>
<p>没有体系的知识是脆弱的、零散的。你或许能用 AI 拼凑出一个能运行的程序，但在面对复杂、未知的问题时，你将因为缺乏坚实的知识框架而寸步难行。而这本书，正是为你打造这张网。</p>
<h3>2. 对抗“能力空心化”，修炼真正的“内功”</h3>
<p>我在之前的文章中反复提及一个概念——<a href="https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age">警惕 AI 带来的“能力空心化”</a>。过度依赖 AI，会<a href="https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age">让初级工程师陷入“知其然，而不知其所以然”的困境</a>。</p>
<p><a href="https://tonybai.com/2025/04/19/learn-go-in-ai-era">系统地学习一本入门书</a>，恰恰是修炼“内功”的最佳方式。它强迫你去理解每一行代码背后的<strong>设计哲学、核心原理、以及那些微妙的权衡取舍</strong>。</p>
<ul>
<li>为什么 Go 的错误处理是这样的？</li>
<li>interface{} 的底层实现是怎样的？</li>
<li>CSP 并发模型的核心思想是什么？</li>
</ul>
<p>这些问题的答案，无法通过简单的 Prompt 获得。它们需要你沉下心来，跟随作者的思路，一步一个脚印地去理解和内化。这个过程，正是在构建你作为一名工程师，那份不可被 AI 替代的核心竞争力。</p>
<h3>3. 纸质书，一种无可替代的沉浸式学习体验</h3>
<p>最后，让我们回归阅读本身。</p>
<p>在信息过载的今天，纸质书提供了一种稀缺的、<strong>主动的、专注的、沉浸式的学习体验</strong>。它能帮助我们暂时摆脱屏幕上无尽的通知和干扰，让大脑进入一种更深度的思考状态。你可以随时在书页上圈点、批注，与作者进行一场跨越时空的对话。这种物理的交互感和知识的“拥有感”，是任何数字媒介都无法比拟的。</p>
<h2>布道者的心声：传递观点，影响他人</h2>
<p>回首这十几年的 Go 之旅，我愈发觉得，布道本身就是一件极具价值的事情。它不仅仅是分享知识，更是<strong>传递一种观点，影响一群人，最终一起做成一件事情。</strong></p>
<p>我写博著书和开设专栏的初衷，也正是如此。我希望传递的，不仅仅是 Go 语言的“术”——那些语法和技巧；更是 Go 语言的“道”——那种<strong>“简单、显式、组合、并发、面向工程”</strong>的编程哲学与乐趣。</p>
<p>在此，我要特别感谢极客时间平台，感谢人民邮电出版社异步图书的专业与付出，但最想感谢的，是四年来那 2.4w+ 的专栏订阅者，以及所有在我的博客、公众号、社区中与我交流、给我反馈的每一位读者。是你们的支持，才让这份“长期主义”有了最坚实的意义。</p>
<h2>行动号召：即刻拥有你的《Go语言第一课》</h2>
<p>现在，这本凝结了无数心血的《Go语言第一课》纸质版，已正式上市！</p>
<p>在本书的定价阶段，我和出版社的编辑老师们有一个共同的坚持：希望能让更多的 Go 语言爱好者，能够以更低的门槛，轻松地获取这份系统化的知识。为此，<strong>我们将这本书的定价一再压缩，最终定在了 79.8 元</strong>。</p>
<p>而为了感谢大家一直以来的支持与耐心等待，我们特别为大家申请了首发专属福利。在活动期间，大家可以通过下方的专属链接，以<strong>【五折优惠】</strong>的价格——算下来仅需不到 40 元——将这本300多页的硬核知识带回家。</p>
<p>这可能是本书在未来很长一段时间内的<strong>最低价格</strong>，希望能让每一位真正热爱 Go 语言的朋友，都能无压力地拥有它。</p>
<p><strong>扫描下方二维码或点击<a href="https://item.jd.com/14515573.html">这里</a></strong>， 即享五折优惠，即刻开启你的Go语言高效学习之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" alt="" /></p>
<p><strong>请注意，此五折优惠二维码仅在新书首发冲量期间有效，机会难得，不要错过！</strong></p>
<p>为了更好地服务本书读者，我也为本书创建了专属的 GitHub 仓库，用于持续发布勘误信息和提供完整的配套示例代码。追求高质量，是我们共同的目标。</p>
<ul>
<li><strong>勘误与代码支持</strong>：https://github.com/bigwhite/goprimer</li>
</ul>
<p>期待在书的扉页里，与你相遇。</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/08/28/go-primer-published/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Go的“七宗罪”：一篇“Go依然不够好”如何引爆社区激辩？</title>
		<link>https://tonybai.com/2025/08/25/go-is-still-not-good/</link>
		<comments>https://tonybai.com/2025/08/25/go-is-still-not-good/#comments</comments>
		<pubDate>Mon, 25 Aug 2025 00:11:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[append]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[err]]></category>
		<category><![CDATA[func]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HackerNews]]></category>
		<category><![CDATA[HN]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[monorepo]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[RAII]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[scope]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[utf-8]]></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=5074</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/25/go-is-still-not-good 大家好，我是Tony Bai。 在技术圈，平静的湖面下往往暗流涌动。对于Go语言社区而言，这股潜藏已久的暗流，被近期的一篇名为《Go is still not good》的博文彻底引爆。作者Thomas Habets，一位自称拥有超过十年Go使用经验的资深开发者，在他的这篇文章中系统性地列举了他眼中Go语言的“七宗罪”。这篇文章迅速登上Hacker News热榜，吸引了超过700条评论，形成了一场规模空前的社区大辩论。 参与者中不乏Go的早期采纳者、贡献者和日常重度使用者。他们争论的焦点，早已超越了语法糖的优劣，直指Go语言最核心的设计哲学——那些曾被誉为“简单”和“务实”的基石，如今在一些开发者眼中，却成了束缚发展、埋下隐患的“原罪”。 在这篇文章中，我就和大家一起跟随这场激辩，逐一剖析这引发轩然大波的“七宗罪”，看看从中能得到哪些有益的启示。 第一宗罪：歧义之空——nil 的双重身份 这是Go语言中最著名的“陷阱”，也是原文作者打响的第一枪。一个持有nil指针的接口变量，其自身并不等于nil。 package main import "fmt" type Error interface { Error() string } type MyError struct{} func (e *MyError) Error() string { return "my error" } func GetError() *MyError { // 假设在某种条件下，我们返回一个 nil 的具体错误类型指针 return nil } func main() { [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-still-not-good-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/25/go-is-still-not-good">本文永久链接</a> &#8211; https://tonybai.com/2025/08/25/go-is-still-not-good</p>
<p>大家好，我是Tony Bai。</p>
<p>在技术圈，平静的湖面下往往暗流涌动。对于Go语言社区而言，这股潜藏已久的暗流，被近期的一篇名为《<a href="https://blog.habets.se/2025/07/Go-is-still-not-good.html">Go is still not good</a>》的博文彻底引爆。作者<a href="https://blog.habets.se/">Thomas Habets</a>，一位自称拥有超过十年Go使用经验的资深开发者，在他的这篇文章中系统性地列举了他眼中Go语言的“七宗罪”。这篇文章迅速<a href="https://news.ycombinator.com/item?id=44982491">登上Hacker News热榜，吸引了超过700条评论</a>，形成了一场规模空前的社区大辩论。</p>
<p>参与者中不乏Go的早期采纳者、贡献者和日常重度使用者。他们争论的焦点，早已超越了语法糖的优劣，直指Go语言最核心的设计哲学——那些曾被誉为“简单”和“务实”的基石，如今在一些开发者眼中，却成了束缚发展、埋下隐患的“原罪”。</p>
<p>在这篇文章中，我就和大家一起跟随这场激辩，逐一剖析这引发轩然大波的“七宗罪”，看看从中能得到哪些有益的启示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>第一宗罪：歧义之空——nil 的双重身份</h2>
<p>这是Go语言中最著名的“陷阱”，也是原文作者打响的第一枪。一个持有nil指针的接口变量，其自身并不等于nil。</p>
<pre><code class="go">package main

import "fmt"

type Error interface {
    Error() string
}

type MyError struct{}

func (e *MyError) Error() string { return "my error" }

func GetError() *MyError {
    // 假设在某种条件下，我们返回一个 nil 的具体错误类型指针
    return nil
}

func main() {
    var err Error = GetError()

    // 输出: false
    // 尽管接口 err 内部持有的值是 nil，但接口本身因为包含了类型信息 (*MyError)，所以它不为 nil。
    fmt.Println(err == nil) 

    if err != nil {
        // 这段代码会被执行，然后可能在后续操作中引发 panic
        fmt.Printf("An error occurred: %v (type: %T)\n", err, err)
        // err.Error() // 若MyError的Error方法有解引用操作，此处会panic
    }
}
</code></pre>
<p>我们知道：Go的接口（interface）在内部实现为一个包含两部分的“胖指针”（fat pointer）：一个指向类型元数据的指针和一个指向实际数据的指针。只有当这两个指针都为nil时，接口变量本身才被认为是nil。在上述例子中，err的内部状态是(type=*MyError, value=nil)。因为类型信息存在，err != nil的判断为真，导致程序逻辑错误地进入了错误处理分支，挑战了开发者的常规直觉。</p>
<p><strong>社区激辩</strong>：</p>
<ul>
<li>
<p><strong>批评者阵营</strong>：Hacker News上，有用户提供了一个经典的Playground示例，展示了这个问题如何在生产环境中导致panic，并评论道：“这确实会在生产中咬你一口，而且在代码审查中极易被忽略。”另一位用户则更为尖锐，他引用了Rob Pike关于Go是为“非研究型、刚毕业的年轻工程师”设计的言论，反问道：“一个声称为了简化编程而设计的语言，却包含如此令人困惑的nil行为，这本身就是一种讽刺。”</p>
</li>
<li>
<p><strong>辩护者阵营</strong>：另一派观点认为，这并非缺陷，而是Go底层数据结构逻辑的直接体现。有开发者解释道：“接口值是一个包含类型和值的偶对。(&amp;Cat, nil)当然不等于(nil, nil)。”他们认为，一旦理解了接口的内存模型，这个问题便不再神秘，甚至可以利用这一特性（例如，在nil接收者上调用方法）。然而，这种辩护本身就强化了批评者的观点：一门标榜高级和简单的语言，却要求开发者对底层的实现细节有如此深刻的理解，这是否可以看作设计上的一种失败呢？</p>
</li>
</ul>
<h2>第二宗罪：作用域之惑——被迫扩展的err变量生命周期</h2>
<p>Go通过if err := foo(); err != nil语法，优雅地将err变量的作用域限制在if块内，这被广泛认为是最佳实践。然而，当函数调用需要返回除error之外的值时，这种优雅便荡然无存。</p>
<pre><code class="go">bar, err := foo()
if err != nil {
    return err
}
// 此处的err变量将在整个函数剩余部分都有效，即使它现在的值是nil

if err = foo2(); err != nil { // 复用err
    return err
}

// ... 大量代码 ...

return err
</code></pre>
<p>Go的短变量声明:=要求左侧至少有一个新变量。为了接收bar这个新值，err也被迫在函数作用域内被重新声明（或首次声明）。这导致err的生命周期被人为地拉长，污染了整个函数的作用域。</p>
<p><strong>社区激辩</strong>：</p>
<ul>
<li><strong>批评者阵营</strong>：原文作者尖锐地指出，这种设计“强迫你做错误的事情”。一个本应是局部的错误变量，现在却像个幽灵一样在整个函数中游荡，增加了代码阅读者的认知负担。读者必须时刻追踪err变量最后一次被赋值的位置，这极易导致bug，尤其是在重构或修改长函数时。</li>
<li><strong>辩护者阵营</strong>：对此的辩护声音较弱，大多认为这是个“可以忍受的小麻烦”。他们认为，这是为了保持语法一致性（:=的规则）而付出的代价。然而，这恰恰暴露了Go在追求一种形式上的“简单”时，牺牲了更重要的“上下文清晰性”。</li>
</ul>
<h2>第三宗罪：所有权之乱——append的隐式副作用</h2>
<p>slice是Go的基石之一，但其与底层数组（backing array）的模糊关系，通过append函数暴露无遗，构成了另一个经典的“搬起石头砸自己的脚”。</p>
<p>原文的例子一针见血地揭示了append行为的不可预测性：</p>
<pre><code class="go">package main

import "fmt"

func main() {
    // 案例一：当容量足够时，发生“幽灵写入”
    a := []string{"hello", "world", "!"}
    b := a[:1]                 // b与a共享底层数组，且cap(b) == 3
    b = append(b, "NIGHTMARE") // 修改了b，因为容量足够，直接修改了底层数组
    fmt.Println(a)// 结果：a变成了[hello NIGHTMARE !]

    // 案例二：当容量不足时，修改“失败”
    a = []string{"hello", "world", "!"}
    b = a[:1]
    b = append(b, "BACON", "THIS", "SHOULD", "WORK") // 容量不足，分配了新数组
    fmt.Println(a)// 结果：a依然是[hello world !]
}
</code></pre>
<p>我们知道：append的行为取决于slice的容量（cap）。如果追加后未超出容量，它会就地修改底层数组；否则，会分配一个新的、更大的数组。这种设计不仅让append的性能变得不确定，更严重的是，它破坏了函数调用的封装性，使得slice既不像值类型（可能被远程修改），也不像纯粹的引用类型（可能因重分配而断开联系）。</p>
<p><strong>社区激辩</strong>：</p>
<ul>
<li><strong>批评者阵营</strong>：Hacker News上一位获得高赞的评论是这样的：“append的例子是Go缺陷中最恶劣、最不可原谅的。”这种行为使得数据流变得难以追踪，迫使开发者必须时刻警惕slice的容量，或养成防御性编程的习惯，例如总是重新接收append的返回值。这与Go追求的“明确”背道而驰。</li>
<li><strong>辩护者阵营</strong>：支持者认为这是为了性能做出的合理权衡，避免了不必要的内存分配。他们强调，Go官方文档已明确说明了slice的工作原理。然而，这再次回到了那个核心问题：一门标榜“简单”的语言，是否应该包含如此微妙且需要深度理解才能安全使用的核心数据结构？</li>
</ul>
<h2>第四宗罪：作用域陷阱——函数级的defer</h2>
<p>defer是Go处理资源释放的利器，但它的作用域是整个函数，而非其所在的词法块（lexical scope）。这在循环中处理资源时会成为一个严重的资源泄漏问题。</p>
<pre><code class="go">for _, file := range files {
    f, err := os.Open(file)
    if err != nil { /* ... */ continue }
    // defer不会在每次循环结束时执行，而是堆积到函数返回时执行
    // 如果文件列表很长，将耗尽文件句柄
    defer f.Close()
    // ... process file
}
</code></pre>
<p>根本原因在于defer语句的执行被推入一个与当前函数关联的栈中，在函数返回前统一执行。这简化了编译器的实现，并确保了panic时资源也能被释放。</p>
<p><strong>社区激辩</strong>：</p>
<ul>
<li><strong>批评者阵营</strong>：一个开发者的高赞评论代表了社区的普遍困惑：“我至今不明白defer为什么是函数作用域而非词法作用域。”这与C++的RAII或Java的try-with-resources相比，是一种设计上的倒退。公认的解决方法是使用匿名函数func(){&#8230;}()包裹循环体，但这无疑增加了代码的丑陋和复杂性。</li>
<li><strong>辩护者阵营</strong>：有用户指出，函数级作用域也有其便利之处，例如可以在if块中有条件地注册一个defer。但总体而言，社区普遍认为，默认应该是更安全、更符合直觉的词法作用域。</li>
</ul>
<h2>第五宗罪：异常之隐——被标准库“吞噬”的panic</h2>
<p>Go的哲学是：error用于可预见的错误，panic用于程序无法继续的灾难。然而，作者指出，标准库中的fmt.Print和net/http服务器等关键部分，会主动recover从panic中恢复，这破坏了panic的基本约定。</p>
<p>这意味着开发者必须编写“异常安全”的代码。你必须假设任何传递给标准库的代码都可能在panic后被恢复。因此，像互斥锁（mutex）这样的资源必须通过defer来确保释放，否则一旦发生被“吞噬”的panic，就会造成死锁。作者愤怒地指出：“所有希望都破灭了。你<strong>必须</strong>写异常安全的代码，但你又<strong>不应该</strong>使用异常。你只能承受异常带来的所有负面影响。”</p>
<p><strong>社区激辩</strong>：这一点在社区中几乎没有辩护的声音。这被视为一种设计上的不一致和“伪善”。语言在表层倡导一种错误处理哲学，却在底层库中悄悄破坏它，迫使开发者为这种矛盾买单。</p>
<h2>第六宗罪：编码之殇——对非UTF-8的“绥靖政策”</h2>
<p>Go的string类型本质是只读的[]byte，不强制其为合法的UTF-8。这在与操作系统交互（如处理文件名）时提供了灵活性，但也埋下了隐患。</p>
<p>作者控诉，这种“宽松”策略是数据丢失的根源。当工具不假思索地按UTF-8处理文件名时，遇到非UTF-8编码的文件名可能会跳过或处理失败，导致在备份、恢复等关键操作中“静默地”遗漏数据。</p>
<p><strong>社区激辩</strong>：</p>
<ul>
<li><strong>批评者阵营</strong>：他们认为类型系统应防止此类错误。有用户激烈地评论道：“Go让你很容易做那些看起来99.7%的时间都有效，但却是愚蠢、错误、不正确的事情……然后有一天，你的用户就因为一个非UTF-8文件名而永久丢失了数据。”</li>
<li><strong>辩护者阵营</strong>：另一方则认为Go的做法才是务实的。有用户指出，一个强制Unicode正确性的文件接口在真实世界中是有问题的。Rust的OsStr虽然严谨，但人体工程学极差。Go的方式虽然“混乱”，但在实践中更方便。这揭示了<strong>严谨性与便利性</strong>之间的深刻矛盾。</li>
</ul>
<h2>第七宗罪：承诺之虚——伪善的“简单”与被忽视的性能</h2>
<p>这并非单一技术点，而是对Go整体设计理念的综合批判。</p>
<ul>
<li><strong>简单性的代价是复杂性转移</strong>：许多评论者指出，Go语言层面的“简单”，是把复杂性推给开发者来承担。没有枚举、没有强大的泛型（即使1.18加入了，也限制颇多）、没有Result类型，导致开发者需要手写大量重复的样板代码和自定义数据结构。</li>
<li><strong>内存管理的“信任危机”</strong>：原文作者提到“RAM is cheap”是危险的思维。Hacker News上有开发者分享了其在内存敏感项目中被Go的非压缩GC和堆碎片化问题折磨的经历，他们甚至不得不重写部分标准库以避免内存分配。这与Go宣称的“高性能”和“无忧GC”形成了鲜明对比。</li>
</ul>
<h2>为何着一篇文章能掀起千层浪？</h2>
<p>这场激辩之所以如此激烈，是因为它触及了Go社区内部长期存在的深层张力：</p>
<ol>
<li><strong>“Google的Go” vs “世界的Go”</strong>：Go的许多设计源于解决Google内部特定问题的需求（C++编译慢、monorepo文化）。这种“出身”决定了它在某些方面与更广阔的编程世界存在脱节。早年<a href="https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/">对单调时钟的忽视</a>就是典型例子。</li>
<li><strong>简单主义 vs 现代语言特性</strong>：Go的创造者们带着一种“回归本源”的复古主义情怀，刻意回避了过去几十年编程语言理论的发展成果，如高级类型系统、代数数据类型等。这使得Go易于上手，但也让它在处理复杂逻辑时显得捉襟见肘，迫使开发者“用代码的冗余换取语言的简单”。</li>
<li><strong>显式 vs 便利</strong>：if err != nil是显式的，但它不便利。Result类型和?操作符是便利的，但它在某种程度上是隐式的。Go坚定地站在了“显式”这一边，但社区中渴望“便利”的声音从未停止。</li>
</ol>
<h2>小结</h2>
<p>将Go的这些“罪状”简单归结为“错误”也是片面的。它们是Go<strong>强硬的、自洽的设计哲学</strong>所带来的必然产物。</p>
<ul>
<li><strong>这是一门有“历史”的现代语言</strong>：Go的设计深受其创造者们在C、Unix、Plan 9上的经验影响。它继承了C的简洁，但也继承了其对底层细节的暴露。</li>
<li><strong>承认权衡，理解其生态位</strong>：Go在“开发效率”、“运行性能”和“语言简单性”之间做出了明确的取舍，在云原生、微服务领域找到了无与伦比的“甜蜜点”。</li>
<li><strong>缓慢的进化也是一种承诺</strong>：Go团队对语言的改变极为谨慎，以维护其著名的向后兼容性承诺。但它并非一成不变。泛型的加入、for range循环变量作用域的修正，都表明Go在倾听社区的声音。</li>
</ul>
<p>《Go is still not good》及其引发的激辩，为我们提供了一个宝贵的窗口，去重新审视这门既年轻又充满“历史感”的语言。它提醒我们，没有完美的语言，只有充满权衡的工具。</p>
<p>对于Go开发者而言，理解这“七宗罪”的来龙去脉，不仅能帮助我们写出更健壮、更地道的代码，更能让我们清晰地认识到Go的优势与边界。与其无休止地争论它是否“足够好”，不如深入思考：<strong>它是否是解决我们当前问题的正确工具？</strong> 而这，或许才是这场大辩论给予我们的最大启示。</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/08/25/go-is-still-not-good/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go考古：创始人亲述Go语言的“创世纪”</title>
		<link>https://tonybai.com/2025/07/03/meet-the-go-team-2012/</link>
		<comments>https://tonybai.com/2025/07/03/meet-the-go-team-2012/#comments</comments>
		<pubDate>Thu, 03 Jul 2025 04:06:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[chubby]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GoogleIO]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[netchan]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Slice]]></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=4866</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/07/03/meet-the-go-team-2012 大家好，我是Tony Bai。 2012 年，Google I/O 大会的舞台上，一个刚刚发布 1.0 版本的编程语言团队，正襟危坐。他们面对着全球开发者的审视和提问，这其中，就有三位图灵奖得主级别的传奇人物：Ken Thompson、Rob Pike 和 Robert Griesemer。 那一年，Go 1.0 的发布，是一个历史性的里程碑。它意味着一个承诺“向后兼容、稳定可靠”的 Go 语言，正式诞生。 今天，就让我们扮演一次“Go 语言考古学家”，拂去时间的尘埃，回到那个被称为“创世纪”的时刻，重温 Go Team 核心成员们的亲口讲述，探寻这门语言最纯粹的初心和设计哲学。 我们为何创造 Go？—— “厌倦了等待 C++ 编译” 在访谈中，当被问及创造 Go 的初衷时，Rob Pike 给出了一个近乎“玩笑”却又无比真实的答案： “我们厌倦了等待 C++ 的编译。” 他生动地描绘了当时在 Google 内部的日常：为了构建一个巨大的 C++ 二进制文件，团队成员不得不在庞大的计算集群上等待超过一个小时。 更令人抓狂的是失控的依赖管理。Rob Pike 提到，他的同事 Mike Burrows（Chubby 的作者）在一次漫长的编译中发现，一个他从未听说过的、与项目毫无关系的头文件，竟然被重复编译了 37,000 次！ “当你用 ifdef 宏来保护依赖时，你最终得到的就是一个极其稠密的、做了太多无用功的依赖之巢。” [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/meet-the-go-team-2012-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/07/03/meet-the-go-team-2012">本文永久链接</a> &#8211; https://tonybai.com/2025/07/03/meet-the-go-team-2012</p>
<p>大家好，我是Tony Bai。</p>
<p>2012 年，Google I/O 大会的舞台上，一个刚刚发布 1.0 版本的编程语言团队，正襟危坐。他们面对着全球开发者的审视和提问，这其中，就有三位图灵奖得主级别的传奇人物：Ken Thompson、Rob Pike 和 Robert Griesemer。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/meet-the-go-team-2012-2.png" alt="" /></p>
<p>那一年，Go 1.0 的发布，是一个历史性的里程碑。它意味着一个承诺“向后兼容、稳定可靠”的 Go 语言，正式诞生。</p>
<p>今天，就让我们扮演一次“Go 语言考古学家”，拂去时间的尘埃，回到那个被称为“创世纪”的时刻，重温 Go Team 核心成员们的亲口讲述，探寻这门语言最纯粹的初心和设计哲学。</p>
<h2>我们为何创造 Go？—— “厌倦了等待 C++ 编译”</h2>
<p>在访谈中，当被问及创造 Go 的初衷时，Rob Pike 给出了一个近乎“玩笑”却又无比真实的答案：</p>
<blockquote>
<p><strong>“我们厌倦了等待 C++ 的编译。”</strong></p>
</blockquote>
<p>他生动地描绘了当时在 Google 内部的日常：为了构建一个巨大的 C++ 二进制文件，团队成员不得不在庞大的计算集群上等待超过一个小时。</p>
<p>更令人抓狂的是失控的依赖管理。Rob Pike 提到，他的同事 Mike Burrows（Chubby 的作者）在一次漫长的编译中发现，一个他从未听说过的、与项目毫无关系的头文件，竟然被重复编译了 <strong>37,000 次</strong>！</p>
<p>“当你用 ifdef 宏来保护依赖时，你最终得到的就是一个极其稠密的、做了太多无用功的依赖之巢。” Rob Pike 总结道。</p>
<p>这个巨大的痛点，催生了 Go 最核心的设计目标之一：<strong>从语言层面，彻底解决依赖问题。</strong></p>
<ul>
<li><strong>清晰的依赖图：</strong> Go 的导入路径直接明了。</li>
<li><strong>拒绝无用功：</strong> 编译器会拒绝未被使用的导入。</li>
<li><strong>高效的编译链：</strong> 设计上保证了“编译包 A 不应再重新编译包 C（如果 A->B->C）”。一旦包 B 被编译，它就携带了关于 C 的所有必要信息。</li>
</ul>
<p>而对于另一位创始人、C 语言和 Unix 的共同发明者 Ken Thompson 来说，促使他下定决心的“临门一脚”则更为直接和幽默。当被问及为何对 Go 如此热情时，他言简意赅：</p>
<blockquote>
<p><strong>“当我试图去读 C++0x（即 C++11）的标准草案时，我就下定决心了。”</strong></p>
</blockquote>
<p>全场爆笑。在一门日趋复杂的巨型语言面前，三位大师不约而同地选择了<strong>回归简单</strong>。</p>
<h2>Go 的“魔法”时刻 —— 那些改变编程方式的设计</h2>
<p>Go 的简洁并非简陋。在这次访谈中，创始人们也分享了那些让他们自己都感到惊喜和自豪的“魔法”设计。</p>
<p><strong>Slices (切片)：Ken Thompson 的神来之笔</strong></p>
<p>Rob Pike 回忆道，团队曾为了“数组”到底该如何工作而苦恼了整整一年。他们既想要静态检查的固定长度数组，又渴望某种形式的可变长度数组。在无数次的挣扎后，有一天，Ken Thompson 带着 slice 的想法走进办公室。</p>
<p>“起初我们并不确定这是不是正确答案，” Rob Pike 说，“但一旦我们开始使用它，一切都变得显而易见。” 一个简单而优雅的设计，完美地解决了这个旷日持久的难题。</p>
<p><strong>Interfaces (接口)：Rob Pike 的挚爱</strong></p>
<p>对于 Rob Pike 而言，接口是他认为 Go 中最强大的特性。</p>
<blockquote>
<p><strong>“接口深刻地改变了我对软件开发的思考方式。一个程序由这些可以轻松‘粘合’在一起的东西组成，这种感觉太棒了。它改变了软件被构建的方式。”</strong></p>
</blockquote>
<p>Go 的接口是隐式实现的。这种非侵入式的设计，让组件之间的耦合度降至最低，极大地促进了代码的解耦和可组合性。</p>
<p><strong>Packages (包)：看似显然，实则艰难</strong></p>
<p>今天我们觉得理所当然的 Go 包机制——一个包可以由多个文件组成，包内全局变量可以任意顺序声明——在当时也是经过了无数次辩论才最终成型的。</p>
<p>“它看起来似乎是显而易见的，但要弄清楚这一点真的非常困难。” Rob Pike 感叹道。这种“松散”的包设计，极大地简化了代码组织和重构的难度。</p>
<h2>有所为，有所不为 —— Go 的设计权衡</h2>
<p>当被问及如何看待 D 语言等其他试图改进 C++ 的语言时，Robert Griesemer 阐述了 Go 截然不同的设计哲学：</p>
<blockquote>
<p><strong>“我的印象是，D 语言会像 C++ 一样不断成长。而在 Go 中，我们试图采取完全相反的方式：尽可能地移除东西，将其简化到骨架，只保留你构建一切所需的绝对最小值。”</strong></p>
</blockquote>
<p>他相信，如果这些小组件是正交且能良好协作的，最终得到的东西会比拥有大量相互掣肘的特性的语言更强大。</p>
<p>这种“少即是多”的哲学，体现在 Go 对许多“流行特性”的刻意“缺失”上。当被问及“最庆幸 Go 缺失了什么特性”时，团队成员提到了：</p>
<ul>
<li><strong>类型继承体系 (Type Hierarchy)</strong></li>
<li><strong>可选参数 (Optional Arguments)</strong></li>
<li><strong>列表推导式 (List Comprehensions)</strong></li>
<li><strong>三元运算符</strong></li>
</ul>
<p>Rob Pike 指出，在 Java 或 C++ 中，你通常从设计类型继承树开始。这项工作耗时耗力，一旦发现设计有误，回头修改的成本极高。Go 通过移除类型继承，让程序在演进过程中更易于调整和适应。</p>
<p>为了凸显 Go 的简洁与 C++ 的复杂之间的对比，Rob Pike 更是转述了当时未能到场的 Russ Cox 的一句玩笑话，它为 Go 的哲学做了最好的注脚：<br />
“C++ 的风格指南里条条框框，而 Go 的风格指南第一句或许应该是：你可以使用这门语言的全部。”</p>
<h2>回望 2012 的“预言” —— 那些已实现和仍在路上的事</h2>
<p>考古的乐趣，在于用今天的视角去审视昨天的预言。在 2012 年，Go Team 对未来的展望，如今看来既有惊人的远见，也留下了些许历史的印记。</p>
<ul>
<li><strong>对 Go 1.1 的精准预言：</strong> 他们当时预测 1.1 版本将专注于性能提升、GC 改进、调度器优化和对更多操作系统的支持。这与后来 Go 1.x 系列的演进路径完全吻合。</li>
<li><strong>对 Go 2.0 的务实态度：</strong> 团队明确表示“Go 2 遥遥无期”，Go 2 的新想法将来自于使用 Go 1 中发现的真实需求。这个务实的态度至今仍在指导着 Go 的发展。</li>
<li><strong>最大的“失误”？</strong> 当被问及此，团队坦诚地提到了 nil 指针（Tony Hoare 的“十亿美元的错误”），以及循环变量的作用域问题。这些话题，至今仍在社区中被热烈讨论。</li>
<li><strong>未解的难题与渴望：</strong> Rob Pike 当时多次提到，他们非常想实现但还没找到完美方案的“网络化的 Channel (netchan)”，以及对一个真正的“抢占式调度器”的渴望。这些难题，在后来的岁月里，通过不同的方式被逐步探索和解决。</li>
</ul>
<h2>小结：回到源头，理解初心</h2>
<p>穿越时空，回到 Go 语言的“创世纪”现场，我们听到的不是高深莫测的理论，而是一群务实的工程师，为了解决自己在日常工作中遇到的真实、具体的痛点，而进行的一场充满智慧、权衡与热情的创造。</p>
<p>他们对简洁的极致追求，对工程效率的深刻理解，以及对“少即是多”的坚定信念，共同塑造了今天我们所热爱的 Go 语言。</p>
<p>理解这段历史，就是理解 Go 的灵魂。</p>
<p>参考资料链接：https://www.youtube.com/watch?v=sln-gJaURzk</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/07/03/meet-the-go-team-2012/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道</title>
		<link>https://tonybai.com/2025/05/31/six-smells-in-go/</link>
		<comments>https://tonybai.com/2025/05/31/six-smells-in-go/#comments</comments>
		<pubDate>Sat, 31 May 2025 02:36:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[closure]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></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=4769</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/31/six-smells-in-go 大家好，我是Tony Bai。 在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。 今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。 坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？” 在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。 典型场景：先异步通知，后更新状态 想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。 package main import ( "fmt" "sync" "time" ) type Order struct { ID string Status string // "pending", "paid", "notified" } func updateOrderStatusInDB(order *Order, status string) { fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/six-smells-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/31/six-smells-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/31/six-smells-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。</p>
<p>今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？”</h2>
<p>在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。</p>
<p><strong>典型场景：先异步通知，后更新状态</strong></p>
<p>想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。</p>
<pre><code class="go">package main

import (
    "fmt"
    "sync"
    "time"
)

type Order struct {
    ID     string
    Status string // "pending", "paid", "notified"
}

func updateOrderStatusInDB(order *Order, status string) {
    fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status)
    order.Status = status // 模拟数据库更新
}

func asyncSendNotification(order *Order) {
    fmt.Printf("营销系统：收到订单 %s 通知，当前状态：%s。准备发送优惠券...\n", order.ID, order.Status)
    // 模拟耗时操作
    time.Sleep(50 * time.Millisecond)
    fmt.Printf("营销系统：订单 %s 优惠券已发送 (基于状态：%s)\n", order.ID, order.Status)
}

func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    // 坏味道：先启动异步通知，再更新数据库状态
    wg.Add(1)
    go func(o *Order) { // 注意这里传递了指针
        defer wg.Done()
        asyncSendNotification(o)
    }(order) // goroutine 捕获的是 order 指针

    // 模拟主流程的其他操作，或者数据库更新前的延时
    time.Sleep(500 * time.Millisecond) 

    updateOrderStatusInDB(order, "paid") // 更新数据库状态

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<p>该示例的可能输出：</p>
<pre><code>主流程：订单 123 支付成功，准备处理...
营销系统：收到订单 123 通知，当前状态：pending。准备发送优惠券...
营销系统：订单 123 优惠券已发送 (基于状态：pending)
数据库：订单 123 状态更新为 paid
主流程：订单 123 处理完毕，最终状态：paid
</code></pre>
<p>我们看到营销系统拿到的优惠券居然是基于“pending”状态。</p>
<p><strong>问题分析：</strong></p>
<p>在上面的代码中，asyncSendNotification goroutine 和 updateOrderStatusInDB 是并发执行的。由于 asyncSendNotification 启动在先，并且捕获的是 order 指针，它很可能在 updateOrderStatusInDB 将订单状态更新为 “paid” <strong>之前</strong> 就读取了 order.Status。这就导致营销系统基于一个过时的状态（”pending”）发送了通知或优惠券，引发业务逻辑错误。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>确保关键操作的同步性或顺序性：</strong> 对于有严格先后顺序要求的操作，不要轻易异步化。如果必须异步，确保依赖的操作完成后再执行。</li>
<li><strong>使用同步原语：</strong> 利用 sync.WaitGroup、channel 等确保操作的正确顺序。例如，可以先更新数据库，再启动异步通知。</li>
<li><strong>传递值而非指针（如果适用）：</strong> 如果异步操作仅需快照数据，考虑传递值的副本，而不是指针。但在很多场景下，我们确实需要操作同一个对象。</li>
<li><strong>在异步回调中重新获取最新状态：</strong> 如果异步回调依赖最新状态，应在回调函数内部重新从可靠数据源（如数据库）获取，而不是依赖启动时捕获的状态。</li>
</ol>
<p><strong>修正示例思路：</strong></p>
<pre><code class="go">// ... (Order, updateOrderStatusInDB, asyncSendNotification 定义不变) ...
func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    updateOrderStatusInDB(order, "paid") // 先更新数据库状态

    // 再启动异步通知
    wg.Add(1)
    go func(o Order) { // 传递结构体副本，或者在异步函数内部重新获取
        defer wg.Done()
        // 实际场景中，如果 asyncSendNotification 依赖的是更新后的状态，
        // 它应该有能力从某个地方（比如参数，或者内部重新查询）获取到 "paid" 这个状态。
        // 这里简化为直接使用传入时的状态，但强调其应为 "paid"。
        // 或者，更好的方式是 asyncSendNotification 接受一个 status 参数。
        clonedOrderForNotification := o // 假设我们传递的是更新后的状态的副本
        asyncSendNotification(&amp;clonedOrderForNotification)
    }(*order) // 传递 order 的副本，此时 order.Status 已经是 "paid"

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<h2>坏味道二：指针与闭包的“爱恨情仇”——“我以为它没变，结果它却跑了！”</h2>
<p>闭包是 Go 语言中一个强大的特性，它能够捕获其词法作用域内的变量。然而，当闭包捕获的是指针，并且这个指针指向的数据在 goroutine 启动后可能被外部修改，或者指针本身被重新赋值时，就可能导致并发问题和难以预料的行为。虽然 Go 1.22+ 通过实验性的 GOEXPERIMENT=loopvar 改变了 for 循环变量的捕获语义，解决了经典的循环变量闭包陷阱，但指针与闭包结合时对共享可变状态的考量依然重要。</p>
<p><strong>典型场景：闭包捕获指针，外部修改指针或其指向内容</strong></p>
<p>我们来看一个不涉及循环变量，但同样能体现指针与闭包问题的场景：</p>
<pre><code class="go">package main

import (
    "fmt"
    "sync"
    "time"
)

type Config struct {
    Version string
    Timeout time.Duration
}

func watchConfig(cfg *Config, wg *sync.WaitGroup) {
    defer wg.Done()
    // 这个 goroutine 期望在其生命周期内使用 cfg 指向的配置
    // 但如果外部在它执行期间修改了 cfg 指向的内容，或者 cfg 本身被重新赋值，
    // 那么这个 goroutine 看到的内容就可能不是启动时的那个了。
    fmt.Printf("Watcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
    time.Sleep(100 * time.Millisecond) // 模拟监控工作
    fmt.Printf("Watcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
}

func main() {
    currentConfig := &amp;Config{Version: "v1.0", Timeout: 5 * time.Second}
    var wg sync.WaitGroup

    fmt.Printf("主流程：初始配置 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 启动一个 watcher goroutine，它捕获了 currentConfig 指针
    wg.Add(1)
    go watchConfig(currentConfig, &amp;wg) // currentConfig 指针被传递

    // 主流程在 watcher goroutine 执行期间，修改了 currentConfig 指向的内容
    time.Sleep(10 * time.Millisecond) // 确保 watcher goroutine 已经启动并打印了初始配置
    fmt.Println("主流程：检测到配置更新，准备在线修改...")
    currentConfig.Version = "v2.0" // 直接修改了指针指向的内存内容
    currentConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：配置已修改为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 或者更极端的情况，主流程让 currentConfig 指向了一个全新的 Config 对象
    // time.Sleep(10 * time.Millisecond)
    // fmt.Println("主流程：检测到配置需要完全替换...")
    // currentConfig = &amp;Config{Version: "v3.0", Timeout: 15 * time.Second} // currentConfig 指向了新的内存地址
    // fmt.Printf("主流程：配置已替换为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)
    // 注意：如果 currentConfig 被重新赋值指向新对象，原 watchConfig goroutine 仍然持有旧对象的指针。
    // 但如果原意是让 watchConfig 感知到“最新的配置”，那么这种方式是错误的。

    wg.Wait()
    fmt.Println("主流程：所有处理完毕。")

    fmt.Println("\n--- 更安全的做法：传递副本或不可变快照 ---")
    // 更安全的做法：如果 goroutine 需要的是启动时刻的配置快照
    stableConfig := &amp;Config{Version: "v1.0-stable", Timeout: 5 * time.Second}
    configSnapshot := *stableConfig // 创建一个副本

    wg.Add(1)
    go func(cfgSnapshot Config, wg *sync.WaitGroup) { // 传递的是 Config 值的副本
        defer wg.Done()
        fmt.Printf("SafeWatcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
        time.Sleep(100 * time.Millisecond)
        // 即使外部修改了 stableConfig，cfgSnapshot 依然是启动时的值
        fmt.Printf("SafeWatcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
    }(configSnapshot, &amp;wg)

    time.Sleep(10 * time.Millisecond)
    stableConfig.Version = "v2.0-stable" // 修改原始配置
    stableConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：stableConfig 已修改为 (Version: %s, Timeout: %v)\n", stableConfig.Version, stableConfig.Timeout)

    wg.Wait()
    fmt.Println("主流程：所有安全处理完毕。")
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>在第一个示例中，watchConfig goroutine 通过闭包（函数参数也是一种闭包形式）捕获了 currentConfig 指针。这意味着 watchConfig 内部对 cfg 的访问，实际上是访问 main goroutine 中 currentConfig 指针所指向的那块内存。</p>
<ul>
<li><strong>当外部修改指针指向的内容时：</strong> 如代码中 currentConfig.Version = “v2.0&#8243;，watchConfig goroutine 在后续访问 cfg.Version 时，会看到这个被修改后的新值，这可能不是它启动时期望的行为。</li>
<li><strong>当外部修改指针本身时 (注释掉的极端情况)：</strong> 如果 currentConfig = &amp;Config{Version: “v3.0&#8243;, &#8230;}，那么 watchConfig 捕获的 cfg 仍然指向<strong>原始的 Config 对象</strong>（即 “v1.0&#8243; 那个）。如果此时的业务逻辑期望 watchConfig 使用“最新的配置对象”，那么这种捕获指针的方式就会导致错误。</li>
</ul>
<p>这些问题的根源在于对<strong>共享可变状态</strong>的并发访问缺乏控制，以及对指针生命周期和闭包捕获机制的理解不够深入。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li>
<p><strong>明确 goroutine 需要的数据快照还是共享状态：</strong></p>
<ul>
<li>如果 goroutine 只需要启动时刻的数据快照，并且不希望受外部修改影响，那么应该<strong>传递值的副本</strong>给 goroutine（或者在闭包内部创建副本）。如第二个示例中的 configSnapshot。</li>
<li>如果 goroutine 需要与外部共享并感知状态变化，那么必须使用<strong>同步机制</strong>（如 mutex、channel、atomic 操作）来保护对共享状态的访问，确保数据一致性和避免竞态条件。</li>
</ul>
</li>
<li>
<p><strong>谨慎捕获指针，特别是那些可能在 goroutine 执行期间被修改的指针：</strong></p>
<ul>
<li>如果捕获了指针，要清楚地知道这个指针的生命周期，以及它指向的数据是否会被其他 goroutine 修改。</li>
<li>如果指针指向的数据是可变的，并且多个 goroutine 会并发读写，<strong>必须加锁保护</strong>。</li>
</ul>
</li>
<li>
<p><strong>考虑数据的不可变性：</strong> 如果可能，尽量使用不可变的数据结构。将不可变的数据传递给 goroutine 是最安全的并发方式之一。</p>
</li>
<li>
<p><strong>对于经典的 for 循环启动 goroutine 捕获循环变量的问题：</strong></p>
<ul>
<li><strong>Go 1.22+ (启用 GOEXPERIMENT=loopvar) 或未来版本：</strong> 语言层面已经解决了每次迭代共享同一个循环变量的问题，每次迭代会创建新的变量实例。此时，直接在闭包中捕获循环变量是安全的。</li>
<li><strong>Go 1.21 及更早版本 (或未启用 loopvar 实验特性)：</strong> 仍然需要通过<strong>函数参数传递</strong>的方式来确保每个 goroutine 捕获到正确的循环变量值。例如：</li>
</ul>
</li>
</ol>
<pre><code class="go">for i, v := range values {
    valCopy := v // 如果 v 是复杂类型，可能需要更深的拷贝
    indexCopy := i
    go func() {
        // 使用 valCopy 和 indexCopy
    }()
}
// 或者更推荐的方式：
for i, v := range values {
    go func(idx int, valType ValueType) { // ValueType 是 v 的类型
        // 使用 idx 和 valType
    }(i, v)
}
</code></pre>
<p>虽然 Go 语言在 for 循环变量捕获方面做出了改进，但指针与闭包结合时对共享状态和生命周期的审慎思考，仍然是编写健壮并发程序的关键。</p>
<h2>坏味道三：错误处理的哲学——“是Bug就让它崩！”真的好吗？</h2>
<p>Go 语言通过返回 error 值来处理可预期的错误，而 panic 则用于表示真正意外的、程序无法继续正常运行的严重错误，通常由运行时错误（如数组越界、空指针解引用）或显式调用 panic() 引发。当 panic 发生且未被 recover 时，程序会崩溃并打印堆栈信息。</p>
<p>一种常见的观点是：“如果是 Bug，就应该让它尽快崩溃 (Fail Fast)”，以便问题能被及时发现和修复。这种观点在很多情况下是合理的。然而，在某些 <strong>mission-critical（关键任务）系统</strong>中，例如金融交易系统、空中交通管制系统、重要的基础设施服务等，一次意外的宕机重启可能导致不可估量的损失或严重后果。在这些场景下，即使因为一个未捕获的 Bug 导致了 panic，我们也可能期望系统能有一定的“韧性”，而不是轻易“放弃治疗”。</p>
<p><strong>典型场景：一个关键服务在处理请求时因 Bug 发生 Panic</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "net/http"
    "runtime/debug"
    "time"
)

// 模拟一个关键数据处理器
type CriticalDataProcessor struct {
    // 假设有一些内部状态
    activeConnections int
    lastProcessedID   string
}

// 处理数据的方法，这里故意引入一个可能导致 panic 的 bug
func (p *CriticalDataProcessor) Process(dataID string, payload map[string]interface{}) error {
    fmt.Printf("Processor: 开始处理数据 %s\n", dataID)
    p.activeConnections++
    defer func() { p.activeConnections-- }() // 确保连接数正确管理

    // 模拟一些复杂逻辑
    time.Sleep(50 * time.Millisecond)

    // ！！！潜在的 Bug ！！！
    // 假设 payload 中 "user" 字段应该是一个结构体指针，但有时可能是 nil
    // 或者，某个深层嵌套的访问可能导致空指针解引用
    // 为了演示，我们简单模拟一个 nil map 访问导致的 panic
    var userDetails map[string]string
    // userDetails = payload["user"].(map[string]string) // 这本身也可能 panic 如果类型断言失败
    // 为了稳定复现 panic，我们直接让 userDetails 为 nil
    if dataID == "buggy-data-001" { // 特定条件下触发 bug
        fmt.Printf("Processor: 触发 Bug，尝试访问 nil map '%s'\n", userDetails["name"]) // 这里会 panic
    }

    p.lastProcessedID = dataID
    fmt.Printf("Processor: 数据 %s 处理成功\n", dataID)
    return nil
}

// HTTP Handler - 版本1: 不做任何 recover
func handleRequestVersion1(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }

        // 模拟从请求中获取 payload
        payload := make(map[string]interface{})
        // if dataID == "buggy-data-001" {
        //  // payload["user"] 可能是 nil 或错误类型，导致 Process 方法 panic
        // }

        err := processor.Process(dataID, payload) // 如果 Process 发生 panic，整个 HTTP server goroutine 会崩溃
        if err != nil {
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

// HTTP Handler - 版本2: 在每个请求处理的 goroutine 顶层 recover
func handleRequestVersion2(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!! PANIC 捕获 !!!!!!!!!!!!!!\n")
                fmt.Fprintf(os.Stderr, "错误: %v\n", err)
                fmt.Fprintf(os.Stderr, "堆栈信息:\n%s\n", debug.Stack())
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")

                // 向客户端返回一个通用的服务器错误
                http.Error(w, "服务器内部错误，请稍后重试", http.StatusInternalServerError)

                // 可以在这里记录更详细的错误到日志系统、发送告警等
                // 例如：log.Errorf("Panic recovered: %v, Stack: %s", err, debug.Stack())
                // metrics.Increment("panic_recovered_total")

                // 重要：根据系统的 mission-critical 程度和业务逻辑，
                // 这里可能还需要做一些清理工作，或者尝试让系统保持在一种“安全降级”的状态。
                // 但要注意，recover 后的状态可能是不确定的，需要非常谨慎。
            }
        }()

        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }
        payload := make(map[string]interface{})

        err := processor.Process(dataID, payload)
        if err != nil {
            // 正常错误处理
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

func main() {
    processor := &amp;CriticalDataProcessor{}

    // mux1 使用 Version1 handler (不 recover)
    // mux2 使用 Version2 handler (recover)

    // 启动 HTTP 服务器 (这里为了演示，只启动一个，实际中会选择一个)
    // 你可以注释掉一个，运行另一个来观察效果

    // http.HandleFunc("/v1/process", handleRequestVersion1(processor))
    // fmt.Println("V1 Server (不 recover) 启动在 :8080/v1/process")
    // go http.ListenAndServe(":8080", nil)

    http.DefaultServeMux.HandleFunc("/v2/process", handleRequestVersion2(processor))
    fmt.Println("V2 Server (recover) 启动在 :8081/v2/process")
    go http.ListenAndServe(":8081", nil)

    fmt.Println("\n请在浏览器或使用 curl 测试:")
    fmt.Println("  正常请求: curl 'http://localhost:8081/v2/process?id=normal-data-002'")
    fmt.Println("  触发Bug的请求: curl 'http://localhost:8081/v2/process?id=buggy-data-001'")
    fmt.Println("  (如果启动V1服务，触发Bug的请求会导致服务崩溃)")

    select {} // 阻塞 main goroutine，保持服务器运行
}
</code></pre>
<p><strong>问题分析：</strong></p>
<ul>
<li><strong>不 Recover (handleRequestVersion1)：</strong> 当 processor.Process 方法因为 Bug（例如访问 nil map userDetails["name"]）而发生 panic 时，如果这个 panic 没有在当前 goroutine 的调用栈中被 recover，它会一直向上传播。对于由 net/http 包为每个请求创建的 goroutine，如果 panic 未被处理，将导致该 goroutine 崩溃。在某些情况下（取决于 Go 版本和 HTTP server 实现的细节），这可能导致整个 HTTP 服务器进程终止，或者至少是该连接的处理异常中断，影响服务可用性。</li>
<li><strong>Recover (handleRequestVersion2)：</strong> 通过在每个请求处理的 goroutine 顶层使用 defer func() { recover() }()，我们可以捕获这个由 Bug 引发的 panic。捕获后，我们可以：
<ul>
<li>记录详细的错误信息和堆栈跟踪，便于事后分析和修复 Bug。</li>
<li>向当前请求的客户端返回一个通用的错误响应（例如 HTTP 500），而不是让连接直接断开或无响应。</li>
<li><strong>关键在于：</strong> 阻止了单个请求处理中的 Bug 导致的 panic 扩散到导致整个服务不可用的地步。服务本身仍然可以继续处理其他正常的请求。</li>
</ul>
</li>
</ul>
<p><strong>“是Bug就让它崩！”的观点在很多开发和测试环境中是值得提倡的，因为它能让我们更快地发现和定位问题。然而，在线上，特别是对于 mission-critical 系统：</strong></p>
<ul>
<li><strong>可用性是第一要务：</strong> 一次意外的全面宕机，可能比单个请求处理失败带来的损失大得多。</li>
<li><strong>数据一致性风险：</strong> 如果 panic 发生在关键数据操作的中间状态，直接崩溃可能导致数据不一致或损坏。recover 之后虽然也需要谨慎处理状态，但至少给了我们一个尝试回滚或记录问题的机会。</li>
<li><strong>用户体验：</strong> 对用户而言，遇到一个“服务器内部错误”然后重试，通常比整个服务长时间无法访问要好一些。</li>
</ul>
<p><strong>避坑与决策指南：</strong></p>
<ol>
<li><strong>在关键服务的请求处理入口或 goroutine 顶层设置 recover 机制：</strong> 这是构建健壮服务的推荐做法。
<ul>
<li>recover 应该与 defer 配合使用。</li>
<li>在 recover 逻辑中，务必记录详细的错误信息、堆栈跟踪，并考虑集成到告警系统。</li>
</ul>
</li>
<li><strong>recover 之后做什么？——视情况而定，但要极其谨慎：</strong>
<ul>
<li><strong>对于单个请求处理 goroutine：</strong> 通常的做法是记录错误，向当前客户端返回错误响应，然后让该 goroutine 正常结束。避免让这个 panic 影响其他请求。</li>
<li><strong>对于核心的、管理全局状态的 goroutine：</strong> 如果发生 panic，表明系统可能处于一种非常不稳定的状态。recover 后，可能需要执行一些清理操作，尝试将系统恢复到一个已知的安全状态，或者进行优雅关闭并重启。<strong>绝对不应该假装什么都没发生，继续使用可能已损坏的状态。</strong></li>
<li><strong>“苟活”的度：</strong> “苟活”不代表对 Bug 视而不见。recover 的目的是保障服务的整体可用性，同时为我们争取定位和修复 Bug 的时间。捕获到的 panic 必须被视为高优先级事件进行处理。</li>
</ul>
</li>
<li><strong>库代码应极度克制 panic：</strong> 库不应该替应用程序做“是否崩溃”的决策。</li>
<li><strong>测试，测试，再测试：</strong> 通过充分的单元测试、集成测试和压力测试，尽可能在上线前发现和消除潜在的 Bug，减少线上发生 panic 的概率。可以使用 Go 的 race detector 来检测并发代码中的竞态条件。</li>
<li><strong>不要滥用 panic/recover 作为正常的错误处理机制：</strong> panic/recover 主要用于处理不可预料的、灾难性的运行时错误或程序缺陷，而不是替代 error 返回值来处理业务逻辑中的预期错误。</li>
</ol>
<p>“是Bug就让它崩！”在开发阶段有助于快速发现问题，但在生产环境，特别是 mission-critical 系统中，<strong>“有控制地恢复，详细记录，并保障整体服务可用性”</strong> 往往是更明智的选择。这并不意味着容忍 Bug，而是采用一种更成熟、更负责任的方式来应对突发状况，确保系统在面对未知错误时仍能表现出足够的韧性。</p>
<h2>坏味道四：http.Client 的“一次性”误区——“每次都新建，省心又省事？”</h2>
<p>Go 标准库的 net/http 包提供了强大的 HTTP客户端功能。但有些开发者（尤其是初学者）在使用 http.Client 时，会为每一个 HTTP 请求都创建一个新的 http.Client 实例。</p>
<p><strong>典型场景：函数内部频繁创建 http.Client</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

// 坏味道：每次调用都创建一个新的 http.Client
func fetchDataFromAPI(url string) (string, error) {
    client := &amp;http.Client{ // 每次都新建 Client
        Timeout: 10 * time.Second,
    }
    resp, err := client.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

// 正确的方式：复用 http.Client
var sharedClient = &amp;http.Client{ // 全局或适当范围复用的 Client
    Timeout: 10 * time.Second,
    // 可以配置 Transport 以控制连接池等
    // Transport: &amp;http.Transport{
    //  MaxIdleConns:        100,
    //  MaxIdleConnsPerHost: 10,
    //  IdleConnTimeout:     90 * time.Second,
    // },
}

func fetchDataFromAPIReusable(url string) (string, error) {
    resp, err := sharedClient.Get(url) // 复用 Client
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

func main() {
    // 模拟多次调用
    // 如果使用 fetchDataFromAPI，每次都会创建新的 TCP 连接
    // _,_ = fetchDataFromAPI("https://www.example.com")
    // _,_ = fetchDataFromAPI("https://www.example.com")

    // 使用 fetchDataFromAPIReusable，会复用连接
    data, err := fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("获取到数据 (部分): %s...\n", data[:50])

    data, err = fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("再次获取到数据 (部分): %s...\n", data[:50])
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>http.Client 的零值或通过 &amp;http.Client{} 创建的实例，其内部的 Transport 字段（通常是 *http.Transport）会维护一个 TCP 连接池，并处理 HTTP keep-alive 等机制以复用连接。<strong>如果为每个请求都创建一个新的 http.Client，那么每次请求都会经历完整的 TCP 连接建立过程（三次握手），并在请求结束后关闭连接。</strong></p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>性能下降：</strong> 频繁的 TCP 连接建立和关闭开销巨大。</li>
<li><strong>资源消耗增加：</strong> 短时间内大量创建连接可能导致客户端耗尽可用端口，或者服务器端累积大量 TIME_WAIT 状态的连接，最终影响整个系统的吞吐量和稳定性。</li>
</ol>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>复用 http.Client 实例：</strong> 这是官方推荐的最佳实践。可以在全局范围创建一个 http.Client 实例（如 http.DefaultClient，或者一个自定义配置的实例），并在所有需要发起 HTTP 请求的地方复用它。</li>
<li><strong>http.Client 是并发安全的：</strong> 你可以放心地在多个 goroutine 中共享和使用同一个 http.Client 实例。</li>
<li><strong>自定义 Transport：</strong> 如果需要更细致地控制连接池大小、超时时间、TLS 配置等，可以创建一个自定义的 http.Transport 并将其赋给 http.Client 的 Transport 字段。</li>
</ol>
<h2>坏味道五：API 设计的“文档缺失”——“这参数啥意思？猜猜看！”</h2>
<p>良好的 API 设计是软件质量的基石，而清晰、准确的文档则是 API 可用性的关键。然而，在实际项目中，我们常常会遇到一些 API，其参数、返回值、错误码、甚至行为语义都缺乏明确的文档说明，导致用户（调用方）在集成时只能靠“猜”或者阅读源码，极易产生误用。</p>
<p><strong>典型场景：一个“凭感觉”调用的服务发现 API</strong></p>
<p>假设我们有一个类似 Nacos Naming 的服务发现客户端，其 GetInstance API 的文档非常简略，或者干脆没有文档，只暴露了函数签名：</p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// 假设这是 Nacos Naming 客户端的一个简化接口
type NamingClient interface {
    // GetInstance 获取服务实例。
    // 关键问题：
    // 1. serviceName 需要包含 namespace/group 信息吗？格式是什么？
    // 2. clusters 是可选的吗？如果提供多个，是随机选一个还是有特定策略？
    // 3. healthyOnly 如果为 true，是否会过滤掉不健康的实例？如果不健康实例是唯一选择呢？
    // 4. 返回的 instance 是什么结构？如果找不到实例，是返回 nil, error 还是空对象？
    // 5. error 可能有哪些类型？调用方需要如何区分处理？
    // 6. 这个调用是阻塞的吗？超时机制是怎样的？
    // 7. 是否有本地缓存机制？缓存刷新策略是？
    GetInstance(serviceName string, clusters []string, healthyOnly bool) (instance interface{}, err error)
}

// 一个非常简化的模拟实现 (坏味道的 API 设计，文档缺失)
type MockNamingClient struct{}

func (c *MockNamingClient) GetInstance(serviceName string, clusters []string, healthyOnly bool) (interface{}, error) {
    fmt.Printf("尝试获取服务: %s, 集群: %v, 只获取健康实例: %t\n", serviceName, clusters, healthyOnly)

    // 模拟一些内部逻辑和不确定性
    if serviceName == "" {
        return nil, errors.New("服务名不能为空 (错误码: Naming-1001)") // 文档里有这个错误码说明吗？
    }

    // 假设我们内部有一些实例数据
    instances := map[string][]string{
        "OrderService":   {"10.0.0.1:8080", "10.0.0.2:8080"},
        "PaymentService": {"10.0.1.1:9090"},
    }

    // 模拟集群选择逻辑 (文档缺失，用户只能猜)
    selectedCluster := ""
    if len(clusters) &gt; 0 {
        selectedCluster = clusters[rand.Intn(len(clusters))] // 随机选一个？
        fmt.Printf("选择了集群: %s\n", selectedCluster)
    }

    // 模拟健康检查和实例返回 (文档缺失)
    if healthyOnly &amp;&amp; rand.Float32() &lt; 0.3 { // 30% 概率找不到健康实例
        return nil, fmt.Errorf("在集群 %s 中未找到 %s 的健康实例 (错误码: Naming-2003)", selectedCluster, serviceName)
    }

    if insts, ok := instances[serviceName]; ok &amp;&amp; len(insts) &gt; 0 {
        return insts[rand.Intn(len(insts))], nil // 返回一个实例地址
    }

    return nil, fmt.Errorf("服务 %s 未找到 (错误码: Naming-4004)", serviceName)
}

func main() {
    client := &amp;MockNamingClient{}

    // 用户A的调用 (基于猜测)
    fmt.Println("用户A 调用:")
    instA, errA := client.GetInstance("OrderService", []string{"clusterA", "clusterB"}, true)
    if errA != nil {
        fmt.Printf("用户A 获取实例失败: %v\n", errA)
    } else {
        fmt.Printf("用户A 获取到实例: %v\n", instA)
    }

    fmt.Println("\n用户B 的调用 (换一种猜测):")
    // 用户B 可能不知道 serviceName 需要什么格式，或者 clusters 参数的意义
    instB, errB := client.GetInstance("com.example.PaymentService", nil, false) // serviceName 格式？clusters 为 nil 会怎样？
    if errB != nil {
        fmt.Printf("用户B 获取实例失败: %v\n", errB)
    } else {
        fmt.Printf("用户B 获取到实例: %v\n", instB)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>当 API 的设计者没有提供清晰、详尽的文档来说明每个参数的含义、取值范围、默认行为、边界条件、错误类型以及API的整体行为和副作用时，API 的使用者就只能依赖猜测、尝试，甚至阅读源码（如果开源的话）来理解如何正确调用。</p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>极易误用：</strong> 用户可能以 API 设计者未预期的方式调用接口，导致程序行为不符合预期，甚至引发错误。</li>
<li><strong>集成成本高：</strong> 理解和调试一个文档不清晰的 API 非常耗时。</li>
<li><strong>脆弱的依赖：</strong> 当 API 的内部实现或未明确定义的行为发生变化时，依赖这些隐性行为的调用方代码很可能会中断。</li>
<li><strong>难以排查问题：</strong> 出现问题时，很难判断是调用方使用不当，还是 API 本身的缺陷。</li>
</ol>
<p><strong>避坑指南 (针对 API 设计者)：</strong></p>
<ol>
<li><strong>编写清晰、准确、详尽的文档是 API 设计不可或缺的一部分！</strong> 这不仅仅是注释，可能还包括独立的 API 参考手册、用户指南和最佳实践。</li>
<li><strong>参数和返回值要有明确的语义：</strong> 名称应自解释，复杂类型应有结构和字段说明。
<ul>
<li>例如，serviceName 是否需要包含命名空间或分组信息？格式是什么？</li>
<li>clusters 参数是可选的吗？如果提供多个，选择策略是什么？是轮询、随机还是有特定优先级？</li>
<li>healthyOnly 的确切行为是什么？如果没有健康的实例，是返回错误还是有其他回退逻辑？</li>
</ul>
</li>
<li><strong>明确约定边界条件和错误情况：</strong>
<ul>
<li>哪些参数是必需的，哪些是可选的？可选参数的默认值是什么？</li>
<li>对于无效输入，API 会如何响应？返回哪些具体的错误码或错误信息？（例如，示例中的 Naming-1001, Naming-2003, Naming-4004 是否有统一的文档说明其含义和建议处理方式？）</li>
<li>API 调用可能产生的副作用是什么？</li>
</ul>
</li>
<li><strong>提供清晰的调用示例：</strong> 针对常见的用例，提供可运行的代码示例。</li>
<li><strong>考虑 API 的易用性和健壮性：</strong>
<ul>
<li>是否需要版本化？</li>
<li>是否需要幂等性保证？</li>
<li>认证和授权机制是否清晰？</li>
<li>超时和重试策略是怎样的？</li>
</ul>
</li>
<li><strong>将 API 的使用者视为首要客户：</strong> 站在使用者的角度思考，他们需要哪些信息才能轻松、正确地使用你的 API。</li>
</ol>
<p><strong>对于 API 的使用者：</strong> 当遇到文档不清晰的 API 时，除了“猜测”，更积极的做法是向 API 提供方寻求澄清，或者在有条件的情况下，参与到 API 文档的改进和完善中。</p>
<p>在之前《<a href="https://tonybai.com/2025/05/23/go-api-design-mcp-sdk/">API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考</a>》一文中，我们了见识了Go团队的API设计艺术，大家可以认知阅读和参考。</p>
<h2>坏味道六：匿名函数类型签名的“笨拙感”——“这函数参数看着眼花缭乱！”</h2>
<p>Go 语言的函数是一等公民，可以作为参数传递，也可以作为返回值。这为编写高阶函数和实现某些设计模式提供了极大的灵活性。然而，当匿名函数的类型签名（特别是嵌套或包含多个复杂函数类型参数时）直接写在函数定义中时，代码的可读性会大大降低，显得冗余和笨拙。</p>
<p><strong>典型场景：复杂的函数签名</strong></p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "strings"
)

// 坏味道：函数签名中直接嵌入复杂的匿名函数类型
func processData(
    data []string,
    filterFunc func(string) bool, // 参数1：一个过滤函数
    transformFunc func(string) (string, error), // 参数2：一个转换函数
    aggregatorFunc func([]string) string, // 参数3：一个聚合函数
) (string, error) {
    var filteredData []string
    for _, d := range data {
        if filterFunc(d) {
            transformed, err := transformFunc(d)
            if err != nil {
                // 注意：这里为了简化，直接返回了第一个遇到的错误
                // 实际应用中可能需要更复杂的错误处理逻辑，比如收集所有错误
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregatorFunc(filteredData), nil
}

// 使用 type 定义函数类型别名，代码更清晰
type StringFilter func(string) bool
type StringTransformer func(string) (string, error)
type StringAggregator func([]string) string

func processDataWithTypeAlias(
    data []string,
    filter StringFilter,
    transform StringTransformer,
    aggregate StringAggregator,
) (string, error) {
    // 函数体与 processData 相同
    var filteredData []string
    for _, d := range data {
        if filter(d) {
            transformed, err := transform(d)
            if err != nil {
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregate(filteredData), nil
}

func main() {
    sampleData := []string{"  apple  ", "Banana", "  CHERRY  ", "date"}

    // 使用原始的 processData，函数调用时也可能显得冗长
    result, err := processData(
        sampleData,
        func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 },
        func(s string) (string, error) {
            trimmed := strings.TrimSpace(s)
            if strings.ToLower(trimmed) == "banana" { // 假设banana是不允许的
                return "", errors.New("包含非法水果banana")
            }
            return strings.ToUpper(trimmed), nil
        },
        func(s []string) string { return strings.Join(s, ", ") },
    )

    if err != nil {
        fmt.Printf("处理错误 (原始方式): %v\n", err)
    } else {
        fmt.Printf("处理结果 (原始方式): %s\n", result)
    }

    // 使用 processDataWithTypeAlias，定义和调用都更清晰
    filter := func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 }
    transformer := func(s string) (string, error) {
        trimmed := strings.TrimSpace(s)
        if strings.ToLower(trimmed) == "banana" {
            return "", errors.New("包含非法水果banana")
        }
        return strings.ToUpper(trimmed), nil
    }
    aggregator := func(s []string) string { return strings.Join(s, ", ") }

    resultTyped, errTyped := processDataWithTypeAlias(sampleData, filter, transformer, aggregator)
    if errTyped != nil {
        fmt.Printf("处理错误 (类型别名方式): %v\n", errTyped)
    } else {
        fmt.Printf("处理结果 (类型别名方式): %s\n", resultTyped)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>Go 语言的类型系统是强类型且显式的。函数类型本身也是一种类型。当我们将一个函数类型（特别是具有多个参数和返回值的复杂函数类型）直接作为另一个函数的参数类型或返回值类型时，会导致函数签名变得非常长，难以阅读和理解。这与 Go 追求简洁和可读性的哲学在观感上有所冲突。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>使用 type 关键字定义函数类型别名：</strong> 这是解决此类问题的最推荐、最地道也是最常见的方法。通过为复杂的函数签名定义一个有意义的类型名称，可以极大地提高代码的可读性和可维护性。如示例中的 StringFilter, StringTransformer, StringAggregator。</li>
<li><strong>何时可以不使用类型别名：</strong>
<ul>
<li>当函数签名非常简单（例如 func() 或 func(int) int）且该函数类型只在局部、极少数地方使用时，直接写出可能问题不大。</li>
<li>但一旦函数签名变复杂，或者该函数类型需要在多个地方使用（作为不同函数的参数或返回值，或者作为结构体字段类型），就应该毫不犹豫地使用类型别名。</li>
</ul>
</li>
<li><strong>理解背后的设计考量：</strong> Go 语言强调类型的明确性。虽然直接写出函数类型显得“笨拙”，但也保证了类型信息在代码中的完全显露，避免了某些动态语言中因类型不明确可能导致的困惑。类型别名则是在这种明确性的基础上，提供了提升可读性的手段。</li>
</ol>
<p>为了更好地简化匿名函数，Go团队也提出了关于引入轻量级匿名函数语法的提案（Issue #21498），该提案一直是社区讨论的焦点，它旨在提供一种更简洁的方式来定义匿名函数，尤其是当函数类型可以从上下文推断时，从而减少样板代码，提升代码的可读性和编写效率。</p>
<h2>小结：于细微处见真章，持续打磨代码品质</h2>
<p>今天我们复盘的这六个 Go 编码“坏味道”——异步时序混乱、指针闭包陷阱、不当的错误处理、http.Client 误用、文档缺失的 API 以及冗长的函数签名——可能只是我们日常开发中遇到问题的冰山一角。</p>
<p>它们中的每一个，看似都是细节问题，但“千里之堤，溃于蚁穴”。正是这些细节的累积，最终决定了我们软件产品的质量、系统的稳定性和团队的开发效率。</p>
<p>识别并规避这些“坏味道”，需要我们：</p>
<ul>
<li><strong>深入理解 Go 语言的特性和设计哲学。</strong></li>
<li><strong>培养严谨的工程思维和对细节的关注。</strong></li>
<li><strong>重视代码审查，从他人的错误和经验中学习。</strong></li>
<li><strong>持续学习，不断反思和总结自己的编码实践。</strong></li>
</ul>
<p>希望今天的分享能给大家带来一些启发。让我们一起努力，写出更少“坑”、更高质量的 Go 代码！</p>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你日常的 Go 开发或 Code Review 中，还遇到过哪些让你印象深刻的“编码坏味道”？</strong></li>
<li><strong>对于今天提到的这些问题，你是否有自己独特的解决技巧或更深刻的理解？</strong></li>
<li><strong>你认为在团队中推广良好的编码规范和实践，最有效的方法是什么？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请<strong>转发给你身边的 Gopher 朋友们</strong>，让我们一起在 Go 的道路上精进！</p>
<p><strong>想与我进行更深入的 Go 语言、编码实践与 AI 技术交流吗？</strong> 欢迎加入我的<strong>“Go &amp; AI 精进营”知识星球</strong>。</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>我们星球见！</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/05/31/six-smells-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性</title>
		<link>https://tonybai.com/2025/05/30/gopher-girlfriend/</link>
		<comments>https://tonybai.com/2025/05/30/gopher-girlfriend/#comments</comments>
		<pubDate>Fri, 30 May 2025 11:33:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></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=4765</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend 大家好，我是Tony Bai。 最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。 那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。 “Gopher 女友”的可爱特质大揭秘！ 让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照： 1. “cute” (可爱) Meme 解读： 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。 Go语言真相： 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的语法简洁、核心概念少、没有过多的“语法糖”，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。 2. “low-maintenance” (低维护) Meme 解读： 她不“作”，好相处，不需要你花太多心思去“伺候”。 Go语言真相： 这简直是 Go 语言的真实写照！ gofmt 强制统一代码风格，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。 强大的工具链 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/30/gopher-girlfriend">本文永久链接</a> &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend</p>
<p>大家好，我是Tony Bai。</p>
<p>最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-2.jpg" alt="" /></p>
<p>那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。</p>
<h2>“Gopher 女友”的可爱特质大揭秘！</h2>
<p>让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照：</p>
<p><strong>1. “cute” (可爱)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。</li>
<li><strong>Go语言真相：</strong> 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的<strong>语法简洁、核心概念少、没有过多的“语法糖”</strong>，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。</li>
</ul>
<p><strong>2. “low-maintenance” (低维护)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她不“作”，好相处，不需要你花太多心思去“伺候”。</li>
<li><strong>Go语言真相：</strong> 这简直是 Go 语言的真实写照！
<ul>
<li><strong>gofmt 强制统一代码风格</strong>，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。</li>
<li><strong>强大的工具链</strong> (go build, go test, go mod 等) 让构建、测试、依赖管理变得异常简单。</li>
<li><strong>静态编译生成单个可执行文件</strong>，部署过程干净利落，没有复杂的运行时依赖和“DLL地狱”。</li>
<li><strong>内置垃圾回收 (GC)</strong> 机制，虽然不是“银弹”，但也极大地减轻了开发者的内存管理负担。</li>
</ul>
</li>
</ul>
<p>这些特性使得Go项目的<strong>维护成本相对较低</strong>，开发者可以将更多精力聚焦在业务逻辑上。</p>
<p><strong>3. “leaves you love letters in go.mod” (在 go.mod 里给你留情书)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 多么浪漫的表达！她把对你的“心意”（依赖）都清清楚楚地写在了 go.mod 这封“情书”里。</li>
<li><strong>Go语言真相：</strong> 自从 Go Modules 成为官方推荐的依赖管理方案后，go.mod 文件就成了每个 Go 项目的“标准配置”。它清晰、明确地记录了项目的模块路径、Go 版本以及所有直接和间接依赖及其版本号。这种<strong>依赖关系的透明化和可追溯性</strong>，就像一封真挚的“情书”，让你对项目的“家底”一目了然，极大地方便了依赖管理和构建复现。</li>
</ul>
<p><strong>4. “panics but quickly recovers” (会panic但能快速恢复)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她偶尔也会有小情绪（panic），但总能很快调整过来（recover），不至于让关系彻底崩溃。</li>
<li><strong>Go语言真相：</strong> Go 语言通过 panic 来表示严重的、通常是程序缺陷导致的运行时错误。但与其他一些语言遇到类似情况直接崩溃不同，Go 提供了 recover 机制。通过在 defer 函数中调用 recover()，我们可以捕获 panic，记录错误信息，执行一些清理操作，甚至尝试让程序从一个可控的状态恢复或优雅降级，而不是让整个服务“一蹶不振”。这种设计赋予了 Go 程序更强的<strong>韧性</strong>。</li>
</ul>
<p><strong>5. “shares her emotions by communicating” (通过沟通分享她的情感)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她乐于沟通，而不是让你猜她的心思。</li>
<li><strong>Go 语言真相：</strong> 这无疑是在致敬 Go 并发编程的核心原语——<strong>channel</strong>！Go 语言信奉“不要通过共享内存来通信，而要通过通信来共享内存” (Don&#8217;t communicate by sharing memory, share memory by communicating) 的并发哲学。Channel 正是 goroutine 之间进行<strong>数据传递和状态同步</strong>的主要桥梁，它使得并发逻辑的表达更加清晰和安全。</li>
</ul>
<p><strong>6. “thinks mutexes are romantic” (认为互斥锁是浪漫的)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 这个有点“硬核”的浪漫！她认为互斥锁 (mutex) 这种保护共享资源、确保“二人世界”不被打扰的机制，是充满“安全感”的浪漫。</li>
<li><strong>Go语言真相：</strong> sync.Mutex 是 Go 中最常用的并发同步原语之一，用于在并发访问共享资源时避免竞态条件。虽然 Go 推崇通过 channel 进行通信，但在某些场景下，使用互斥锁保护共享数据仍然是必要且高效的。这个梗幽默地反映了 Gopher 对<strong>并发安全</strong>的极致追求和对底层同步机制的熟悉。</li>
</ul>
<p><strong>7. “doesn&#8217;t cry when invalid memory address or nil pointer dereference” (当无效内存地址或空指针解引用时不会哭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 遇到问题，她不“哭哭啼啼”（难以追踪的错误），而是直接“告诉你”（panic）。</li>
<li><strong>Go 语言真相：</strong> 当 Go 程序遇到空指针解引用、数组越界等严重的运行时错误时，它会立即 panic，并打印出清晰的错误信息和堆栈跟踪。这与某些语言可能产生的段错误 (segmentation fault) 或未定义行为，导致问题难以定位和复现相比，无疑是一种更“直接”和有助于<strong>快速暴露和定位 Bug</strong> 的行为。</li>
</ul>
<p><strong>8. “thinks ORM is astrology for devs” (认为 ORM 对开发者来说是占星术)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她对那些过度封装、隐藏细节、让人感觉像“玄学”的 ORM 框架持保留态度。</li>
<li><strong>Go语言真相：</strong> 这是 Go 社区一个广为人知的“文化梗”。许多 Gopher 更倾向于使用标准库的 database/sql 包配合轻量级的 SQL 构建库（如 sqlx等），或者直接编写原生 SQL。这背后是对<strong>数据层掌控力、性能透明度以及避免不必要的“魔法”和复杂抽象</strong>的追求。他们认为，SQL 本身就是一种强大的 DSL，过度封装反而可能引入新的问题。</li>
</ul>
<p><strong>9. “cooks you meals from scratch” (从零开始为你做饭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她心灵手巧，能用最新鲜的食材（标准库）为你烹制美味佳肴，而不是依赖各种半成品（重型框架或过多第三方库）。</li>
<li><strong>Go 语言真相：</strong> Go 拥有一个异常<strong>强大且设计精良的标准库</strong>。无论是网络编程 (net/http, net)、JSON/XML 处理 (encoding/json, encoding/xml)、文件操作 (os, io)、加密解密 (crypto/*)，还是并发原语 (sync, sync/atomic)，标准库都提供了高质量的实现。这使得 Go 开发者在很多场景下可以“自给自足”，减少对外部依赖，构建出更轻量、更可控的系统。</li>
</ul>
<p><strong>10. “reviews your code every night” (每晚都审查你的代码)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她非常关心你的代码质量，时刻帮你把关。</li>
<li><strong>Go 语言真相：</strong> 这可以从几个层面理解：
<ul>
<li><strong>静态类型检查：</strong> Go 是一门静态类型语言，编译器在编译阶段就能帮你发现大量的类型错误和低级 Bug，就像一位尽职的“审查员”。</li>
<li><strong>go vet 等工具：</strong> Go 工具链内置了 go vet 等静态分析工具，可以帮助检查代码中潜在的错误或可疑构造。</li>
<li><strong>社区文化：</strong> Go 社区非常重视 Code Review 的实践，鼓励通过同行评审来提升代码质量。</li>
<li><strong>语言设计本身：</strong> Go 语言的简洁性和一些强制性规范（如未使用变量的编译错误），也在某种程度上“迫使”开发者写出更清晰、更规范的代码，更易于审查。</li>
</ul>
</li>
</ul>
<p><strong>11. “compiles fast” (编译快)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她做事麻利，从不拖沓。</li>
<li><strong>Go 语言真相：</strong> 这绝对是 Go 语言最令人称道的特性之一！Go 的<strong>编译速度极快</strong>，即使是中大型项目，编译过程通常也只需要十几秒钟。这极大地提升了开发者的工作效率和迭代速度，减少了漫长的等待时间，让开发体验如丝般顺滑。快速编译使得“编码-编译-测试”的循环非常高效。</li>
</ul>
<h2>小结：“Go语言女友”，为何如此理想？</h2>
<p>看完了对 “gopher gf” Meme 图的逐条解读，我们不难发现，这位“理想女友”的每一个“可爱特质”，都精准地映射了 Go 语言在现实世界中的核心优势：</p>
<ul>
<li><strong>简洁易学 (cute)</strong></li>
<li><strong>维护成本低 (low-maintenance)</strong></li>
<li><strong>依赖管理清晰 (leaves you love letters in go.mod)</strong></li>
<li><strong>具备韧性的错误处理 (panics but quickly recovers)</strong></li>
<li><strong>推崇通信共享内存的并发模型 (shares her emotions by communicating)</strong></li>
<li><strong>重视并发安全 (thinks mutexes are romantic)</strong></li>
<li><strong>明确的运行时错误反馈 (doesn&#8217;t cry when invalid memory address or nil pointer dereference)</strong></li>
<li><strong>崇尚直接、避免过度抽象 (thinks ORM is astrology for devs)</strong></li>
<li><strong>强大的标准库 (cooks you meals from scratch)</strong></li>
<li><strong>利于代码质量保障的特性与文化 (reviews your code every night)</strong></li>
<li><strong>闪电般的编译速度 (compiles fast)</strong></li>
</ul>
<p>正是这些特性，使得 Go 语言在云原生、微服务、分布式系统、网络编程、命令行工具等众多领域大放异彩，成为越来越多开发者和企业的首选。它就像一位可靠、高效、易于相处且不乏生活情趣的“伴侣”，帮助我们更轻松、更愉快地构建出色的软件系统。</p>
<p>当然，Meme 终归是 Meme，它用一种轻松幽默的方式，概括了 Go 语言的诸多美好。现实中的 Go 语言也并非完美无缺，它依然在不断发展和进化。但不可否认的是，这些“可爱”的特质，正是 Go 语言独特魅力和强大生命力的源泉。</p>
<p>那么，你心中的“Go 语言女友”又是怎样的呢？或者，你最欣赏 Go 语言的哪个“可爱”特质？</p>
<p>欢迎在<strong>评论区</strong>分享你的看法和脑洞！如果你觉得这篇文章有趣且让你对 Go 语言有了更深的（或者说更“萌”的）理解，也请<strong>转发给你身边的 Gopher 朋友们</strong>，一起感受这份来自代码世界的“浪漫”与“可爱”！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<blockquote>
<p>注：本文部分内容经过AI润色和优化，以提升读者阅读体验。</p>
</blockquote>
<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/05/30/gopher-girlfriend/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>AI会写Go代码了，初学者还需要系统学习吗？</title>
		<link>https://tonybai.com/2025/04/19/learn-go-in-ai-era/</link>
		<comments>https://tonybai.com/2025/04/19/learn-go-in-ai-era/#comments</comments>
		<pubDate>Sat, 19 Apr 2025 03:26:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AIGC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[map]]></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=4591</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/19/learn-go-in-ai-era 大家好！我是Tony Bai。 近来，AI领域的技术迭代速度惊人，尤其在代码生成能力上的显著提升，已有目共睹。现在，AI不仅能辅助编写Go代码片段，还能应对一些更复杂的逻辑结构，甚至还能完成一个完整工程的全部代码，这在开发者社区无疑引发了热烈讨论和对未来的思考。对于初学者来说，一个现实的问题摆在面前：我们还需要老老实实、一步一个脚印地去系统学习吗？比如像《Go语言第一课》专栏这样的系统课程还有必要去学吗？ 这个问题确实值得我们认真思考。AI的便捷是显而易见的，但它能帮助我们构建起真正的专业能力吗？ 如今，一个普遍的认知是，AI更像是一个能力的放大器，它能够增强你已有的知识和技能，但却很难从零开始为你构建坚实的基础。依赖于碎片化的AI交互，我们或许能快速“上手”，但要真正“掌握”某个领域，成为一名专业人士，可能还有很长的路要走。 那么，在AI时代，系统学习能为渴望成长的你，尤其是初学者，提供哪些不可替代的价值呢？ 我觉得至少有如下几点： 为你构建坚实的知识体系，告别“东拼西凑” AI能帮你生成代码片段，但这就像给你一堆散落的乐高积木。系统学习则教你如何将它们搭建成一个完整的城堡。 初学一门语言，最怕的就是知识点零散、不成体系。以Go学习为例，系统课程精心编排了学习路径，覆盖了环境搭建、基础语法、核心特性(接口、并发等)到简单实践。 它就像一张清晰的地图，引导你系统性地认识Go的世界，为你提供“一站式”的基础构建服务，避免在碎片信息中迷失方向，让起步更高效、更稳固。 助你拓展认知边界，进入学习“拉伸区” 还是以Go学习为例，系统的课程学习不仅仅是知道“怎么做”，也会适时地告诉你一些“为什么”。比如了解Go的设计哲学（简洁、显式、组合、面向并发等），理解某些特性（如接口设计）背后的考量。 这并非要求初学者一开始就深究底层，而是像《认知觉醒》一书里说的，帮助你适度地拓展“舒适区”，进入能获得更快提升的“拉伸区”。 理解了这些，你会发现自己应用起来更灵活，学习新知识也更快，整体学习效率自然更高。这是一种更高效的学习方式。 奠定“专业”基石，让你真正驾驭AI AI能写出代码，但判断代码的好坏、进行复杂调试、做出架构决策，这些都需要你具备扎实的专业基础。 系统学习正是帮你奠定这个基础，让你未来能有效地指导和评估AI写出的代码，而不是仅仅停留在简单地复制粘贴，甚至让AI生成一堆你自己都看不懂、无法评估好坏的代码，这样的代码一旦上生产可能带来潜在的风险和隐患。 只有足够专业，你才能有效地向AI提问，辨别AI答案的优劣，最终驾驭AI，让它成为你专业能力的延伸，而非被其能力所取代。碎片化的学习，是无法构建起这种专业壁垒的。而系统的课程学习，正是你迈向专业之路的第一块、也是至关重要的基石。 提供可靠、经过验证的学习起点 网络信息真假难辨，AI的回答也可能存在谬误。对于初学者来说，一开始接触到准确、可靠的信息至关重要。 一门好的课程，其内容是经过作者和编辑团队反复打磨、验证和审校的，源自大量实践和教学经验，确保了其准确性和权威性。它为你提供了一个可以信赖的学习起点和参照系，这本身就是一种重要的学习服务。 所以，回到最初的问题：AI会写Go代码了，初学者还需要系统学习吗？ 我的答案是：如果你不满足于“能用”，而是渴望真正“掌握”Go，渴望成为一名具备深度思考和解决复杂问题能力的Gopher，那么，系统学习依然是必经之路。 而《Go语言第一课》，这门我在极客时间打磨许久的专栏，正是基于上述理念设计的。它专注于为你铺设一条清晰、可靠、高效的Go入门之路，帮助你从“知道”走向“理解”，为未来成长为一名专业的Gopher奠定基础。 如果你正准备开始学习Go，或者希望巩固基础、构建体系，可以扫描下方二维码，订阅《Go语言第一课》专栏，为你的Go专业之路，打下第一个坚实桩基！，你也可以与其他订阅和学习该专栏的数万Gopher一起交流学习心得，共享学习成果。 最后补充一点信息： 为了让课程能更好地服务大家，最近我和极客时间的编辑老师一起，为《Go语言第一课》做了一次重要的课程迭代，增加了6篇“加餐”内容，涉及测试、性能、I/O、语言新特性等，希望能为你的学习之路提供一些额外的助力。当然，课程的核心价值，始终在于主体内容所构建的那个系统化的入门基础。 现在，轮到你了：作为Go学习者（特别是初学者），你如何看待AI对学习的影响？你认为系统学习最大的价值是什么？欢迎在评论区分享你的想法，我们一起探讨！如果觉得这篇文章说到了你的心坎里，请点个“在看**”支持一下吧！ 愿我们都能拥抱AI，但不忘构建自身的专业核心！ 原「Gopher部落」已重装升级为「Go &#38; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！ 我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得： 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI [...]]]></description>
			<content:encoded><![CDATA[<p><a href="https://tonybai.com/2025/04/19/learn-go-in-ai-era">本文永久链接</a> &#8211; https://tonybai.com/2025/04/19/learn-go-in-ai-era</p>
<p>大家好！我是Tony Bai。</p>
<p>近来，AI领域的技术迭代速度惊人，尤其在代码生成能力上的显著提升，已有目共睹。现在，AI不仅能辅助编写Go代码片段，还能应对一些更复杂的逻辑结构，甚至还能完成一个完整工程的全部代码，这在开发者社区无疑引发了热烈讨论和对未来的思考。对于初学者来说，一个现实的问题摆在面前：我们还需要老老实实、一步一个脚印地去系统学习吗？比如像《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》专栏这样的系统课程还有必要去学吗？</p>
<p>这个问题确实值得我们认真思考。AI的便捷是显而易见的，但它能帮助我们构建起真正的<strong>专业能力</strong>吗？</p>
<p>如今，一个普遍的认知是，AI更像是一个能力的放大器，它能够增强你已有的知识和技能，但却很难从零开始为你构建坚实的基础。依赖于碎片化的AI交互，我们或许能快速“上手”，但要真正“掌握”某个领域，成为一名<strong>专业人士</strong>，可能还有很长的路要走。</p>
<p><strong>那么，在AI时代，系统学习能为渴望成长的你，尤其是初学者，提供哪些不可替代的价值呢？</strong> 我觉得至少有如下几点：</p>
<ul>
<li><strong>为你构建坚实的知识体系，告别“东拼西凑”</strong></li>
</ul>
<p>AI能帮你<strong>生成代码片段</strong>，但这就像给你一堆散落的乐高积木。系统学习则教你如何将它们搭建成一个完整的城堡。</p>
<p>初学一门语言，最怕的就是知识点零散、不成体系。以Go学习为例，系统课程精心编排了学习路径，覆盖了环境搭建、基础语法、核心特性(接口、并发等)到简单实践。</p>
<p>它就像一张清晰的地图，引导你<strong>系统性地</strong>认识Go的世界，为你提供<strong>“一站式”的基础构建服务</strong>，避免在碎片信息中迷失方向，让起步更<strong>高效</strong>、更稳固。</p>
<ul>
<li><strong>助你拓展认知边界，进入学习“拉伸区”</strong></li>
</ul>
<p>还是以Go学习为例，系统的课程学习不仅仅是知道“怎么做”，也会适时地告诉你一些“为什么”。比如了解Go的设计哲学（简洁、显式、组合、面向并发等），理解某些特性（如接口设计）背后的考量。</p>
<p>这并非要求初学者一开始就深究底层，而是像《<a href="https://book.douban.com/subject/35193035/">认知觉醒</a>》一书里说的，帮助你<strong>适度地拓展“舒适区”，进入能获得更快提升的“拉伸区”</strong>。</p>
<p>理解了这些，你会发现自己应用起来更灵活，学习新知识也更快，整体<strong>学习效率自然更高</strong>。这是一种<strong>更高效的学习方式</strong>。</p>
<ul>
<li><strong>奠定“专业”基石，让你真正驾驭AI</strong></li>
</ul>
<p>AI能<strong>写出代码</strong>，但判断代码的好坏、进行复杂调试、做出架构决策，这些都需要你具备扎实的专业基础。</p>
<p>系统学习正是帮你奠定这个基础，让你未来能<strong>有效地指导和评估AI写出的代码</strong>，而不是仅仅停留在简单地复制粘贴，甚至让AI生成一堆你自己都看不懂、无法评估好坏的代码，这样的代码一旦上生产可能带来潜在的风险和隐患。</p>
<p>只有足够专业，你才能有效地向AI提问，辨别AI答案的优劣，最终驾驭AI，让它成为你专业能力的延伸，而非被其能力所取代。<strong>碎片化的学习，是无法构建起这种专业壁垒的。</strong>而系统的课程学习，正是你迈向专业之路的<strong>第一块、也是至关重要的基石</strong>。</p>
<ul>
<li><strong>提供可靠、经过验证的学习起点</strong></li>
</ul>
<p>网络信息真假难辨，AI的回答也可能存在谬误。对于初学者来说，一开始接触到准确、可靠的信息至关重要。</p>
<p>一门好的课程，其内容是经过作者和编辑团队<strong>反复打磨、验证和审校</strong>的，源自大量实践和教学经验，确保了其<strong>准确性和权威性</strong>。它为你提供了一个<strong>可以信赖的学习起点和参照系</strong>，这本身就是一种重要的<strong>学习服务</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/learn-go-in-ai-era-2.png" alt="" /></p>
<p><strong>所以，回到最初的问题：AI会写Go代码了，初学者还需要系统学习吗？</strong></p>
<p>我的答案是：<strong>如果你不满足于“能用”，而是渴望真正“掌握”Go，渴望成为一名具备深度思考和解决复杂问题能力的Gopher，那么，系统学习依然是必经之路。</strong></p>
<p>而<strong>《Go语言第一课》</strong>，这门我在极客时间打磨许久的专栏，正是基于上述理念设计的。它专注于为你铺设一条清晰、可靠、高效的Go入门之路，帮助你从“知道”走向“理解”，为未来成长为一名专业的Gopher奠定基础。</p>
<p><strong>如果你正准备开始学习Go，或者希望巩固基础、构建体系，可以扫描下方二维码，订阅《Go语言第一课》专栏，为你的Go专业之路，打下第一个坚实桩基！</strong>，你也<strong>可以与其他订阅和学习该专栏的数万Gopher一起交流学习心得，共享学习成果</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-primer-2025.jpg" alt="" /></p>
<p><strong>最后补充一点信息：</strong> 为了让课程能更好地服务大家，最近我和极客时间的编辑老师一起，为《Go语言第一课》做了一次重要的课程迭代，<strong>增加了6篇“加餐”内容</strong>，涉及测试、性能、I/O、语言新特性等，希望能为你的学习之路提供一些额外的助力。当然，课程的核心价值，始终在于主体内容所构建的那个系统化的入门基础。</p>
<p><strong>现在，轮到你了：作为Go学习者（特别是初学者），你如何看待AI对学习的影响？你认为系统学习最大的价值是什么？欢迎在</strong>评论区分享你的想法<strong>，我们一起探讨！如果觉得这篇文章说到了你的心坎里，请点个“</strong>在看**”支持一下吧！</p>
<p><strong>愿我们都能拥抱AI，但不忘构建自身的专业核心！</strong></p>
<hr />
<p><strong>原「Gopher部落」已重装升级为「Go &amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！</strong></p>
<p>我们致力于打造一个高品质的 <strong>Go 语言深度学习</strong> 与 <strong>AI 应用探索</strong> 平台。在这里，你将获得：</p>
<ul>
<li><strong>体系化 Go 核心进阶内容:</strong> 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。</li>
<li><strong>前沿 Go+AI 实战赋能:</strong> 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。</li>
<li><strong>星主 Tony Bai 亲自答疑:</strong> 遇到难题？星主第一时间为你深度解析，扫清学习障碍。</li>
<li><strong>高活跃 Gopher 交流圈:</strong> 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。</li>
<li><strong>独家资源与内容首发:</strong> 技术文章、课程更新、精选资源，第一时间触达。</li>
</ul>
<p>衷心希望「Go &amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" 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>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/19/learn-go-in-ai-era/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>“类型名称”在Go语言规范中的演变</title>
		<link>https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec/</link>
		<comments>https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec/#comments</comments>
		<pubDate>Tue, 24 Sep 2024 14:31:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alias]]></category>
		<category><![CDATA[aliastypeparams]]></category>
		<category><![CDATA[AnonymousType]]></category>
		<category><![CDATA[ANSI]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DefinedType]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gospec]]></category>
		<category><![CDATA[int]]></category>
		<category><![CDATA[iso]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[NamedType]]></category>
		<category><![CDATA[NominalType]]></category>
		<category><![CDATA[PredeclareType]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[StructuralType]]></category>
		<category><![CDATA[TypeName]]></category>
		<category><![CDATA[typeparameter]]></category>
		<category><![CDATA[TypeSystem]]></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=4296</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec Go语言规范（The Go Programming Language Specification）是Go语言的核心文档，定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准，它确保了语言的稳定性和一致性，特别是在类型系统设计中，Go团队通过规范推动了语言的简洁性、稳定性与可维护性。对于Go开发者而言，Go语言规范也是语法特性使用的参考手册(虽然语言规范读起来比较抽象和晦涩)。 Go语言规范由Google的Go核心开发团队维护和演进，这与ISO标准的C/C++语言规范有所不同。C和C++语言的ISO标准更新较慢，需经过复杂的全球共识和审核流程，而相比之下，Go语言的管理方式就显得更加灵活，也能够迅速适应新需求。 然而，这种灵活性也带来了潜在的弊端。随着新语法特性的引入和演进，一些已有的概念的含义可能会发生变化，导致前后的不一致性，从而让开发者感到困惑。例如，Go中的Type Name(类型名称)就经历了从最初的Named Type，到Defined Type和Alias Type，最终又回归到Named Type的过程。 近期Go语言之父之一的Robert Griesemer在Go官博发表了一篇名为”What&#8217;s in an (Alias) Name?“的文章，其中就对Go spec中Type Name的历史演进做了回顾，这里我们就基于这段回顾对“类型名称(Type Name)”在Go语言规范中的演变做一下简要梳理，希望能帮助大家更好的理解Go。 1. Go规范中的Type Name（类型名称） 在Go语言规范中，Type Name是指给定类型的标识符，它为一个类型提供了唯一的名称。Type Name用于识别和引用各种类型，这包括Go内置(也叫预声明Predeclared Type)的基础类型(比如int、string)和用户自定义的类型，比如： var x int // int是基础类型的Type Name type MyInt int // MyInt是用户定义类型的Type Name 你可能会问，Go还有没有类型名称的类型吗？当然有了，有一些特殊的类型没有直接的类型名称。通常，这些类型是匿名类型(Anonymous Type)，即它们并没有通过命名来标识，主要的匿名类型包括： 字面量定义的复合类型（Composite Literals） Go支持在代码中使用复合字面量来定义结构体、数组、切片、map等类型，而不为这些类型显式地定义名称。这些类型是在使用时定义的，并没有为其单独声明一个类型名称。 var data = struct { Name string; [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-evolution-of-type-name-in-go-spec-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec">本文永久链接</a> &#8211; https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec</p>
<p><a href="https://go.dev/ref/spec">Go语言规范（The Go Programming Language Specification）</a>是Go语言的核心文档，定义了该语言的语法、<a href="https://tonybai.com/2022/12/18/go-type-system">类型系统</a>和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准，它确保了语言的稳定性和一致性，特别是在类型系统设计中，Go团队通过规范推动了语言的简洁性、稳定性与可维护性。对于Go开发者而言，Go语言规范也是语法特性使用的参考手册(虽然语言规范读起来比较抽象和晦涩)。</p>
<p>Go语言规范由Google的Go核心开发团队维护和演进，这与<a href="https://isocpp.org/std/the-standard">ISO标准的C/C++语言规范</a>有所不同。C和C++语言的ISO标准更新较慢，需经过复杂的全球共识和审核流程，而相比之下，Go语言的管理方式就显得更加灵活，也能够迅速适应新需求。</p>
<p>然而，这种灵活性也带来了潜在的弊端。随着新语法特性的引入和演进，一些已有的概念的含义可能会发生变化，导致前后的不一致性，从而让开发者感到困惑。例如，Go中的Type Name(类型名称)就经历了从最初的Named Type，到<a href="https://tonybai.com/2021/12/02/go-has-implicit-type-convertion">Defined Type</a>和Alias Type，最终又回归到Named Type的过程。</p>
<p>近期Go语言之父之一的Robert Griesemer在Go官博发表了一篇名为”<a href="https://go.dev/blog/alias-names">What&#8217;s in an (Alias) Name?</a>“的文章，其中就对Go spec中Type Name的历史演进做了回顾，这里我们就基于这段回顾对“类型名称(Type Name)”在Go语言规范中的演变做一下简要梳理，希望能帮助大家更好的理解Go。</p>
<h2>1. Go规范中的Type Name（类型名称）</h2>
<p>在Go语言规范中，Type Name是指给定类型的标识符，它为一个类型提供了唯一的名称。Type Name用于识别和引用各种类型，这包括Go内置(也叫预声明Predeclared Type)的基础类型(比如int、string)和用户自定义的类型，比如：</p>
<pre><code>var x int       // int是基础类型的Type Name
type MyInt int  // MyInt是用户定义类型的Type Name
</code></pre>
<p>你可能会问，Go还有没有类型名称的类型吗？当然有了，有一些特殊的类型没有直接的类型名称。通常，这些类型是<strong>匿名类型(Anonymous Type)</strong>，即它们并没有通过命名来标识，主要的匿名类型包括：</p>
<ul>
<li>字面量定义的复合类型（Composite Literals）</li>
</ul>
<p>Go支持在代码中使用复合字面量来定义结构体、数组、切片、map等类型，而不为这些类型显式地定义名称。这些类型是在使用时定义的，并没有为其单独声明一个类型名称。</p>
<pre><code>var data = struct { Name string; Age int }{"Alice", 30}  // 匿名结构体类型
var arr = [5]int{1,2,3,4,5} // 匿名数组类型
var arr = []int{1, 2, 3}  // 匿名切片类型
var m = map[string]int{"foo": 1, "bar": 2}  // 匿名map类型
</code></pre>
<ul>
<li>匿名函数类型</li>
</ul>
<p>Go支持函数作为一等公民，函数本身可以作为类型，当定义匿名函数（即未命名函数）时，这些函数没有类型名称。</p>
<pre><code>var f = func() int { //匿名函数类型func() int
    return 42
}
</code></pre>
<p>Type Name是一个广泛的概念，在Go spec中，Go设计者们将其做了细分，比如Named Type、Defined Type等。那么随着Go版本的变化，Go中的Type Name的分类有哪些重要的演进和变化呢，下面我们就重点说明一下Go spec中Type Name分类的三次重要变化。</p>
<h2>2. 初始阶段：简单而明确的Named Type (2009-2017)</h2>
<p>Go 1.0是Go语言的首个正式发布版本，其中确立了类型名称的基础概念。在这一阶段，<a href="https://tonybai.com/2022/12/18/go-type-system">Go的类型系统</a>已经具备了高度的简洁性和一致性，这也是该语言设计的核心原则之一。</p>
<p>在Go语言的早期阶段(2009-2017)，Go规范就确定了简单明确的Named Type的概念，它指的是通过下面语法定义的类型T：</p>
<pre><code>type T existingType
</code></pre>
<p>这些通过类型声明定义的T被称为Named Type。而这里的existingType可以Predeclared的预声明类型（比如int、string），可以是已存在的Named Type，也可以是前面提到的匿名类型。</p>
<p>通过给现有类型赋予新名称来定义新的类型，与匿名类型等未命名类型形成鲜明对比。这种简单的分类满足了早期Go程序员的需求，为代码组织和类型系统提供了清晰的基础，提升了代码的可读性和模块化。</p>
<p>我们可以用示意图来展示这个阶段的Go类型名称分类：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-evolution-of-type-name-in-go-spec-2.png" alt="" /></p>
<p>而Named Type的定义方式也可以用下图表示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-evolution-of-type-name-in-go-spec-3.png" alt="" /></p>
<p>我们看到，可以基于Predeclare Type、匿名类型以及已存在的Named Type来定义一个新的Named Type。并且，Named Type具有一些专有特性，比如可拥有自己的方法、只与自身类型赋值兼容，不与其底层类型直接兼容（除非进行显式类型转换）等。</p>
<h2>3. 变革之始：别名类型的引入 (Go 1.9, 2017)</h2>
<p>然而，随着<a href="https://tonybai.com/2017/07/14/some-changes-in-go-1-9">Go 1.9</a>在2017年引入别名类型(Alias Type)，情况开始变得复杂：</p>
<pre><code>type T = Q // T为Q类型的别名类型
</code></pre>
<p>别名类型的引入是为了支持大规模代码库的重构，但它也模糊了Named Type的界限，因为别名也是一个类型名称。</p>
<p>为了应对这一变化，Go团队引入了”Defined Type”的概念以代替界限模糊的Named Type，用以特指通过类型定义(type T Q)创建的新类型。</p>
<p>这样改动后，整个Go类型系统的类型名称分类就变成如下示意图中的状态了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-evolution-of-type-name-in-go-spec-4.png" alt="" /></p>
<p>Defined Type定义和Alias Type的定义分别如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-evolution-of-type-name-in-go-spec-5.png" alt="" /></p>
<p>两者看起来差别不大，但只有Defined Type才拥有专有属性，比如可拥有自己的方法、只与自身类型赋值兼容等。我们也可以为Alias Type定义方法，但那个方法属于原类型。</p>
<h2>4. 泛型时代的到来：概念的重塑 (Go 1.18, 2022)</h2>
<p>2022年，<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18的发布</a>标志着Go语言进入了<a href="https://tonybai.com/2022/03/25/intro-generics">泛型时代</a>，这一重大特性的引入再次挑战了现有的类型分类方式。</p>
<p>比如类型参数也是类型，它们有名称，与Defined Type一样，两个不同命名的类型参数表示不同的类型。换句话说，类型参数是Named Type，而且它们的行为在某些方面与Go原始的Named Type类似。更重要的是，Go的Predeclare Type（如int、string等）只能通过它们的名称来访问，并且像Defined Type和类型参数一样，如果它们的名称不同，它们也会不同，这样预声明的类型也变成了Named Type。</p>
<p>为了适应泛型，Go规范重新引入了Named Type，并将其范围扩大到包括预声明类型、Defined Type、类型参数以及部分情况下的别名类型。</p>
<p>重新引入Named Type后，Defined Type依然得以保留，整个Go系统类型的最新类型名称分类状态如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-evolution-of-type-name-in-go-spec-6.png" alt="" /></p>
<h2>5. 当前的权衡</h2>
<p>在”<a href="https://go.dev/blog/alias-names">What&#8217;s in an (Alias) Name?</a>“的文章中，Robert还提到了学院派类型系统理论中的Nominal type（名义类型）和Structural type（结构类型)两个概念，虽然Go spec目前完全没有使用这两个概念。</p>
<p><a href="https://en.wikipedia.org/wiki/Nominal_type_system">Nominal type</a>，也叫名义类型。这种类型的身份（identity）明确地与其名称相关联。两个类型即使结构完全相同，如果名称不同，也被视为不同的类型。像Go 1.18以后spec中的预声明类型（如int、string等）、Defined types（通过type关键字定义的类型）和类型参数都属于这种类型，这大体与Named Type是重叠的。</p>
<p>而<a href="https://en.wikipedia.org/wiki/Structural_type_system">Structural type</a>（结构类型）的类型的身份仅取决于其结构或组成，而不依赖于名称。如果两个类型的结构相同，它们就被视为相同的类型，即使它们可能有不同的名称，像Go中的接口类型(在某种意义上)、通过类型字面量创建的类型（如匿名结构体、函数类型等）等都可以归属与这种类型。值得注意的是，指向类型字面量的别名类型（如type AliasName = struct{ &#8230; }）也可看作是structural type。</p>
<p>不过Robert也提到了，后续<strong>Go还会继续沿用Named Type、Defined Type等术语</strong>，而不会用这些学院派的类型术语来更新Go spec，这主要有几方面考虑：</p>
<ul>
<li>历史一致性：Go语言从早期就使用了named type、defined type等术语。突然改变可能会导致现有文档、教程和代码库的混乱。</li>
<li>概念特殊性：Go的类型系统有其特殊性，不完全符合传统的nominal/structural二分法。例如，Go的接口类型结合了nominal和structural的特性。这么做，也可以避免引起其他语言中该术语用法的混淆。</li>
<li>实用性考虑：”named type”、”defined type”等术语在Go的上下文中有明确的含义，直接对应于语言的特定特性和语法结构。这使得它们在讨论Go特定概念时更加实用。</li>
</ul>
<h2>6. 小结</h2>
<p>本文基于Robert的文章讲述了Go语言类型系统中的类型名称的演变历程。我们回顾了Type Name在Go语言规范中的重要变化，从最初的简单Named Type到后来的Defined Type和Alias Type，再到引入泛型时代后的重新定义Named Type。每一次变化不仅反映了Go语言的不断发展，也展示了Go团队在应对复杂性和保持语言简洁性之间的平衡。</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/24/the-evolution-of-type-name-in-go-spec/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程</title>
		<link>https://tonybai.com/2024/07/05/go-fundamentals-translation/</link>
		<comments>https://tonybai.com/2024/07/05/go-fundamentals-translation/#comments</comments>
		<pubDate>Thu, 04 Jul 2024 20:28:26 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GoFundamentals]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherGuides]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go语言编程指南]]></category>
		<category><![CDATA[map]]></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=4216</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/07/05/go-fundamentals-translation 嘿，各位Gopher们！猜猜发生了什么？我们团队翻译的《Go Fundamentals：Gopher Guides》中文版终于出版啦！没错，就是那本被我们亲切地称为《Go语言编程指南》的Go入门宝典。说实话，看到这本书终于摆在书架上，我的心情就像是刚刚用Go写出了一个超高效的并发程序一样兴奋！ 翻译这回事儿，我可不是新手 先让我跟大家聊聊我的“翻译前传”。其实这已经是我第二次和人民邮电出版社合作翻译技术书籍了。第一次是什么？就是那本豆瓣评分高达8.0的《七周七语言》！ 没错，就是这本让很多程序员大呼过瘾的语言大乱斗。那次我和其他几位未曾谋过面的译者一起，在杨海玲和李松峰编辑老师的指导下完成了翻译。说实话，那次经历给了我不少信心，也让我爱上了这种通过翻译学习新知识的感觉。 这次，我带了“一个团队”来翻译 这一次翻译《Go语言编程指南》，可就不一样了。我们睿驰车联网先行产品团队的一群Gopher集体出动！想想看，一群平时用Go撸产品的工程师聚在一起翻译Go的书，那场面，简直是Gopher的狂欢啊！ 我们的翻译天团是这样的： 我，白明：虽然是领队，但也得撸起袖子干活，包揽了第1~3章的翻译，还得负责全书的校对。累？不存在的！ 刘瑞强：专攻第4、8、11和13章，简直是四处开花。 于昊：包下了第7、10、12、14章，都是难啃的骨头儿！ 郭宇：负责第5、6、9章，还有本书其他的零零碎碎，简直是全能选手。 说真的，这次翻译不仅让我们的英语水平突飞猛进，我们对Go的理解也是噌噌往上涨。现在我们用Go开发的车联网中间件，已经在好几家主机厂的项目中大显身手了。这感觉，就像是用Go写出的程序一样：高效又实用！ 这本书到底讲了啥？ 哦，差点忘了介绍这本书的内容。《Go语言编程指南》可不是一本普通的入门书，它源自作者马克·贝茨和科瑞·拉诺的Gopher Guides系列Go培训教程。 这俩哥们教授该课程很多年，深受广大Gopher欢迎。 同时，这本书也是Go语言的全家桶！从最基础的语法到并发编程的高级主题，再到刚刚落地Go没多久的语言特性新贵“泛型”等，应有尽有。无论你是Go语言的新手，还是想进阶的老手，这本书都能让你有所收获。 再具体一点，书中包括了： Go的包和模块管理：不用再为GOPATH抓狂了！ 基础语法：变量、类型、控制流，麻雀虽小五脏俱全。 复合类型：数组、切片、map和函数，Go的精髓都在这儿了。 结构体和方法：面向对象？Go有自己的方式。 测试：写出好代码，测试必不可少。 接口和泛型：Go的泛型来了，你还不学吗？ 并发编程：goroutine、channel，Go的杀手锏！ 是不是很想一睹为快啊！赶紧去下单吧！ 翻译路上的酸甜苦辣 说实话，这一年半的翻译时光，还真是既有苦也有乐。有时候为了一个术语的翻译，我们能在群里讨论半天。但最后得出的结果，总是让人满意的。我们不仅仅是翻译，更像是在重新咀嚼消化这本书的内容。遇到原文表述不清的地方，我们还得集体讨论，然后用更清晰的方式表达出来。遇到原文中有误的地方，我们会用脚注标记处原作中的小瑕疵，并对原文予以纠正。 最让我感动的是，看到团队成员们在翻译过程中的成长。大家不仅英语水平提高了，对Go的理解也更深了。现在讨论起Go的特性，大家都能侃侃而谈，这感觉，真是太棒了！ 最后的碎碎念 说到最后，真的要感谢人民邮电出版社的杨绣国老师。杨老师的专业水平和敬业精神依旧让我佩服不已。没有她的策划、协调和帮助，这本书可能还在我们的电脑里躺着呢。 当然，我还要向书籍的原作者马克·贝茨和科瑞·拉诺致敬。哥们儿，你们写的书真是太棒了！ 最后，如果你正在看这篇文章，而且对Go感兴趣，不如去买本《Go语言编程指南》看看？相信我，你这钱“买不了吃亏，买不了上当，真正的物有所值”。也许有一天，你也会成为Go语言的高手，到时候别忘了回来在留言区告诉我哦！ 好了，我得去写Go代码了。记住，Stay hungry，stay foolish and keep Go-ing！ 最后最后，我再模式化的补一句：因团队能力有限，翻译可能存在不当之处，恳请读者批评指正。 注：如发现原文或译文中的问题，欢迎在这篇文章的评论中留言指出。 Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！ 著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。 Gopher Daily(Gopher每日新闻) &#8211; [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-fundamentals-translation-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/07/05/go-fundamentals-translation">本文永久链接</a> &#8211; https://tonybai.com/2024/07/05/go-fundamentals-translation</p>
<p>嘿，各位Gopher们！猜猜发生了什么？我们团队翻译的《<a href="https://www.gopherguides.com/golang-fundamentals-book">Go Fundamentals：Gopher Guides</a>》中文版终于出版啦！没错，就是那本被我们亲切地称为《<a href="https://book.douban.com/subject/36940180/">Go语言编程指南</a>》的Go入门宝典。说实话，看到这本书终于摆在书架上，我的心情就像是刚刚用Go写出了一个超高效的并发程序一样兴奋！</p>
<p><img src="https://tonybai.com/wp-content/uploads/gofundamentals.jpg" alt="" /></p>
<h2>翻译这回事儿，我可不是新手</h2>
<p>先让我跟大家聊聊我的“翻译前传”。其实这已经是我第二次和人民邮电出版社合作翻译技术书籍了。第一次是什么？就是那本豆瓣评分高达8.0的《<a href="https://book.douban.com/subject/10555435/">七周七语言</a>》！</p>
<p><img src="https://tonybai.com/wp-content/uploads/sevenlanguagesinsevenweeks.jpg" alt="" /></p>
<p>没错，就是这本让很多程序员大呼过瘾的语言大乱斗。那次我和其他几位未曾谋过面的译者一起，在杨海玲和李松峰编辑老师的指导下完成了翻译。说实话，那次经历给了我不少信心，也让我爱上了这种通过翻译学习新知识的感觉。</p>
<h2>这次，我带了“一个团队”来翻译</h2>
<p>这一次翻译《Go语言编程指南》，可就不一样了。我们睿驰车联网先行产品团队的一群Gopher集体出动！想想看，一群平时用Go撸产品的工程师聚在一起翻译Go的书，那场面，简直是Gopher的狂欢啊！</p>
<p>我们的翻译天团是这样的：</p>
<ul>
<li>我，白明：虽然是领队，但也得撸起袖子干活，包揽了第1~3章的翻译，还得负责全书的校对。累？不存在的！</li>
<li>刘瑞强：专攻第4、8、11和13章，简直是四处开花。</li>
<li>于昊：包下了第7、10、12、14章，都是难啃的骨头儿！</li>
<li>郭宇：负责第5、6、9章，还有本书其他的零零碎碎，简直是全能选手。</li>
</ul>
<p>说真的，这次翻译不仅让我们的英语水平突飞猛进，我们对Go的理解也是噌噌往上涨。现在我们用Go开发的车联网中间件，已经在好几家主机厂的项目中大显身手了。这感觉，就像是用Go写出的程序一样：高效又实用！</p>
<h2>这本书到底讲了啥？</h2>
<p>哦，差点忘了介绍这本书的内容。《Go语言编程指南》可不是一本普通的入门书，它源自作者马克·贝茨和科瑞·拉诺的<a href="https://www.gopherguides.com/">Gopher Guides系列Go培训教程</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-fundamentals-translation-2.png" alt="" /></p>
<p>这俩哥们教授该课程很多年，深受广大Gopher欢迎。 同时，这本书也是Go语言的全家桶！从最基础的语法到并发编程的高级主题，再到刚刚落地Go没多久的语言特性新贵“泛型”等，应有尽有。无论你是Go语言的新手，还是想进阶的老手，这本书都能让你有所收获。</p>
<p>再具体一点，书中包括了：</p>
<ul>
<li>Go的包和模块管理：不用再为GOPATH抓狂了！</li>
<li>基础语法：变量、类型、控制流，麻雀虽小五脏俱全。</li>
<li>复合类型：数组、切片、map和函数，Go的精髓都在这儿了。</li>
<li>结构体和方法：面向对象？Go有自己的方式。</li>
<li>测试：写出好代码，测试必不可少。</li>
<li>接口和泛型：Go的泛型来了，你还不学吗？</li>
<li>并发编程：goroutine、channel，Go的杀手锏！</li>
</ul>
<p>是不是很想一睹为快啊！赶紧<a href="https://item.jd.com/14669120.html">去下单</a>吧！</p>
<h2>翻译路上的酸甜苦辣</h2>
<p>说实话，这一年半的翻译时光，还真是既有苦也有乐。有时候为了一个术语的翻译，我们能在群里讨论半天。但最后得出的结果，总是让人满意的。我们不仅仅是翻译，更像是在重新咀嚼消化这本书的内容。遇到原文表述不清的地方，我们还得集体讨论，然后用更清晰的方式表达出来。遇到原文中有误的地方，我们会用脚注标记处原作中的小瑕疵，并对原文予以纠正。</p>
<p>最让我感动的是，看到团队成员们在翻译过程中的成长。大家不仅英语水平提高了，对Go的理解也更深了。现在讨论起Go的特性，大家都能侃侃而谈，这感觉，真是太棒了！</p>
<h2>最后的碎碎念</h2>
<p>说到最后，真的要感谢人民邮电出版社的杨绣国老师。杨老师的专业水平和敬业精神依旧让我佩服不已。没有她的策划、协调和帮助，这本书可能还在我们的电脑里躺着呢。</p>
<p>当然，我还要向书籍的原作者马克·贝茨和科瑞·拉诺致敬。哥们儿，你们写的书真是太棒了！</p>
<p>最后，如果你正在看这篇文章，而且对Go感兴趣，不如<a href="https://item.jd.com/14669120.html">去买本《Go语言编程指南》看看</a>？相信我，你这钱“买不了吃亏，买不了上当，真正的物有所值”。也许有一天，你也会成为Go语言的高手，到时候别忘了回来在留言区告诉我哦！</p>
<p>好了，我得去写Go代码了。记住，<strong>Stay hungry，stay foolish and keep Go-ing</strong>！</p>
<p>最后最后，我再模式化的补一句：<strong>因团队能力有限，翻译可能存在不当之处，恳请读者批评指正</strong>。</p>
<blockquote>
<p>注：如发现原文或译文中的问题，欢迎在这篇文章的评论中留言指出。</p>
</blockquote>
<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>
</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/07/05/go-fundamentals-translation/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go测试的20个实用建议</title>
		<link>https://tonybai.com/2024/01/01/go-testing-by-example/</link>
		<comments>https://tonybai.com/2024/01/01/go-testing-by-example/#comments</comments>
		<pubDate>Mon, 01 Jan 2024 11:36:06 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assert]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[bytes]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[fatal]]></category>
		<category><![CDATA[fuzzing]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.dev]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherConAu]]></category>
		<category><![CDATA[Ivy]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[parser]]></category>
		<category><![CDATA[POSIX]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[table-driven-test]]></category>
		<category><![CDATA[testdata]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[txtar]]></category>
		<category><![CDATA[Unittest]]></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=4099</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/01/01/go-testing-by-example 2023年11月初，Go语言技术负责人Russ Cox在GopherCon Australia 2023大会上进行了题为“Go Testing By Example”的演讲： 12月初Russ Cox重新录制了该演讲内容的视频，并在个人网站上放了出来。这个演讲视频是关于如何编写好的Go测试的，Russ Cox介绍了20个实用建议，非常值得Go初学者甚至Go资深开发者学习并应用到实践中。这里是基于该视频整理的文字稿(可能并非逐字逐句)，供广大Gopher参考。 注：在GopherCon Australia 2023，退休后暂定居澳大利亚的Go语言之父Rob Pike也做了一个名为“What We Got Right, What We Got Wrong”的主题演讲。在Go开源14年之后，有很多事情值得思考。这个演讲“事后诸葛亮般地”探讨了Go迄今为止取得的一些经验教训：不仅包括进展顺利的方面，还包括本可以做得更好的方面。可惜目前该演讲视频或文字稿并未放出，我们也只能等待。 大家好！这是几周前我在GopherCon Australia 2023进行的一次演讲，演讲的内容是关于如何编写好的测试。 不过首先让我们来思考一下为什么我们要编写测试。一些有关编程的书中常讲到：测试是为了发现程序中的错误！比如Brian W. Kernighan和Rob Pike合著的《The Practice of Programming》一书中讲到：“测试是一种坚定的、系统地尝试，旨在破坏你认为可以正确运行的程序”。这是真实的。这就是为什么程序员应该编写测试。但对于今天在这里的大多数人来说，这不是我们编写测试的原因，因为我们不仅仅是程序员，我们是软件工程师。什么意思呢？我想说的是，软件工程就是当你编程时增加时间和其他程序员时所发生的事情。编程意味着让程序运行，你有一个问题需要解决，你编写一些代码，运行它，测试它，调试它，得到答案，你就完成了。这本已经相当困难了，而测试是该过程的重要组成部分。但软件工程意味着你在长期与其他人一起开发的程序中完成所有这些工作，这改变了测试的性质。 让我们先看一个对二分查找函数的测试： 如图所示，这个函数接受一个有序(sorted)切片、一个目标值(target)和一个比较函数(cmp)。它使用二分搜索算法查找并返回两个内容：第一，如果目标存在，则返回其索引(index)，第二是一个布尔值，指示目标是否存在。 大多数二分查找算法的实现都有错误，这个也不例外。我们来测试一下。 下面是一个很好的二分搜索的交互式测试： 你输入两个数字n和t，测试程序便创建一个包含n个元素的切片，其元素值按10倍增，然后程序在切片中搜索t并打印结果，然后你反复重复这一过程。 这可能看起来不足为奇，但有多少人曾经通过运行这种交互式测试程序来测试生产环境用的代码(production code)？我们所有人都这样做过。当你独自编程时，像这样的交互式测试程序对于查找bug非常有用，到目前为止代码看起来可以正常工作。 但这个交互式测试程序只适合独自编程时使用，如果你从事软件工程，意味着你要长时间保持程序的运行，并与其他人合作，那么这种类型的测试程序就不太有用了。 你需要一种每个人都可以在日常工作中运行的测试程序，可以在他们编写代码的同时运行，并且可以由计算机在每次代码提交时自动运行。问题在于仅通过手动测试程序只能确保它在今天正常工作，而自动化、持续的测试可以确保它在明天和未来都可以正常工作，即使其他不熟悉这段代码的人开始对其进行维护。并且我们要明确一点：那个不太熟悉代码的人可能是指未来六个月甚至六周后的你。 这是一个软件工程师的测试。你可以在不了解代码工作原理的情况下运行它。任何同事或任何计算机都可以使用”go test”运行该测试，并可以立即知道该测试是否通过。我肯定你已经见过这样的测试了。 软件工程的理想是拥有能够捕捉到后续可能出现的所有错误的测试。如果你的测试达到了这个理想状态，那么当你的所有测试都通过时，你应该可以放心地自动将你的代码部署到生产环境中，这就是人们所称的持续部署。如果你还没有这样做，如果这个想法让你感到紧张，那么你应该问问自己为什么。要么你的测试已经足够好，要么它们还不够好。如果它们足够好，那为什么不这样做呢？而如果它们不够好，那就倾听这些疑虑，并找出它们告诉你哪些测试被遗漏了。 几年前，我正在为新的Go官方网站go.dev编写代码。那时我们还在手动部署该网站，并且至少每周一次。我做的一项代码变更在我的机器上运行正常，但在部署到生产环境后便无法正常工作了，这着实令人非常烦恼和尴尬。解决办法是进行更好的测试和自动化的持续部署。现在，每当代码库中有新的提交时，我们使用一个Cloud Build程序来运行本地测试，并将代码推送到一个全新的服务器，然后运行一些只能在生产环境中运行的测试。如果一切正常，我们会将流量打到新的服务器。这样做改善了两点。首先，我不再导致令人尴尬的网站宕机。其次，每个人都不再需要考虑如何部署网站。如果他们想做变更，比如修复拼写错误或添加新的博客文章，他们只需发送更改请求，对其进行审核、测试和提交，然后自动化流程会完成其余工作。 要确信当其他人更改代码时你的程序不会出错，要确信只要测试通过就可以随时将程序推送到生产环境，你需要一套非常好的测试。但是什么样的测试才算是好的呢？ 一般来说，使测试代码优秀的因素与使非测试代码优秀的因素是相同的：勤奋(hard work)、专注(attention)和时间(time)。对于编写优秀的测试代码，我没有什么“银弹式”的或硬性的规则，就像编写优秀的非测试代码一样。然而，我确实有一系列基于我们在Go上的良好实践的建议，我将在这次演讲中分享20个编写优秀测试代码的实用建议。 建议1：让添加新测试用例变得容易 这是最重要的建议。因为如果添加一个新测试用例很困难，你就不会去做。在这方面，Go已经提供了很好的支持。 上图是函数Foo的一个最简单的测试。我们专门设计了Go测试，使其非常容易编写。没有繁杂的记录或仪式会妨碍你。在包级别的测试中，这已经相当不错了，但在特定的包中，你可以做得更好。 我相信你已经了解了表驱动测试。我们鼓励使用表驱动测试，因为它们非常容易添加新的测试用例。这是我们之前看到的那个测试用例：假设我们只有这一个测试用例，然后我们想到了一个新的测试用例。我们根本不需要编写任何新的代码，只需要添加一行新的数据。如果目标是“使添加新的测试用例变得容易”，那么对于像这样的简单函数，向表中添加一行数据就足够了。不过，这也引出了一个问题：我们应该添加哪些测试用例？这将引导我们来到下一个建议。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/01/01/go-testing-by-example">本文永久链接</a> &#8211; https://tonybai.com/2024/01/01/go-testing-by-example</p>
<p>2023年11月初，Go语言技术负责人Russ Cox在<a href="https://gophercon.com.au/">GopherCon Australia 2023</a>大会上进行了题为<a href="https://research.swtch.com/testing">“Go Testing By Example”</a>的演讲：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-2.png" alt="" /></p>
<p>12月初Russ Cox重新录制了该演讲内容的视频，并在<a href="https://research.swtch.com/testing">个人网站</a>上放了出来。这个演讲视频是关于如何编写好的Go测试的，Russ Cox介绍了20个实用建议，非常值得Go初学者甚至Go资深开发者学习并应用到实践中。这里是基于该视频整理的文字稿(可能并非逐字逐句)，供广大Gopher参考。</p>
<blockquote>
<p>注：在GopherCon Australia 2023，退休后暂定居澳大利亚的Go语言之父Rob Pike也做了一个名为“What We Got Right, What We Got Wrong”的主题演讲。在<a href="https://tonybai.com/2023/11/11/go-opensource-14-years/">Go开源14年</a>之后，有很多事情值得思考。这个演讲“事后诸葛亮般地”探讨了Go迄今为止取得的一些经验教训：不仅包括进展顺利的方面，还包括本可以做得更好的方面。可惜目前该演讲视频或文字稿并未放出，我们也只能等待。</p>
</blockquote>
<hr />
<p>大家好！这是几周前我在GopherCon Australia 2023进行的一次演讲，演讲的内容是关于如何编写好的测试。</p>
<p>不过首先让我们来思考一下为什么我们要编写测试。一些有关编程的书中常讲到：<strong>测试是为了发现程序中的错误</strong>！比如Brian W. Kernighan和Rob Pike合著的《<a href="https://book.douban.com/subject/1459281/">The Practice of Programming</a>》一书中讲到：“测试是一种坚定的、系统地尝试，旨在破坏你认为可以正确运行的程序”。这是真实的。这就是为什么程序员应该编写测试。但对于今天在这里的大多数人来说，这不是我们编写测试的原因，<strong>因为我们不仅仅是程序员，我们是软件工程师</strong>。什么意思呢？我想说的是，软件工程就是当你编程时增加时间和其他程序员时所发生的事情。编程意味着让程序运行，你有一个问题需要解决，你编写一些代码，运行它，测试它，调试它，得到答案，你就完成了。这本已经相当困难了，而测试是该过程的重要组成部分。但软件工程意味着你在长期与其他人一起开发的程序中完成所有这些工作，这改变了测试的性质。</p>
<p>让我们先看一个对二分查找函数的测试：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-3.png" alt="" /></p>
<p>如图所示，这个函数接受一个有序(sorted)切片、一个目标值(target)和一个比较函数(cmp)。它使用二分搜索算法查找并返回两个内容：第一，如果目标存在，则返回其索引(index)，第二是一个布尔值，指示目标是否存在。</p>
<p>大多数二分查找算法的实现都有错误，这个也不例外。我们来测试一下。</p>
<p>下面是一个很好的二分搜索的交互式测试：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-4.png" alt="" /></p>
<p>你输入两个数字n和t，测试程序便创建一个包含n个元素的切片，其元素值按10倍增，然后程序在切片中搜索t并打印结果，然后你反复重复这一过程。</p>
<p>这可能看起来不足为奇，但有多少人曾经通过运行这种交互式测试程序来测试生产环境用的代码(production code)？我们所有人都这样做过。当你独自编程时，像这样的交互式测试程序对于查找bug非常有用，到目前为止代码看起来可以正常工作。</p>
<p>但这个交互式测试程序只适合独自编程时使用，如果你从事软件工程，意味着你要长时间保持程序的运行，并与其他人合作，那么这种类型的测试程序就不太有用了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-5.png" alt="" /></p>
<p>你需要一种每个人都可以在日常工作中运行的测试程序，可以在他们编写代码的同时运行，并且可以由计算机在每次代码提交时自动运行。问题在于仅通过手动测试程序只能确保它在今天正常工作，而自动化、持续的测试可以确保它在明天和未来都可以正常工作，即使其他不熟悉这段代码的人开始对其进行维护。并且我们要明确一点：那个不太熟悉代码的人可能是指未来六个月甚至六周后的你。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-6.png" alt="" /></p>
<p>这是一个软件工程师的测试。你可以在不了解代码工作原理的情况下运行它。任何同事或任何计算机都可以使用”go test”运行该测试，并可以立即知道该测试是否通过。我肯定你已经见过这样的测试了。</p>
<p>软件工程的理想是拥有能够捕捉到后续可能出现的所有错误的测试。如果你的测试达到了这个理想状态，那么当你的所有测试都通过时，你应该可以放心地自动将你的代码部署到生产环境中，这就是人们所称的<strong>持续部署</strong>。如果你还没有这样做，如果这个想法让你感到紧张，那么你应该问问自己为什么。要么你的测试已经足够好，要么它们还不够好。如果它们足够好，那为什么不这样做呢？而如果它们不够好，那就倾听这些疑虑，并找出它们告诉你哪些测试被遗漏了。</p>
<p>几年前，我正在为新的Go官方网站go.dev编写代码。那时我们还在手动部署该网站，并且至少每周一次。我做的一项代码变更在我的机器上运行正常，但在部署到生产环境后便无法正常工作了，这着实令人非常烦恼和尴尬。解决办法是进行更好的测试和自动化的持续部署。现在，每当代码库中有新的提交时，我们使用一个Cloud Build程序来运行本地测试，并将代码推送到一个全新的服务器，然后运行一些只能在生产环境中运行的测试。如果一切正常，我们会将流量打到新的服务器。这样做改善了两点。首先，我不再导致令人尴尬的网站宕机。其次，每个人都不再需要考虑如何部署网站。如果他们想做变更，比如修复拼写错误或添加新的博客文章，他们只需发送更改请求，对其进行审核、测试和提交，然后自动化流程会完成其余工作。</p>
<p>要确信当其他人更改代码时你的程序不会出错，要确信只要测试通过就可以随时将程序推送到生产环境，你需要一套非常好的测试。但是什么样的测试才算是好的呢？</p>
<p>一般来说，使测试代码优秀的因素与使非测试代码优秀的因素是相同的：勤奋(hard work)、专注(attention)和时间(time)。对于编写优秀的测试代码，我没有什么“银弹式”的或硬性的规则，就像编写优秀的非测试代码一样。然而，我确实有一系列基于我们在Go上的良好实践的建议，我将在这次演讲中分享20个编写优秀测试代码的实用建议。</p>
<h2>建议1：让添加新测试用例变得容易</h2>
<p>这是最重要的建议。因为如果添加一个新测试用例很困难，你就不会去做。在这方面，Go已经提供了很好的支持。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-7.png" alt="" /></p>
<p>上图是函数Foo的一个最简单的测试。我们专门设计了Go测试，使其非常容易编写。没有繁杂的记录或仪式会妨碍你。在包级别的测试中，这已经相当不错了，但在特定的包中，你可以做得更好。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-8.png" alt="" /></p>
<p>我相信你已经了解了表驱动测试。我们鼓励使用表驱动测试，因为它们非常容易添加新的测试用例。这是我们之前看到的那个测试用例：假设我们只有这一个测试用例，然后我们想到了一个新的测试用例。我们根本不需要编写任何新的代码，只需要添加一行新的数据。如果目标是“使添加新的测试用例变得容易”，那么对于像这样的简单函数，向表中添加一行数据就足够了。不过，这也引出了一个问题：我们应该添加哪些测试用例？这将引导我们来到下一个建议。</p>
<h2>建议2：使用测试覆盖率来发现未经测试的代码</h2>
<p>毕竟，测试无法捕捉到未运行的代码中的错误。Go内置了对测试覆盖率的支持。下面是它的样子：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-9.png" alt="" /></p>
<p>你可以运行“go test -coverprofile”来生成一个覆盖率文件，然后使用“go tool cover”在浏览器中查看它。在上图的显示中，我们可以看到我们的测试用例还不够好：实际的二分查找代码是红色的，表示完全未经测试。下一步是查看未经测试的代码，并思考什么样的测试用例会使这些代码行运行。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-10.png" alt="" /></p>
<p>经过仔细检查，我们只测试了一个空切片，所以让我们添加一个非空的切片的测试用例。现在我们可以再次运行覆盖率测试。这次我将用我写的一个小命令行程序“uncover”来读取覆盖率文件。Uncover会显示未被测试覆盖的代码行。它不会给你网页视图那样的全局视图，但它可以让你保持在一个终端窗口中。Uncover向我们展示了只剩下一行代码未被测试执行。这是进入切片的第二半部分的行，这是有道理的，因为我们的目标是第一个元素。让我们再添加一个测试，搜索最后一个元素。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-11.png" alt="" /></p>
<p>当我们运行测试时，它通过了，我们达到了100%的覆盖率。很棒。我们完成了吗？没有，这将引导我们到下一个实用建议。</p>
<h2>建议3：覆盖率不能替代思考</h2>
<p>覆盖率对于指出你可能忽略的代码部分非常有用，但机械工具无法替代对于高难度的输入、代码中的微妙之处以及可能导致代码出错的情况进行的实际思考。即使代码拥有100%的测试覆盖率，仍然可能存在bug，而这段代码就存在bug。这个提示也适用于<a href="https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18">覆盖率驱动的模糊测试(fuzzing test)</a>。模糊测试只是尝试通过代码探索越来越多的路径，以增加覆盖率。模糊测试也非常有帮助，但模糊测试也不能替代思考。那么这里缺少了什么呢？</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-12.png" alt="" /></p>
<p>需要注意的一点是，唯一一个无法找到目标的测试用例是一个空输入切片。我们应该检查在具值的切片中无法找到目标的情况。具体来说，我们应该检查当目标小于所有值、大于所有值和位于值的中间时会发生什么。所以让我们添加三个额外的测试用例。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-13.png" alt="" /></p>
<p>注意添加新测试用例是多么容易。如果你想到一个你的代码可能无法正确处理的情况，添加该测试用例应该尽可能简单，否则你就会觉得麻烦而不去添加。如果太困难，你就不会添加。你还可以看到我们正在开始列举这个函数可能出错的所有重要路径。这些测试对未来的开发进行了约束，以确保二分查找至少能够正常工作。当我们运行这些测试时，它们失败了。返回的索引i是正确的，但表示target是否找到的布尔值是错误的。所以让我们来看看这个问题。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-14.png" alt="" /></p>
<p>阅读代码，我们发现返回语句中的布尔表达式是错误的。它只检查索引是否在范围内。它还需要检查该索引处的值是否等于target值。所以我们可以进行这个更改，如图所示，然后测试通过了。现在我们对这个测试感到非常满意：覆盖率是良好的，我们也经过了深思熟虑。还能做什么呢？</p>
<h2>建议4：编写全面的测试</h2>
<p>如果你能够测试函数的每一个可能输入，那就应该这样做。但现实中可能无法做到，但通常你可以在一定约束条件下测试特定数量以内的所有输入。下面是一个二分查找的全面测试：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-15.png" alt="" /></p>
<p>我们首先创建一个包含10个元素的切片，具体来说就是从1到19的奇数。然后我们考虑该切片的所有可能长度的前缀。对于每个前缀，我们考虑从0到两倍长度的所有可能目标，其中0是小于切片中的所有值，两倍长度是大于切片中的所有值。这将详尽地测试每个可能的搜索路径，以及长度不超过我们的限制10的所有可能尺寸的切片。但是现在我们怎么知道答案是什么呢？我们可以根据测试用例的具体情况进行一些数学计算，但有一种更好、更通用的方法。这种方法是编写一个与真正实现不同的参考实现。理想情况下，参考实现应该明显是正确的，但它只需与真实实现采用不同的方法即可。通常，参考实现将是一种更简单、更慢的方法，因为如果它更简单和更快，你会将其用作真正的实现。在这种情况下，我们的参考实现称为slowFind。测试检查slowFind和Find是否可以在答案上达成一致。由于输入很小，slowFind可以采用一个简单的线性搜索。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-16.png" alt="" /></p>
<p>通过生成所有可能的输入并将结果与简单的参考实现进行比较，这种模式非常强大。它做的一件重要的事情是覆盖了所有基本情况，例如0个元素的切片、1个元素的切片、长度为奇数的切片、长度为偶数的切片、长度为2的幂的切片等等。大多数程序中的绝大多数错误都可以通过小规模的输入进行重现，因此测试所有小规模的输入非常有效。事实证明，这个全面测试通过了。我们的思考相当不错。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-17.png" alt="" /></p>
<p>现在，如果全面测试失败，那意味着Find和slowFind不一致，至少有一个有bug，但我们不知道是哪一个有问题。添加一个直接测试slowFind会有所帮助，而且很容易，因为我们已经有了一个测试数据表。这是表驱动测试的另一个好处：可以使用这些表来测试多个实现。</p>
<h2>建议5：将测试用例与测试逻辑分开</h2>
<p>在表驱动测试中，测试用例在表中，而处理这些测试用例的循环则是测试逻辑。正如我们刚才所看到的，将它们分开可以让你在多个上下文中使用相同的测试用例。那么现在我们的二分查找函数完成了吗？事实证明没有，还有一个bug存在，这引导我们到下一个问题。</p>
<h2>建议6：寻找特殊情况</h2>
<p>即使我们对所有小规模情况进行了全面测试，仍然可能存在潜在的bug：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-18.png" alt="" /></p>
<p>现在，这里再次展示了代码。还剩下一个bug。你可以暂停视频，花一些时间来查看它。</p>
<p>有人看出bug在哪里了吗？如果你没有看到，没关系。这是一个非常特殊的情况，人们花了几十年的时间才注意到它。Knuth告诉我们，尽管二分查找在1946年发表，但第一个正确的二分查找实现直到1964年才发表。但是这个bug直到2006年才被发现。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-19.png" alt="" /></p>
<p>bug是这样的，如果切片中的元素数量非常接近int的最大值，那么i+j会溢出，因此i+j/2就不是切片中间位置的正确计算方法了。这个bug于2006年在一个使用64位内存和32位整数的C程序中被发现，这个程序用于索引包含超过10亿个元素的数组。在Go语言中，这种特定组合基本上不会发生，因为我们要求使用64位内存时，也要使用64位整数，这正是为了避免这种bug。但是，由于我们了解到这个bug，而且你永远不知道你或其他人将来如何修改代码，所以避免这个bug是值得的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-20.png" alt="" /></p>
<p>有两种常见的修复方法可以避免数学计算溢出。速度稍快的方法是进行无符号除法。假设我们修复了这个问题。现在我们完成了吗？不。因为我们还没有编写测试。</p>
<h2>建议7：如果你没有添加测试，那就没有修复bug</h2>
<p>这句话在两个不同的方面下都是正确的。</p>
<p>第一个是编程方面。如果你没有进行测试，bug可能根本没有被修复。这听起来可能很愚蠢，但你有多少次遇到过这种情况？有人告诉你有一个bug，你立即知道修复方法。你进行了更改，并告诉他们问题已经修复。然后他们却回来告诉你，不，问题还存在。编写测试可以避免这种尴尬。你可以说，很抱歉我没有修复你的bug，但我确实修复了一个bug，并会再次查看这个问题。</p>
<p>第二个是软件工程方面，即“时间和其他程序员”的方面。bug并不是随机出现的。在任何给定的程序中，某些错误比其他错误更有可能发生。因此，如果你犯了一次这个错误，你或其他人很可能在将来再次犯同样的错误。如果没有测试来阻止它们，bug就会重新出现。</p>
<p>现在，这个特定的测试很难编写，因为输入范围非常大，但即使测试很难编写，这个建议仍然成立。实际上，在这种情况下，这个建议通常更为正确。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-21.png" alt="" /></p>
<p>为了测试这种情况，一种可能性是编写一个仅在32位系统上运行的测试，对两千兆字节的uint8进行二分查找。但这需要大量的内存，并且我们现在已经没有多少32位系统了。对于测试这种难以找到的bug，通常还有更巧妙的解决方案。我们可以创建一个空结构体的切片，无论它有多长，都不会占用内存。这个测试在一个包含MaxInt个空结构体的切片上调用Find函数，寻找一个空结构体作为目标，但是它传入了一个总是返回-1的比较函数，声称切片元素小于目标。这将使二分查找探索越来越大的切片索引，从而导致溢出问题。如果我们撤销我们的修复并运行这个测试，那么测试肯定会失败。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-22.png" alt="" /></p>
<p>而使用了我们的修复后，测试通过了。现在bug已经修复了。</p>
<h2>建议8：并非所有东西都适合放在表中</h2>
<p>这个特殊情况不适合放在表中，但这没关系。但是很多东西确实适合放在表中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-23.png" alt="" /></p>
<p>这是我最喜欢的一个测试表之一。它来自fmt.Printf的测试用例。每一行都是一个printf格式、一个值和预期的字符串。真实的表太大了，无法放在幻灯片上，但这里摘录了一些表中的代码行。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-24.png" alt="" /></p>
<p>如果你仔细阅读整个表，你会看到其中一些明显是修复bug的内容。记住<strong>建议7：如果你没有添加测试，那就没有修复bug</strong>。表格使得添加这些测试变得非常简单，并且添加这些测试可以确保这些bug不会再次出现。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-25.png" alt="" /></p>
<p>表格是将测试用例与测试逻辑分离并且方便添加新的测试用例的一种方法，但有时你会有很多测试，甚至写Go语法的开销也是不必要的。例如，这里是strconv包的一个测试文件，用于测试字符串与浮点数之间的转换。你可能认为编写解析器来处理这个输入太麻烦了，但一旦你知道了如何处理，其实并不需要太多工作，而且定义测试专用的小型语言实际上非常有用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-26.png" alt="" /></p>
<p>因此，我将快速介绍一下解析器，以展示它并不复杂。我们读取文件，然后将其分割成行。对于每一行，我们计算错误消息的行号。切片元素0表示第1行。我们去掉行尾的任何注释。如果行为空白行，我们跳过它。到目前为止，这是相当标准的样板代码。现在是重点。我们将行分割为字段，并提取出四个字段。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-27.png" alt="" /></p>
<p>然后根据类型字段在float32或float64的数学运算中进行转换。myatof64基本上是strconv.ParseFloat64的变体，不同之处在于它处理允许我们按照从论文中复制的方式编写测试用例的十进制p格式。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-28.png" alt="" /></p>
<p>最后，如果结果不是我们想要的，我们打印错误。这非常类似于基于表格的测试。我们只是解析文件，而不是遍历表格。它无法放在一个幻灯片上，但在开发时它可以放在一个屏幕上。</p>
<h2>建议9：测试用例可以放在testdata文件中</h2>
<p>测试不必都要放在源代码中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-29.png" alt="" /></p>
<p>作为另一个例子，Go正则表达式包包含了一些从AT&amp;T POSIX正则表达式库复制过来的testdata文件。我不会在这里详细介绍，但我很感激他们选择为该库使用基于文件的测试，因为这意味着我可以重用testdata文件，将其用于Go。这是另一种ad-hoc格式，但它易于解析和编辑。</p>
<h2>建议10：与其他实现进行比较</h2>
<p>与AT&amp;T正则表达式的测试用例进行比较有助于确保Go的包以完全相同的方式处理各种边缘情况。我们还将Go的包与C++的RE2库进行比较。为了避免需要编译C++代码，我们以记录所有测试用例的方式运行它，并将该文件作为testdata提交到Go中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-30.png" alt="" /></p>
<p>在文件中存储测试用例的另一种方法是使用成对的文件，一个用于输入，一个用于输出。为了实现go test -json，有一个名为test2json的程序，它读取测试输出并将其转换为JSON输出。测试数据是成对的文件：测试输出和JSON输出。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-31.png" alt="" /></p>
<p>这是最简短的文件。测试输出位于顶部，它是test2json的输入，应该生成底部的JSON输出。以下是实现，展示了从文件中读取测试数据的惯用方法。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-32.png" alt="" /></p>
<p>我们首先使用filepath.Glob查找所有的testdata。如果失败或找不到任何文件，我们会报错。否则，我们循环遍历所有文件。对于每个文件，我们通过获取基本文件名（不包括testdata/目录名和文件后缀）来创建子测试名称。然后我们用该名称运行一个子测试。如果你的测试用例足够复杂，每个文件一个子测试通常是有意义的。这样，当一个测试用例失败时，你可以使用go test -run只运行特定的文件。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-33.png" alt="" /></p>
<p>对于实际的测试用例，我们只需要读取文件，运行转换器，并检查结果是否匹配。对于检查，我最开始使用了bytes.Equal，但随着时间的推移，编写一个自定义的diffJSON函数来解析两个JSON结果并打印实际差异的详细说明变得更有价值。</p>
<h2>建议11：使测试失败易读</h2>
<p>回顾一下，我们已经在二分查找中看到了这一点。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-34.png" alt="" /></p>
<p>我认为我们都同意粉色框不是一个好的失败。但是黄色框中有两个细节使得这些失败尤为出色。首先，我们在单个if语句中检查了两个返回值，然后在简洁的单行中打印了完整的输入和输出。其次，我们不会在第一个失败处停止。我们使用t.Error而不是t.Fatal，以便执行更多的测试用例。结合起来，这两个选择让我们可以看到每个失败的完整细节，并在多个失败中寻找模式。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-35.png" alt="" /></p>
<p>回到test2json，这是它的测试失败的情况。它计算出哪些事件是不同的，并清晰地标记它们。重要的是，在你编写测试时，你不必写这种复杂的代码。bytes.Equal在开始时是可以的，并且可以专注于代码。但是随着失败变得更加微妙，并且你发现自己花费太多时间只是阅读失败输出，这是一个好的信号，它告诉你是时候花一些时间使其更易读了。此外，如果确切的输出发生更改并且你需要更正所有的测试数据文件，这种类型的测试可能会有点麻烦。</p>
<h2>建议12：如果答案可能会改变，编写代码来更新它们</h2>
<p>通常的做法是在测试中添加一个“-update”标志。这是test2json的更新代码示例。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-36.png" alt="" /></p>
<p>测试定义了一个新的“-update标志”。当标志为true时，测试将计算的答案写入答案文件，而不是调用diffJSON。现在，当我们对JSON格式进行有意的更改时，“go test -update”会更新所有答案。你还可以使用版本控制工具如“git diff”来审查更改，并在看起来不正确时撤销更改。在谈论测试文件的主题上，有时将一个测试用例分割成多个文件会很烦人。如果我今天编写这个测试，我就不会这样做。</p>
<h2>建议13： 使用txtar进行多文件测试用例</h2>
<blockquote>
<p>注：导入txtar：import “golang.org/x/tools/txtar”</p>
</blockquote>
<p>Txtar是我们几年前专门为解决多文件测试用例问题而设计的一种新的存档格式。其Go解析器位于golang.org/x/tools/txtar中，我还找到了用Ruby、Rust和Swift编写的解析器。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-37.png" alt="" /></p>
<p>Txtar的设计有三个目标。首先，足够简单，可以手动创建、编辑和阅读。其次，能够存储文本文件的树形结构，因为我们在go命令中需要这个功能。第三，能够在git历史记录和代码审查中进行良好的差异比较。其他的包括成为完全通用的存档格式、存储二进制数据、存储文件模式(file mode)、存储符号链接等都不是目标，因为存档文件(archived file)格式往往变得十分复杂，而复杂性与第一个目标直接相矛盾。这些目标和非目标导致了一个非常简单的格式。下面是一个示例：txtar文件以注释开头。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-38.png" alt="" /></p>
<p>本例中为”Here are some greetings.”，然后通常会有零个或多个文件，每个文件由形如”&#8211; 文件名 &#8211;”的行引入。这个存档包含两个单行文件，hello和g&#8217;day。就是这样，这就是整个格式。没有转义，没有引用，没有对二进制数据的支持，没有符号链接，没有可能的语法错误，没有复杂之处。下面是一个在测试数据中使用txtar文件的真实示例。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-39.png" alt="" /></p>
<p>该测试数据用于计算差异的包：在这种情况下，注释对于人们来说很有用，用于记录正在进行的测试，然后在这个测试中，每个用例由两个文件和它们的差异后面跟随的两个文件组成。</p>
<p>使用txtar文件几乎和编写它们一样简单。下面是我们之前查看的diff包的测试。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-40.png" alt="" /></p>
<p>这是通常的基于文件的循环，但我们在文件上调用了txtar.ParseFile。然后我们坚持认为存档包含三个文件，第三个文件的名称为diff。然后我们对两个输入文件进行差异比较，并检查结果是否与预期的差异匹配。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-41.png" alt="" /></p>
<p>这就是整个测试。你可能已经注意到，在使用之前，文件数据会被传递给”clean”函数进行清理。clean函数允许我们在不使txtar格式本身复杂化的情况下添加一些特定于diff的扩展。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-42.png" alt="" /></p>
<p>第一个扩展处理以空格结尾的行，在差异中确实会出现这种情况。许多编辑器希望去除这些尾随空格，因此测试允许在txtar的数据行末尾放置$，并且clean函数会删除该$。在这个示例中，标记的行需要以一个空格结尾。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-43.png" alt="" /></p>
<p>此外，txtar要求文件中的每一行都以换行符结尾，但我们希望测试diff在不以换行符结尾的文件上的行为。因此，测试允许在结尾处放置一个字面意义上的“尖号D”。clean函数会删除“尖号D”和其后的换行符。在这种情况下，&#8217;new&#8217;文件最终没有最后的换行符，而diff正确报告了这一点。因此，尽管txtar非常简单，你也可以轻松地在其上添加自己的格式调整。当然，重要的是要记录这些调整，以便下一个参与测试的人能够理解它们。</p>
<h2>建议14：对现有格式进行注解(annotation)来创建测试迷你语言</h2>
<p>对现有格式进行注释，比如在txtar中添加$和尖号D，是一个强大的工具。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-44.png" alt="" /></p>
<p>这里是对现有格式进行注释的一个示例。这是Go类型检查器(type checker)的一个测试。这是一个普通的Go输入文件，但是期望的类型错误已经以/&#42;ERROR&#42;/注释的形式添加了进去。我们使用/&#42;注释，这样我们就可以将它们放置在错误报告的确切位置上。测试运行类型检查器，并检查它是否在预期位置产生了预期的消息，并且没有产生任何意外的消息。下面是类型检查器的另一个示例。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-45.png" alt="" /></p>
<p>在这个测试中，我们在通常的Go语法之上添加了一个assert注释。这使我们能够编写常量算术的测试，就像这个例子一样。类型检查器已经计算了每个常量表达式的布尔值，所以检查assert其实只是检查常量是否被求值为true。下面是另一个带有注释的格式示例。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-46.png" alt="" /></p>
<p>Ivy是一个交互式计算器。你输入程序，通常是简单的表达式，它会打印出答案。测试用例是看起来像这样的文件：未缩进的行是Ivy的输入，缩进的行是注释，指示Ivy应该打印出预期的输出。编写新的测试用例再也没有比这更简单的了。这些带注释的格式扩展了现有的解析器和打印器(printer)。有时编写自己的解析器和打印器是有帮助的。毕竟，大多数测试涉及创建或检查数据，当你可以使用方便的形式处理数据时，这些测试总是可以更好。</p>
<h2>建议15：编写解析器和打印器来简化测试</h2>
<p>这些解析器和打印器不一定是用于testdata中数据文件的独立脚本。你也可以在常规的Go代码中使用它们。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-47.png" alt="" /></p>
<p>这是一个运行deps.dev代码的一个测试片段。这个测试设置了一些数据库表行。它调用了一个使用数据库并正在进行测试的函数。然后它检查数据库是否包含了预期的结果。Insert和Want调用使用了一个专门为这些测试编写的用于数据库内容的迷你语言。解析器就像它看起来的那样简单：它将输入分割成行，然后将每行分割成字段。第一行给出了列名。就是这样。这些字符串中的确切间距并不重要，但是如果它们都对齐，当然看起来更美观。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-48.png" alt="" /></p>
<p>因此，为了支持这个测试，deps.dev团队还有一个专门为这些测试编写的代码格式化程序。它使用Go标准库解析测试源代码文件。然后它遍历Go语法树，查找Insert或Want的调用。它提取字符串参数并将它们解析为表格。然后它将表格重新打印为字符串，将字符串重新插入语法树中，并重新打印语法树为Go源代码。这只是gofmt的一个扩展版本，使用了与gofmt相同的包。我这里不会展示这些代码，但代码量其实不多。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-49.png" alt="" /></p>
<p>解析器和打印器需要花费了一些时间来编写。但现在，每当有人编写一个测试时，编写测试就更容易了。每当一个测试失败或需要更新时，调试也更容易了。如果你正在进行软件工程，收益将随着程序员数量和项目生命周期的增加而扩大。对于deps.dev来说，已经花费在这个解析器和打印器上的时间已经多次节省了。或许更重要的是，因为测试更容易编写，你可能会写更多的测试，这将导致更高质量的代码。</p>
<h2>建议16：代码质量受测试质量限制</h2>
<p>如果你不能编写高质量的测试，你将无法编写足够的测试，并且最终无法得到高质量的代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-50.png" alt="" /></p>
<p>现在我想向你展示一些我曾经参与的最高质量的测试，这些测试是针对go命令的测试。它们将我们到目前为止看到的许多思想汇集在一起。这是一个简单但真实的go命令测试。这是一个txtar输入，其中包含一个名为hello.go的文件。archive comment是一个逐行简单命令语言编写的脚本。在脚本中，”env”设置一个环境变量来关闭Go module机制。井号引入注释。而”go”运行go命令，它应该运行hello world。该程序应该将hello world打印到标准错误中。”stderr”命令检查前一个命令打印的标准错误流是否与正则表达式匹配。因此，这个测试运行”go run hello.go”并检查它是否将hello world打印到标准错误中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-51.png" alt="" /></p>
<p>这里是另一个真实的测试。请注意底部的a.go是一个无效的程序，因为它导入了一个空字符串。第一行开头的感叹号是一个”非”操作符。NOT go list a.go意味着go list a.go应该失败。下一行的”NOT stdout .”表示标准输出不应该有与正则表达式”.”匹配的内容，也就是不应该打印任何文本。接下来，标准错误流应该有一个无效的导入路径的消息。最后，不应该发生panic。</p>
<h2>建议17：使用脚本可以编写很好的测试</h2>
<p>这些脚本使添加新的测试用例变得非常容易。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-52.png" alt="" /></p>
<p>这是我们最小的测试用例：两行代码。最近我在破坏了unknown command的错误消息后添加了这个测试用例。总共，我们有超过700个这样的脚本测试，从两行到500多行不等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-53.png" alt="" /></p>
<p>这些测试脚本取代了一个更传统的使用方法(method)的测试框架。这张幻灯片展示了其中一个真实的测试，前面是脚本编写的测试用例，后面是等价的Go编写的传统测试代码。细节并不重要，只需注意脚本要比传统测试方法更容易编写和理解。</p>
<h2>建议18：尝试使用rsc.io/script来创建基于脚本的测试用例</h2>
<p>距离我们创建go脚本测试已经过去了大约五年时间，我们对这个特定的脚本引擎非常满意。Bryan Mills花了很多时间为它提供了一个非常好的API，早在11月份，我将其发布到了rsc.io/script以供导入使用。现在我说”尝试”是因为它还比较新，并且具有讽刺意味的是，它本身的测试还不够多，因为可导入的包只有几周的历史，但你仍然可能会发现它很有用。当我们对其有更多经验时，我们可能会将其放在更官方的位置上。如果你尝试了它，请告诉我结果如何。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-54.png" alt="" /></p>
<p>提取脚本引擎的动机是为了在go命令测试的不同部分中重用它。这个脚本正在准备一个包含我们想要在常规go命令脚本测试中导入的模块的Git存储库(repo)。你可以看到它设置了一些环境变量，运行了真正的git init，设置了时间，在存储库中运行了更多的git命令来添加一个hello world文件，然后检查我们得到了我们想要的存储库。再一次，测试并不是从一开始就是这样的，这引出了下一个实用建议。</p>
<h2>建议19：随着时间的推移改进你的测试</h2>
<p>最初，我们没有这些存储库脚本。我们手工创建小型测试存储库，并将它们发布到GitHub、Bitbucket和其他托管服务器，具体取决于我们所需的版本控制系统。这种方法还算可以，但这意味着如果这些服务器中的任何一个宕机，测试就会失败。最终，我们花时间构建了自己的云服务器，可以为每个版本控制系统提供存储库服务。现在，我们手工创建存储库，将其压缩并复制到服务器上。这样做更好，因为现在只有一个服务器可能会使我们的测试失败，但有时也会出现网络问题。测试存储库本身也没有进行版本控制，并且与使用它们的测试不在一起，这也是一个问题。作为测试的一部分，基于脚本的版本完全可以在本地构建和提供这些存储库。而且现在很容易找到、更改和审查存储库的描述。这需要很多基础设施，但也测试了很多代码。如果你只有10行代码，你完全<strong>不需要</strong>拥有数千行的测试框架。但是如果你有十万行代码，这大约是go命令的规模，那么开发几千行代码来改进测试，甚至是一万行代码，几乎可以肯定是一个不错的投资。</p>
<h2>建议20：追求持续部署</h2>
<p>也许出于策略原因，你无法每次都实际部署那些通过了所有测试的代码提交，但无论如何都要追求这一目标。正如我在演讲开始时提到的，对于持续部署的任何疑问都是有益的小声音，它们告诉你需要更好的测试。而更好的测试的关键当然是让添加新测试变得容易。即使你从未实际启用持续部署，追求这一目标也可以帮助你保持诚实，提高测试的质量和代码的质量。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-55.png" alt="" /></p>
<p>我之前提到过<a href="https://go.dev">Go官方网站</a>使用了持续部署。在每次提交时，我们运行测试来决定是否可以部署最新版本的代码并将流量路由到它。此时，你不会感到惊讶，我们为这些测试编写了一个测试脚本语言。上图是它们的样子。每个测试以一个HTTP请求开始。这里我们GET主页go.dev。然后对响应进行断言。每个断言的形式为”字段(field)，运算符(operator)，值(value)”。这里字段(field)是body，运算符(operator是contains，值(value)是body中必须包含的字面值。这个测试检查页面是否渲染过了，因此它检查基本文本以及一个副标题。为了更容易编写测试，根本没有引号。值就是运算符后面的其余部分。接下来是另一个测试用例。出于历史原因，/about需要重定向到pkg.go.dev。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-56.png" alt="" /></p>
<p>这是另一个案例。这里没有什么特别的，只是检查案例研究页面是否渲染(rendering)了，因为它是由许多其他文件合成的。测试可以检查的另一个字段是HTTP响应代码，这是一个错误修复。我们错误地在Go存储库根目录中提供了这些文件，就好像它们是Go网站页面一样。我们希望改为返回404。你还可以测试标头foo的值，其中foo是某个标头。在这种情况下，标头Content-Type需要正确设置为主博客页面及其JSON feed。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-57.png" alt="" /></p>
<p>这是另一个示例。这个示例使用正则表达式匹配运算符tilde和“\s+”语法，以确保页面具有正确的文本，无论单词之间有多少空格。这变得有点老套了，所以我们添加了一个名为trimbody的新字段，它是将所有空格序列替换为单个空格后的body。这个示例还显示了值可以作为多个缩进的行提供，以便更容易进行多行匹配。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-58.png" alt="" /></p>
<p>我们还有一些无法在本地运行但在生产环境中仍值得运行的测试，因为我们将实时流量迁移到服务器之前需要进行这些测试。下面是其中两个。这些依赖于对生产环境playground后端的网络访问。这些案例除了URL不同之外都是相同的。这不是一个非常易读的测试，因为这些是我们唯一的POST测试。如果我们添加了更多这样的测试，我可能会花时间使它们看起来更好，以随着时间推移改进你的测试。但是现在它们还可以，它们起到了重要的作用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-59.png" alt="" /></p>
<p>最后，和往常一样，添加错误修复很容易。在问题51989中，live web站点根本没有呈现。因此，这个测试检查页面<strong>确实</strong>呈现并包含一个独特的文本片段。问题51989不会再次发生，至少不会在实际的网站上。肯定会有其他错误，但那个问题已经彻底解决了，这就是进步。以上这些是我有时间向你展示的这些例子。</p>
<h2>小结</h2>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-60.png" alt="" /></p>
<p>最后一个想法。我相信你经历过追踪错误并最终发现一个重要的代码片段是错误的情况。但不知何故，这个代码片段的错误大部分时间都无关紧要，或者错误被其他错误的代码抵消了。你可能会想：“这段代码以前是怎么工作的？”如果是你自己编写的代码，你可能会认为自己很幸运。如果是别人编写的代码，你可能会对他们的能力产生质疑，然后又认为他们很幸运。但是，大多数时候，答案并不是运气。对于这段代码为什么会工作的问题的答案几乎总是：因为它有一个测试。当然，代码是错误的，但测试检查了它足够正确，使系统的其他部分可以正常工作，这才是最重要的。也许编写这段代码的人确实是一个糟糕的程序员，但他们是一个优秀的软件工程师，因为他们编写了一个测试，这就是为什么包含该代码的整个系统能够工作的原因。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-testing-by-example-61.png" alt="" /></p>
<p>我希望你从这次演讲中得出的结论不是任何特定测试的具体细节，尽管我希望你可以留意对小型解析器和打印机的良好使用带来的好处。任何人都可以学会编写它们，并且有效地使用它们可以成为软件工程的超能力。最终，这对这些软件包来说是好测试。对于你的软件包，好测试可能看起来会有所不同。这没关系。但要使添加新的测试用例变得容易，并确保你拥有良好、清晰、高质量的测试。请记住，代码质量受测试质量的限制，因此逐步投入改进测试。你在项目上工作的时间越长，你的测试就应该变得越好。并且要追求持续部署，至少作为一种思想实验，以了解哪些方面的测试还不够充分。</p>
<p>总的来说，要像编写优秀的非测试代码一样，思考并投入同样的思想、关心和努力来编写优秀的测试代码，<strong>这绝对是值得的</strong>。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/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://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>
</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/01/01/go-testing-by-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>2023年Go语言盘点：稳中求新，稳中求变</title>
		<link>https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/</link>
		<comments>https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/#comments</comments>
		<pubDate>Sun, 31 Dec 2023 05:27:21 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[2023]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[Arrow]]></category>
		<category><![CDATA[Camel]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[clear]]></category>
		<category><![CDATA[comparable]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[devlake]]></category>
		<category><![CDATA[dubbo]]></category>
		<category><![CDATA[forrange]]></category>
		<category><![CDATA[gemini]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Githut]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[gonew]]></category>
		<category><![CDATA[gopherchina]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[gorilla]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[IEEE]]></category>
		<category><![CDATA[index]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[loopvar]]></category>
		<category><![CDATA[max]]></category>
		<category><![CDATA[min]]></category>
		<category><![CDATA[mux]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[PYPL]]></category>
		<category><![CDATA[redmonk]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[Spectrum]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[Telemetry]]></category>
		<category><![CDATA[Tesla]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[v2]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[zap]]></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=4092</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language 时光荏苒，转眼间已经是2023年的最后一天了。《2022年Go语言盘点：泛型落地，无趣很好，稳定为王》仿佛就写在昨天。 回首这一年，全球彻底从新冠大流行中得以复苏，Go语言也不例外，最直观的表现就是全球各地的GopherCon技术大会或小型Meetup都纷纷从停办/线上的状态来到了线下，并获得Gopher们的热烈欢迎和踊跃参与，比如下图中的GopherCon、GopherCon UK、GopherCon Europe、GopherCon Australia、Golab等。 尤其值得一提的是我们本土最大的Gopher技术大会GopherChina 2023，今年为了满足不同地域Gopher的需求，GoCN社区在6月和11月分别在北京和上海举办了两次GopherChina大会，这也是历史首次。 Go语言团队的大神们也开始重新“乐此不疲”地参与到上述这些大会中，以推进全球Go社区与生态的建设。就连已经退居二线的Go语言之父Rob Pike也亲自“现身说法”，在年底的GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。 大神回顾一生，我们盘点一年。在这篇文章中，我就和大家一起聊聊Go在2023年的状态、所处的位置以及Go未来演进的机制与策略。 1. Go的2023 1.1 稳 一如往年，Go在2023年发布了两个大版本，分别是2023年2月份的Go 1.20和8月份的Go 1.21。 在这两个版本中，Go语法特性一如既往的求稳，除了支持切片类型到数组类型(或数组类型的指针)的类型转换，其余更是像语法的修修补补，比如：comparable“放宽”了对泛型实参的限制、unsafe包继续添加“语法糖”、增加min、max和clear预定义函数、增强type inference能力等。 这些并不会让Gopher感到“意外”，因为这与Russ Cox在2022年宣称的“Go is boring”的精神是一脉相承的。 不过，除了Go语法特性变化方面的“寡淡”之外，Go在其他方面还是求新和求变的，接下来我们先来看看Go是如何求新的。 注：求新与求变可能存在交集的地方，边界可能也有一定模糊性，也存在相互促进的情况，希望大家阅读下面内容时不要吹毛求疵:)。 1.2 求新 Go在语法特性求稳的同时，在编译器、工具链、运行时以及标准库等方面都在努力优化和打磨，旨在进一步提升Go兼具的生产力与运行时效率，其中很多优化和打磨的措施不乏新颖。 Go 1.20版本中首次引入的PGO(profile-guided optimization)技术预览版，到Go 1.21版本变为默认开启，Go官方给出的PGO优化的效果数据是：PGO优化带来的性能提升一般是2%~7%，而在最新的Go 1.22rc1中，这个数字已经变为2%~14%了。 在内存管理方面，Go 1.20引入了试验特性arena包，虽然它没能在Go 1.21中按时转正，如今处于proposal-hold状态，但这也算是一次在内存管理机制上的求新。 Go是一门面向软件工程的编程语言，在这一年中，Go在软件工程领域的求新例子也是不少。比如：可用于大幅简化Go项目创建的gonew工具，它支持基于go project template clone并创建一个属于你的Go项目；再比如对应用执行时的代码覆盖率的采集，可以帮助开发者更进一步了解最终可执行程序代码执行路径上的测试覆盖情况；而govulncheck工具则是Go在软件工程与供应链安全领域的求新尝试，该工具丰富了我们对Go项目进行安全漏洞检查的手段。 注：关于供应链安全问题，Russ Cox近期有一个专门的Talk：Open Source [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language">本文永久链接</a> &#8211; https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language</p>
<p>时光荏苒，转眼间已经是2023年的最后一天了。《<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">2022年Go语言盘点：泛型落地，无趣很好，稳定为王</a>》仿佛就写在昨天。</p>
<p>回首这一年，全球彻底从新冠大流行中得以复苏，Go语言也不例外，最直观的表现就是<strong>全球各地的GopherCon技术大会或小型Meetup都纷纷从停办/线上的状态来到了线下</strong>，并获得Gopher们的热烈欢迎和踊跃参与，比如下图中的<a href="https://www.gophercon.com/">GopherCon</a>、<a href="https://www.gophercon.co.uk/">GopherCon UK</a>、<a href="https://gophercon.eu/">GopherCon Europe</a>、<a href="https://gophercon.com.au/">GopherCon Australia</a>、<a href="https://golab.io/">Golab</a>等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-2.png" alt="" /></p>
<p>尤其值得一提的是我们本土最大的Gopher技术大会<a href="https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==&amp;mid=2247494867&amp;idx=1&amp;sn=9bf0dfa3ef48867da891aac4359a0c5e&amp;chksm=e8600b32df178224428d5ee27fd11e379011afc0bdb7e0445617275c4c43c484f72200b585dc#rd">GopherChina 2023</a>，今年为了满足不同地域Gopher的需求，GoCN社区在6月和11月分别在北京和上海举办了两次GopherChina大会，这也是历史首次。</p>
<p>Go语言团队的大神们也开始重新“乐此不疲”地参与到上述这些大会中，以推进全球Go社区与生态的建设。就连已经退居二线的<a href="https://tonybai.com/2023/12/11/simplicity/">Go语言之父Rob Pike</a>也亲自“现身说法”，在年底的GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。</p>
<p>大神回顾一生，我们盘点一年。在这篇文章中，我就和大家一起聊聊Go在2023年的状态、所处的位置以及Go未来演进的机制与策略。</p>
<h2>1. Go的2023</h2>
<h3>1.1 稳</h3>
<p>一如往年，Go在2023年发布了两个大版本，分别是2023年2月份的<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20/">Go 1.20</a>和8月份的<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21</a>。</p>
<p>在这两个版本中，Go语法特性一如既往的求稳，除了支持<strong>切片类型到数组类型(或数组类型的指针)的类型转换</strong>，其余更是像语法的修修补补，比如：comparable“放宽”了对泛型实参的限制、unsafe包继续添加“语法糖”、增加min、max和clear预定义函数、增强type inference能力等。</p>
<p>这些并不会让Gopher感到“意外”，因为这与<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">Russ Cox在2022年宣称的“Go is boring”</a>的精神是一脉相承的。</p>
<p>不过，除了Go语法特性变化方面的“寡淡”之外，Go在其他方面还是求新和求变的，接下来我们先来看看Go是如何求新的。</p>
<blockquote>
<p>注：求新与求变可能存在交集的地方，边界可能也有一定模糊性，也存在相互促进的情况，希望大家阅读下面内容时不要吹毛求疵:)。</p>
</blockquote>
<h3>1.2 求新</h3>
<p>Go在语法特性求稳的同时，在编译器、工具链、运行时以及标准库等方面都在努力优化和打磨，旨在进一步提升Go兼具的生产力与运行时效率，其中很多优化和打磨的措施不乏新颖。</p>
<p><a href="https://go.dev/blog/pgo-preview">Go 1.20版本中首次引入的PGO(profile-guided optimization)技术预览版</a>，到Go 1.21版本变为默认开启，Go官方给出的PGO优化的效果数据是：PGO优化带来的性能提升一般是2%~7%，而在<a href="https://tonybai.com/2023/12/25/go-1-22-foresight">最新的Go 1.22rc1</a>中，这个数字已经变为2%~14%了。</p>
<p>在内存管理方面，<a href="https://github.com/golang/go/issues/51317">Go 1.20引入了试验特性arena包</a>，虽然它没能在Go 1.21中按时转正，如今处于proposal-hold状态，但这也算是一次在内存管理机制上的求新。</p>
<p>Go是一门面向软件工程的编程语言，在这一年中，Go在软件工程领域的求新例子也是不少。比如：可用于大幅简化Go项目创建的<a href="https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool">gonew</a>工具，它支持基于go project template clone并创建一个属于你的Go项目；再比如<a href="https://go.dev/blog/integration-test-coverage">对应用执行时的代码覆盖率的采集</a>，可以帮助开发者更进一步了解最终可执行程序代码执行路径上的测试覆盖情况；而<a href="https://tonybai.com/2022/09/10/an-intro-of-govulncheck">govulncheck工具</a>则是Go在软件工程与<a href="https://tonybai.com/2022/03/14/software-supply-chain-security-in-go">供应链安全</a>领域的求新尝试，该工具丰富了我们对Go项目进行安全漏洞检查的手段。</p>
<blockquote>
<p>注：关于供应链安全问题，Russ Cox近期有一个专门的Talk：<a href="https://research.swtch.com/acmscored">Open Source Supply Chain Security at Google</a>，感兴趣的童鞋可以学习一下。</p>
</blockquote>
<p>Go始终对IT界出现的新技术、新趋势以及Go社区的新想法保持open。在WASM出现早期，<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go就提供了对wasm的porting支持</a>，如今在Go 1.21中，Go还对尚未形成最终规范的<a href="https://go.dev/blog/wasi">WASI(WebAssembly System Interface)</a>提供了支持。</p>
<p>Go社区的反馈也是Go团队求新的来源，比如一个典型例子就是<a href="https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go">log/slog加入标准库</a>，让Go标准库原生支持了结构化日志输出，且日志性能不输<a href="https://tonybai.com/2021/07/14/uber-zap-advanced-usage">像zap这样的第三方开源log包</a>。</p>
<p>Go社区也跟随Go团队的节奏，走在求新的道路上。2023年，IT界最大的事件就是<strong>以ChatGPT为代表的大语言模型的横空出世</strong>，这很可能是一个百年不遇的、对人类文明进步有着重要里程碑意义的事件。各行各业，言必称大模型，言必称AI。在传统机器学习、深度学习以及<a href="https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations">神经网络</a>方面生态并不丰富的Go，也在尝试与大模型对接，比如：支持快速在本地启动和运行llama2、mistral 7B、codellama、vicuna等大模型的<a href="https://github.com/jmorganca/ollama">ollama开源项目</a>在短短几个月就收获近30k个小星星的关注；再比如Gemini大模型推出后，Google一并开源了支持与Google各种大模型项目对接的<a href="https://github.com/google/generative-ai-go">Google AI Go SDK开源项目</a>，并提供了详细的教程<a href="https://ai.google.dev/tutorials/go_quickstart?hl=zh-cn">指导Gopher如何通过该SDK与大模型交互</a>。</p>
<blockquote>
<p>注：Google把Gemini Pro的API免费提供给个人用户了，该模型具备GPT 3.5 级别的能力，32k 上下文，38 种语言支持以及多模态支持，唯一的约束是每分钟60个请求。</p>
</blockquote>
<p>在<a href="https://go.dev/blog/survey2023-h2-results">2023年第二次Go用户调查报告</a>中，Go 开发者表示，他们对改善其编写代码的质量、可靠性和性能的人工智能/机器学习工具感兴趣，而不是编写代码的工具。一位时刻警醒、从不忙碌的专家“审阅者”可能是一种更有帮助的AI开发者辅助形式。Go官方表示了对该调查结果的重视，也许在后续的Go工具链中“AI加持”会成为常态。</p>
<h3>1.3 求变</h3>
<p>2023年8月，在Go 1.21版本刚刚发布后，Go官博就发布了Russ Cox编写的两篇文章：《<a href="https://go.dev/blog/compat">Backward Compatibility, Go 1.21, and Go 2</a>》和《<a href="https://go.dev/blog/toolchain">Forward Compatibility and Toolchain Management in Go 1.21</a>》，进一步明确了Go承诺的向后兼容的范围和方案，并<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">第一次阐述了向前兼容性的具体方案</a>，这两篇文章为Go语言后续的“求变”奠定了理论基础。</p>
<p>在向后兼容方面，从Go 1.21开始Russ Cox提出一些举措，比如：Go将扩展和规范化了GODEBUG的使用，其大致思路如下：</p>
<ul>
<li>对于每个在Go1兼容性承诺范围内的且可能会破坏(break)现有代码的新特性/新改变(比如：panic(nil)语义的改变)加入时，Go会向GODEBUG设置<br />
中添加一个新选项(比如GODEBUG=panicnil=1)，以保留采用原语义进行编译的兼容能力；</li>
<li>GODEBUG中新增的选项将至少保留两年(4个Go release版本)，对于一些影响重大的GODEBUG选项(比如http2client和http2server)，保留的时间可能更长，甚至一直保留；</li>
<li>GODEBUG的选项设置与go.mod的go version是匹配的。例如，即便你现在的工具链是Go 1.21，如果go.mod中的go version为1.20，那么GODEBUG控制的新特性语义将不起作用，依旧保持Go 1.20时的行为。除非你将go.mod中的go version升级为go 1.21.0；</li>
<li>在Go 1.21及以后版本中，除了可以使用像GODEBUG=panicnil=1的环境变量恢复原先语义外，还可以在main包中使用//go:debug指示符。</li>
</ul>
<p>在向前兼容方面，Russ Cox提出的方案有些复杂难懂，这里就不赘述了，感兴趣的童鞋可以阅读一下我之前的文章《<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">聊聊Go语言的向前兼容性和toolchain规则</a>》了解更多细节。</p>
<h4>1.3.1 语法填坑</h4>
<p>在Go的诸多“求变”中，影响最大的还是对已有语法坑的“修正”，这些“填坑”工作或多或少都会对存量代码带去影响，甚至是break change，Go社区的反对声音也是不少。但无论怎样，这些工作已经在Go 1.21版本拉开帷幕了。比如：改变panic(nil)的语义以及对<a href="https://go.dev/blog/loopvar-preview">循环变量语义的变更</a>，大家可以在《<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">Go 1.21中值得关注的几个变化</a>》一文中了解更多细节。</p>
<p>对现有语法坑的修正也进一步促进了“求新”，比如在修正loopvar语义的同时，for range支持对更多类型表达式的迭代也在进行中，比如Go 1.22中，<a href="https://tonybai.com/2023/12/25/go-1-22-foresight/">for range将支持迭代整型表达式</a>，并以试验特性提供了对函数迭代器的支持。</p>
<h4>1.3.2 标准库v2示范</h4>
<p>Go号称是“自带电池”的语言，其高质量的标准库得到了广大Gopher的欢迎。Go团队也一直努力推进Go标准库功能的丰富性，比如：Go 1.22中对http.ServeMux功能进行了增强，使其像第三方的gorilla/mux那样增加对带有通配符路由的匹配。</p>
<p><a href="https://tonybai.com/2023/12/25/go-1-22-foresight/">Go 1.22中，标准库还首次出现了v2版本包：math/rand/v2</a>，这为后续标准库的vN方式演进提供了示范，从Go团队的官方issue、discussion中了解到，后续如sync/v2、encoding/json/v2等已经列上日程了。</p>
<h2>2. Go所处的位置</h2>
<p>很多人关注Go当前的状态：国内大厂用的多么？小厂是不是也在广泛采纳。这些问题我在往年的Go语言盘点时也都做过梳理，今年就不再提了。没有哪个大厂在广泛采用一门语言后，会在一年内全部推翻重写的；小厂对Go的采纳也是有惯性的。</p>
<p>今年先从我的两个意外“收获”开始。</p>
<h3>2.1 两个意外的“收获”</h3>
<p>2023年10月中旬，世界知名电动车厂商Tesla发布了<a href="https://developer.tesla.com/docs/fleet-api">新版fleet API</a>和<a href="https://github.com/teslamotors/vehicle-command">vehicle command SDK</a>，鉴于本人也在智能网联汽车行业内打拼，于是对Tesla的此次发布做了一些深入了解。在Tesla的github主页上我赫然发现：Go是目前Tesla开源项目的第二大语言。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-3.png" alt="" /></p>
<p>相对于传统的主机厂(车厂)，Telsa算是比较开放的了。开放包含两个含义，一是将车端能力的开放，二是项目的开源。就目前了解到，国内主机厂还鲜有将车端能力开放出来的，开源就更是鲜见。但Tesla在这两方面都做到了，既开放了车端API，又做了针对性的开源，虽然目前其开源项目并不多。以前Tesla涉及到云端服务的项目多用<a href="">Ruby</a>，但从2022年开始，Go语言的使用逐渐增多，包括前面提到的Fleet API的<a href="https://github.com/teslamotors/fleet-telemetry">Fleet Telemetry的参考server实现</a>以及<a href="https://github.com/teslamotors/vehicle-command">Tesla车辆远控SDK</a>。</p>
<p>我们再来看看Apache基金会。众所周知，Apache基金会的开源项目多以Java语言为主，但一次偶然的机会翻看Apache基金会的github项目主页，我发现Go语言在Apache开源项目中已经悄悄地跻身到第五名，如果仅算后端语言的话，Go排名第三，仅次于Java和Python。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-4.png" alt="" /></p>
<p>并且，Apache基金会下面的Go项目实际也不少，大家可以通过https://github.com/orgs/apache/repositories?language=go&amp;type=all查询。其中还不乏优秀之作，比如：<a href="https://github.com/apache/incubator-answer">构建Q&amp;A知识系统的answer</a>、<a href="https://github.com/apache/dubbo-go">Apache Dubbo的go实现dubbo-go</a>、<a href="https://github.com/apache/trafficcontrol">CDN实现trafficcontrol</a>、<a href="https://github.com/apache/camel-k">Kubernetes原生的轻量级企业应用集成框架Camel K</a>、<a href="https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1">Apache Arrow的Go实现</a>以及<a href="https://github.com/apache/incubator-devlake">针对开发过程的聚合数据平台devlake</a>等。</p>
<p>我们知道：Apache项目在企业级应用和平台方面具有广泛的应用，从Go语言在Apache基金会项目中的使用比例的提升现象来看，Go在企业应用市场中的普及度和受欢迎程度确实有所增长。</p>
<h3>2.2 Go语言排名</h3>
<p>编程语言之间的竞争与争议，通常被称为“编程语言战争”(programming language war)，它其实反映了不同技术群体和范式之间的碰撞。这些“火药味”比较浓的语言之争通常比较主观。近10年来，业界出现了一些被广泛接受的编程语言排行榜，它们基于一些相对客观的数据来反映不同编程语言在现实开发中的真实状态。但不同编程语言排行榜都有不同的数据来源和数据模型，单一的排行榜往往是“盲人摸象”，无法反映全貌。但目前又没有一个可以让我们一窥全貌的权威排行榜。因此，要想更客观地、更全面的反映一门编程语言的实际情况，我们需要将多个排行榜参照着看。</p>
<p>下面我们就来看看在目前世界上著名的编程语言排行榜上，Go语言在其中的最新排名情况(请注意：各个榜单的发布时间不同，导致各榜单的数据会有一定时间差)。</p>
<h4>2.2.1 <a href="https://pypl.github.io/PYPL.html">PYPL编程语言排行榜</a></h4>
<p>PYPL编程语言流行指数是通过分析语言教程在谷歌上的搜索频率而创建的。语言教程被搜索的次数越多，说明该语言越受欢迎，原始数据来自Google Trends：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-5.png" alt="" /><br />
<center>PYPL编程语言排行榜，数据时间：2023.12</center></p>
<h4>2.2.2 <a href="https://spectrum.ieee.org/the-top-programming-languages-2023">IEEE Spectrum排行榜</a></h4>
<p>IEEE Spectrum排行榜是通过调查来自全球软件工程师和招聘网站的数据，统计各语言的流行度的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-6.png" alt="" /><br />
<center>IEEE Spectrum排行榜，数据时间：2023.8</center></p>
<h4>2.2.3 <a href="https://redmonk.com/sogrady/2023/05/16/language-rankings-1-23/">RedMonk编程语言排行榜</a></h4>
<p>RedMonk排行榜是根据GitHub和Stack Overflow这两个开发者社区上的讨论数量来推算语言的受关注度。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-7.png" alt="" /><br />
<center>RedMonk编程语言排行榜，数据时间：2023.5</center></p>
<h4>2.3.4 <a href="https://github.blog/2023-11-08-the-state-of-open-source-and-ai/">Github Octoverse</a></h4>
<p>GitHub Octoverse排行榜直观反映了过去一年GitHub上各编程语言的实际使用和流行趋势，是从开源项目量的维度来衡量编程语言活跃度的。在Top 10语言榜单上，2023年Go超越Ruby第一次跻身Github Top10语言：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-8.png" alt="" /><br />
<center>Github Octoverse编程语言排行榜，数据时间：2023.11</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-9.png" alt="" /><br />
<center>Github Octoverse编程语言排行榜，数据时间：2023.11</center></p>
<h4>2.3.5 <a href="https://madnight.github.io/githut/">Github Language Stats(githut)</a></h4>
<p>Github Language Stats是一个个人项目，它基于github公开数据，按时间、pr数量、star数量等维度对各个语言在github上的使用情况进行分析：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-10.png" alt="" /><br />
<center>Githut按PR数量，数据时间：2023第三季度</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-11.png" alt="" /><br />
<center>Githut按Star数量，数据时间：2023第三季度</center></p>
<h4>2.3.6 <a href="https://www.tiobe.com/tiobe-index/">TIOBE编程语言排行榜</a></h4>
<p>TIOBE编程语言排行榜理论上来说，是世界上最知名的编程语言排行榜，它根据各大搜索引擎编程语言相关的搜索查询量来计算一个综合指数。但这些年TIOBE榜单数据的“上蹿下跳”，让开发者对该榜单是“又爱又恨”。下面是TIOBE index 2023年12月份的榜单：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-2023-review-of-go-programming-language-12.png" alt="" /></p>
<p>当你看到Fortran排在Go的前面，你就get到该榜单的抽风式的“不靠谱”了:)。</p>
<p>综合上述6个榜单，我们可以看到Go语言的2023基本处于稳定发展状态，没有“大踏步”的前进，也没有意想不到的大幅退步。</p>
<p>今年在国内某乎上总有一些有关“Go在国内是否已凉”的话题，从上面实际情况来看，话题中那些抹黑Go的观点可以不攻自破了。有人会说Rust的强势上升对Go会有一定冲击，这的确不可否认，就像Go当年火速蹿升给Java带去一定冲击一样，这是一门编程语言在演进阶段必会经历的过程，没有什么值得大惊小怪的。5年后，Rust可能同样也会受到来自其他语言的冲击。</p>
<p>Go语言未来会变得如何，关键还要看Go团队对Go未来演进方向的把握是否得当以及Go社区与生态是否给力。2023年，Go团队也明确了未来的演进机制和策略，接下来我们就来看看。</p>
<h2>3. Go的未来演进</h2>
<p>2023年是<a href="https://tonybai.com/2023/11/11/go-opensource-14-years/">Go语言开源的第14个年头</a>，Go语言早已蜕下了少年期的青涩，进入到了青年期。这意味着它拥有了越来越成熟稳定的语言特性，同时生态系统也日益丰富完善。作为一门青壮年语言，Go语言在系统设计方面展现出的高度工程化思想，使其轻松应对复杂系统的构建。以go module为主的模块化支持帮助大规模程序更加清晰化，丰富的并发控制手段使其可以处理海量请求。与此同时，Go语言生态也在蓬勃成长——各种高质量框架应运而生，无数module可复用，大量的云原生组件可供选择。这为开发者极大减轻了从零开始搭建系统的工作量。</p>
<p>和我们人类一样，一门语言进入青年期后的成熟特征并不能完全掩饰其未来演进的迷茫！在Ken Thompson、Rob Pike相继退休后，Russ Cox成为了Go这艘大船的“掌舵者”，Russ Cox与Go团队对编程语言的思考，对Go语言价值观的判断将直接决定Go未来的航向。</p>
<p>好在，在2023年的GopherCon大会上，我们得到了Russ Cox的答案：那就是<a href="https://tonybai.com/2023/12/10/go-changes/">基于共同目标和数据驱动的决策</a>。这里借用Russ Cox在演讲中给出的结论来看看具体的演进驱动机制：</p>
<ul>
<li>首先，Go需要不断变化，特别是随着计算世界的变化。</li>
<li>其次，任何改变的目标都是为了使Go在软件工程中变得更好，尤其是在规模化(scaling)方面。</li>
<li>第三，一旦我们确定了目标，达成共识的下一个最重要的部分是拥有共享数据来做出决策。</li>
<li>第四，Go工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。</li>
</ul>
<p>综上来看，Go团队要“拥抱变化”，但不能“无头苍蝇”一样的胡乱改变，而是严谨地基于广泛的数据反馈，包括来自用户调查、vscode插件运行的用户反馈、全年进行的研究访谈和用户体验研究等，以及来自即将<a href="https://research.swtch.com/telemetry">加入Go工具链的可选遥测(opt-in Telemetry)功能</a>获取到的更多真实的Go使用数据。</p>
<p>相信在Go工具链的可选遥测(opt-in Telemetry)功能加入后，Go团队能基于这些用户数据拿到更准确地决策依据，继续让Go这艘大船行驶在正确、光明的航向上！</p>
<h2>4. 小结</h2>
<p>在2023年，Go语言继续保持了其稳定性和可靠性的特点。发布了两个大版本，Go 1.20和Go 1.21，其中语法特性的改变相对较少，注重修复和优化。然而，Go语言在其他方面仍然保持着求新和求变的态势。</p>
<p>Go语言团队致力于优化编译器、工具链、运行时和标准库，以提升生产力和运行时效率。引入了一些新的特性和优化措施，例如PGO（profile-guided optimization）技术的引入和优化、内存管理方面的改进等。同时，Go语言在软件工程领域也进行了一些创新，如简化项目创建的gonew工具、代码覆盖率的采集工具、供应链安全领域的govulncheck工具等。</p>
<p>Go语言始终保持对新技术、新趋势和社区的开放姿态。在2023年，Go语言对WASM和WASI的支持得到了进一步加强。同时，Go社区也积极响应并跟随Go团队的步伐，面对IT界出现的大语言模型等新兴技术，Go社区也在不断探索和应用。</p>
<p>总体而言，2023年对于Go语言来说是一个稳中求新、稳中求变的年份。Go语言保持着其简洁、高效和易用的特点，同时积极适应和采纳新的技术和需求，为开发者提供更好的开发体验和工具支持。</p>
<p>展望未来，Go团队已经明确了更加以共识和用户数据为驱动的演进机制，保证Go的发展方向与实际需求保持同步。随着可选的工具链遥测功能加入，相信他们能基于更丰富的用户数据做出更正确、更具预见性的正确决策。</p>
<p>我个人依旧坚持我之前的判断：<strong>Go将进入或已处于自己的黄金5-10年</strong>。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/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://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>
</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/12/31/the-2023-review-of-go-programming-language/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
