<?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; Unittest</title>
	<atom:link href="http://tonybai.com/tag/unittest/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 12 Apr 2026 22:30:28 +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>“为什么很多工程师还在无视 AI 编程？”—— 这里的答案，或许决定了你三年后的身价</title>
		<link>https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming/</link>
		<comments>https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming/#comments</comments>
		<pubDate>Mon, 29 Dec 2025 06:08:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AINativeDevelopmentWorkflow]]></category>
		<category><![CDATA[AIProgramming]]></category>
		<category><![CDATA[AI原生开发工作流]]></category>
		<category><![CDATA[AI编程]]></category>
		<category><![CDATA[ArchitecturalDecision]]></category>
		<category><![CDATA[BoilerplateCode]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[Cursor]]></category>
		<category><![CDATA[EarlyAdopter]]></category>
		<category><![CDATA[Freelancer]]></category>
		<category><![CDATA[GitHubCopilot]]></category>
		<category><![CDATA[LegacyCode]]></category>
		<category><![CDATA[MicroservicesArchitecture]]></category>
		<category><![CDATA[Productivity]]></category>
		<category><![CDATA[SecurityandCompliance]]></category>
		<category><![CDATA[SoftwareEngineer]]></category>
		<category><![CDATA[SystemDesign]]></category>
		<category><![CDATA[TrustCrisis]]></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>
		<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=5620</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming 大家好，我是Tony Bai。 “我注意到一件让我非常惊讶的事：似乎大多数软件工程师并没有充分利用（甚至根本不用）像 Claude Code、Cursor 或 GitHub Copilot 这样的 AI 编程工具。 我所在的自由职业者社区里，每个人都在疯狂压榨这些工具的极限，生产力飙升。但当我和传统公司的工程师聊天时，画风完全不同。大多数人几乎不用 AI，公司文化也不支持。 自由职业者/早期采用者与普通大厂员工之间，似乎出现了一道巨大的鸿沟。” 近日，Reddit 上的一篇热帖，再次引爆了关于“AI 编程”的讨论。显然，这不仅是一个技术问题，更是一场关于职业生存、工程伦理与未来选择的深刻辩论。 为什么在 AI 席卷全球的今天，仍有大量工程师选择“无视”甚至“抵制”它？这背后的原因，远比“懒惰”或“守旧”要复杂得多。 信任危机：“它写得很快，但错得离谱” 对于许多资深工程师来说，拒绝 AI 的首要原因不是“傲慢”，而是恐惧——对不可控代码的恐惧。 一位 20 年经验的老兵在高赞评论中写道： “AI 工具既棒极了又糟透了。它们能飞快地生成代码，但也会以一种极具想象力或极其隐蔽的方式破坏整个系统，让你花上几个小时去修补。” 这道出了无数人的心声。自己写的代码，就算有 Bug，你也知道逻辑脉络；而 AI 生成的代码，虽然看着像模像样，但你不仅要理解它，还要审查它是否引入了安全漏洞、性能陷阱或是荒谬的幻觉。 “如果我花了 80% 的时间在构思，20% 的时间在写代码。AI 颠倒了这个过程，但我那 80% 的时间变成了帮 AI 擦屁股。” 一位开发者如是说。 环境的枷锁：大厂的围墙 vs. 荒野的求生 帖主观察到的“鸿沟”，其实是生存环境的差异。 自由职业者/创业者：他们是荒野猎人。每一分钟的节省都直接转化为收入。他们往往处理的是从 0 到 1 的新项目，没有历史包袱。AI [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/why-many-software-engineers-still-ignore-ai-programming-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming">本文永久链接</a> &#8211; https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming</p>
<p>大家好，我是Tony Bai。</p>
<blockquote>
<p>“我注意到一件让我非常惊讶的事：似乎大多数软件工程师并没有充分利用（甚至根本不用）像 Claude Code、Cursor 或 GitHub Copilot 这样的 AI 编程工具。</p>
<p>我所在的自由职业者社区里，每个人都在疯狂压榨这些工具的极限，生产力飙升。但当我和传统公司的工程师聊天时，画风完全不同。大多数人几乎不用 AI，公司文化也不支持。</p>
<p><strong>自由职业者/早期采用者与普通大厂员工之间，似乎出现了一道巨大的鸿沟。</strong>”</p>
</blockquote>
<p>近日，Reddit 上的<a href="https://www.reddit.com/r/ClaudeAI/comments/1ot9b8n/why_are_so_many_software_engineers_still_ignoring/">一篇热帖</a>，再次引爆了关于“AI 编程”的讨论。显然，这不仅是一个技术问题，更是一场关于职业生存、工程伦理与未来选择的深刻辩论。</p>
<p>为什么在 AI 席卷全球的今天，仍有大量工程师选择“无视”甚至“抵制”它？这背后的原因，远比“懒惰”或“守旧”要复杂得多。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/ai-app-dev-primer-qr.png" alt="img{512x368}" /></p>
<h2>信任危机：“它写得很快，但错得离谱”</h2>
<p>对于许多资深工程师来说，拒绝 AI 的首要原因不是“傲慢”，而是<strong>恐惧</strong>——对不可控代码的恐惧。</p>
<p>一位 20 年经验的老兵在高赞评论中写道：</p>
<blockquote>
<p>“AI 工具既棒极了又糟透了。它们能飞快地生成代码，但也会以一种<strong>极具想象力或极其隐蔽的方式</strong>破坏整个系统，让你花上几个小时去修补。”</p>
</blockquote>
<p>这道出了无数人的心声。自己写的代码，就算有 Bug，你也知道逻辑脉络；而 AI 生成的代码，虽然看着像模像样，但你不仅要理解它，还要审查它是否引入了安全漏洞、性能陷阱或是荒谬的幻觉。</p>
<p>“如果我花了 80% 的时间在构思，20% 的时间在写代码。AI 颠倒了这个过程，但我那 80% 的时间变成了<strong>帮 AI 擦屁股</strong>。” 一位开发者如是说。</p>
<h2>环境的枷锁：大厂的围墙 vs. 荒野的求生</h2>
<p>帖主观察到的“鸿沟”，其实是<a href="https://tonybai.com/2025/08/06/blitzkrieg-vs-attrition-in-ai-age/"><strong>生存环境</strong>的差异</a>。</p>
<ul>
<li><strong>自由职业者/创业者</strong>：他们是荒野猎人。每一分钟的节省都直接转化为收入。他们往往处理的是从 0 到 1 的新项目，没有历史包袱。AI 在这种场景下是神兵利器，能让他们以一当十。</li>
<li><strong>大厂员工</strong>：他们是城堡守卫。面对的是数百万行、有着 10 年甚至更长历史的“屎山”代码。这里充满了复杂的业务逻辑、诡异的依赖关系和严苛的安全合规要求。
<ul>
<li><strong>复杂的上下文</strong>：AI 很难理解一个庞大、老旧代码库的全部上下文。</li>
<li><strong>安全与合规</strong>：正如许多评论指出的，很多公司出于数据泄露的恐惧，直接封禁了 AI 工具，或者只允许使用“阉割版”或“内部部署的大模型”。</li>
<li><strong>激励机制</strong>：在大厂，多干活往往不意味着多拿钱，甚至可能因为引入了 AI 生成的 Bug 而背锅。既然工资照发，为什么要冒险去改变工作流？</li>
</ul>
</li>
</ul>
<p>一位开发者总结得精辟：“微服务架构、遗留代码和复杂的业务逻辑，是 AI 目前难以逾越的护城河。”</p>
<h2>技能的诅咒：新手狂欢，高手叹息？</h2>
<p>这里出现了一个有趣的“技能倒挂”现象。</p>
<ul>
<li><strong>初级开发者</strong>：往往对 AI 趋之若鹜。因为 AI 能帮他们写出自己原本写不出来的代码，填补了能力的空白。</li>
<li><strong>高级开发者</strong>：态度两极分化。
<ul>
<li><strong>抵制者</strong>：他们以此为荣，认为编程是一门精密的艺术，容不得 AI 的“大概差不多”。他们享受对每一行代码的掌控感。</li>
<li><strong>驾驭者</strong>：他们把 AI 当作“超级实习生”。他们不让 AI 做架构决策，只让它写单元测试、生成样板代码、转换数据格式。他们深知 AI 的局限，所以只在 AI 擅长的领域使用它。</li>
</ul>
</li>
</ul>
<p>正如评论所言：“<strong>用 AI 编程就像坐自动驾驶的车。新手觉得‘哇，车自己会动！’，老司机则时刻把手放在方向盘上，因为他知道这玩意儿随时可能把车开进沟里。</strong>”</p>
<h2>未来的分岔路：你是工匠，还是操作员？</h2>
<p>这场讨论最终指向了一个终极问题：<strong>软件工程师的未来是什么？</strong></p>
<p>有人悲观：“这就像当年会计师抵制 Excel 一样。拒绝工具的人，最终会被淘汰。”<br />
有人乐观：“AI 将消灭平庸的‘代码搬运工’，但会放大真正懂得系统设计、能解决复杂问题的工程师的价值。”</p>
<p>无论你属于哪个阵营，一个趋势是不可逆转的：<strong>编码（Coding）本身的门槛正在降低，但工程（Engineering）的门槛并未改变，甚至在提高。</strong></p>
<p>未来的工程师，可能分为两类：</p>
<ol>
<li><strong>AI 操纵者</strong>：利用 AI 快速交付产品，关注的是“结果”而非“过程”。</li>
<li><strong>系统守望者</strong>：负责审查 AI 的产出，解决 AI 无法处理的极端边界情况，维护系统的架构与安全。</li>
</ol>
<h2>小结：打破“傲慢与偏见”</h2>
<p>回到最初的问题：“为什么很多人无视 AI？”</p>
<ul>
<li>也许不是无视，而是<strong>审慎</strong>。</li>
<li>也许不是傲慢，而是<strong>负责</strong>。</li>
<li>也许不是懒惰，而是<strong>受限</strong>。</li>
</ul>
<p>但对于我们每一个个体而言，最危险的态度是<strong>“傲慢的无视”</strong>。你可以因为安全原因不用，可以因为质量原因少用，但绝不能因为“看不起”而不去了解。</p>
<p><strong>去试一试吧。</strong> 不要只用它写 Hello World，试着让它重构一个函数，写一个测试，解释一段晦涩的代码。了解它的上限，摸清它的下限。</p>
<p>因为在不久的将来，评价一个工程师的标准，或许不再是你写代码有多快，而是<strong>你能多好地驾驭这个不知疲倦、偶尔发疯、但潜力无限的“硅基队友”。</strong></p>
<p>资料链接：https://www.reddit.com/r/ClaudeAI/comments/1ot9b8n/why_are_so_many_software_engineers_still_ignoring/</p>
<hr />
<p><strong>你属于哪一类？</strong></p>
<p>在AI浪潮面前，你觉得自己更像是一个在荒野中狂奔的“猎人”，还是在城堡中坚守的“守卫”？你所在的团队对AI编程持什么态度？</p>
<p>欢迎在评论区分享你的真实处境和思考！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>跨越20年的对话：从 Eiffel 的“契约”到 Go 的“接口”</title>
		<link>https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface/</link>
		<comments>https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface/#comments</comments>
		<pubDate>Fri, 12 Dec 2025 23:34:43 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BertrandMeyer]]></category>
		<category><![CDATA[buildtags]]></category>
		<category><![CDATA[DbC]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[DesignbyContract]]></category>
		<category><![CDATA[DuckTyping]]></category>
		<category><![CDATA[Eiffel]]></category>
		<category><![CDATA[Encapsulation]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[failfast]]></category>
		<category><![CDATA[fuzzing]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Invariants]]></category>
		<category><![CDATA[LiskovSubstitutionPrinciple]]></category>
		<category><![CDATA[ObjectOrientedSoftwareConstruction]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Postconditions]]></category>
		<category><![CDATA[Preconditions]]></category>
		<category><![CDATA[RaceCondition]]></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>
		<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=5524</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface 大家好，我是Tony Bai。 20年前，当我第一次翻开 Bertrand Meyer 的那本巨著《面向对象软件构造》(Object-Oriented Software Construction) 时，一种醍醐灌顶的感觉油然而生。书中那个名为 Eiffel 的语言，以及它所倡导的 “契约式设计” (Design by Contract, DbC)，仿佛为当时混乱的软件开发世界点亮了一盏明灯。 虽然 Eiffel 语言最终并未像 Java 或 C++ 那样统治世界，但它留下的思想遗产——前置条件、后置条件、不变量——却潜移默化地渗透进了现代软件工程的骨髓。 时光流转，当我们站在云原生时代的潮头，手握 Go 语言 这把利器时，你是否意识到：Go 的接口 (Interface) 设计，其实是一场跨越 20 年的、对契约精神的现代演绎与致敬。 今天，让我们重温经典，看看那些曾被奉为圭臬的“契约”，是如何在 Go 的代码世界里重生的。 什么是“契约”？—— 软件世界的商业法则 在人类社会中，商业活动的基石是合同（契约）。甲方（Client）和乙方（Supplier）通过一纸文书，明确了彼此的权利与义务。 Bertrand Meyer 的天才之处，在于他将这种商业隐喻完美地移植到了软件模块的交互中。他认为，软件的高可靠性不能靠“运气”或“防御性编程的堆砌”，而应靠明确定义的契约。 Eiffel 语言直接将这种契约内置到了语法层面，形成了著名的“三驾马车”： 前置条件 (Preconditions / require) 定义：在调用函数之前，调用方 (Client) 必须确保为真的条件。 商业隐喻：你要坐飞机（调用服务），必须先买票且准时到达（满足前置条件）。如果没买票，航空公司（服务方）有权拒绝服务。 后置条件 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/from-eiffel-contract-to-go-interface-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface">本文永久链接</a> &#8211; https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface</p>
<p>大家好，我是Tony Bai。</p>
<p>20年前，当我第一次翻开 Bertrand Meyer 的那本巨著<strong>《<a href="https://book.douban.com/subject/1547078/">面向对象软件构造</a>》(Object-Oriented Software Construction)</strong> 时，一种醍醐灌顶的感觉油然而生。书中那个名为 <strong>Eiffel</strong> 的语言，以及它所倡导的 <strong>“契约式设计” (Design by Contract, DbC)</strong>，仿佛为当时混乱的软件开发世界点亮了一盏明灯。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/from-eiffel-contract-to-go-interface-2.png" alt="" /></p>
<p>虽然 Eiffel 语言最终并未像 Java 或 C++ 那样统治世界，但它留下的思想遗产——前置条件、后置条件、不变量——却潜移默化地渗透进了现代软件工程的骨髓。</p>
<p>时光流转，当我们站在云原生时代的潮头，手握 <strong>Go 语言</strong> 这把利器时，你是否意识到：<strong>Go 的接口 (Interface) 设计，其实是一场跨越 20 年的、对契约精神的现代演绎与致敬。</strong></p>
<p>今天，让我们重温经典，看看那些曾被奉为圭臬的“契约”，是如何在 Go 的代码世界里重生的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/api-design-pattern-and-implementation-qr.png" alt="" /></p>
<h2>什么是“契约”？—— 软件世界的商业法则</h2>
<p>在人类社会中，商业活动的基石是<strong>合同（契约）</strong>。甲方（Client）和乙方（Supplier）通过一纸文书，明确了彼此的<strong>权利</strong>与<strong>义务</strong>。</p>
<p>Bertrand Meyer 的天才之处，在于他将这种商业隐喻完美地移植到了软件模块的交互中。他认为，软件的高可靠性不能靠“运气”或“防御性编程的堆砌”，而应靠<strong>明确定义的契约</strong>。</p>
<p>Eiffel 语言直接将这种契约内置到了语法层面，形成了著名的<strong>“三驾马车”</strong>：</p>
<ol>
<li>
<p><strong>前置条件 (Preconditions / require)</strong></p>
<ul>
<li><strong>定义</strong>：在调用函数之前，<strong>调用方 (Client)</strong> 必须确保为真的条件。</li>
<li><em>商业隐喻</em>：你要坐飞机（调用服务），必须先买票且准时到达（满足前置条件）。如果没买票，航空公司（服务方）有权拒绝服务。</li>
</ul>
</li>
<li>
<p><strong>后置条件 (Postconditions / ensure)</strong></p>
<ul>
<li><strong>定义</strong>：在函数执行之后，<strong>服务方 (Supplier)</strong> 承诺必须为真的条件。</li>
<li><em>商业隐喻</em>：只要你买了票且准时登机，航空公司必须把你安全送到目的地（满足后置条件）。</li>
</ul>
</li>
<li>
<p><strong>不变量 (Invariants / invariant)</strong></p>
<ul>
<li><strong>定义</strong>：在对象的整个生命周期中（所有公开方法调用前后），始终保持为真的“真理”。</li>
<li><em>商业隐喻</em>：无论飞机怎么飞，乘客数量绝不能超过座位数。</li>
</ul>
</li>
</ol>
<p><strong>“契约”的核心价值在于信任</strong>：如果每个人都遵守契约，我们就不需要在每一行代码里都写那种偏执的 if (x != null) 检查。代码将变得更干净、更高效、更健壮。</p>
<p>为了让你直观感受这种思想的冲击力，让我们看一段 <strong>Eiffel</strong> 代码。这是一个简单的字典（Dictionary）插入操作，请注意看它是如何用 require、ensure 和 invariant 将逻辑严丝合缝地包裹起来的：</p>
<pre><code class="eiffel">class DICTIONARY [ELEMENT]

feature
    count: INTEGER
    capacity: INTEGER

    put (x: ELEMENT; key: STRING) is
        -- 将元素 x 插入字典，通过 key 检索
        require
            -- [前置条件]：调用者的责任
            not_full: count &lt; capacity
            key_not_empty: not key.empty
        do
            -- ... 这里是具体的插入算法实现 ...
            -- ... 真正的业务逻辑代码 ...
        ensure
            -- [后置条件]：实现者的承诺
            element_added: has (x)
            key_associated: item (key) = x
            count_increased: count = old count + 1
        end

invariant
    -- [不变量]：始终为真的真理
    consistent_count: 0 &lt;= count and count &lt;= capacity

end
</code></pre>
<blockquote>
<p>注：对于不熟悉 Eiffel 语法的同学，其实只需关注四个关键词：require 是对入参的“资格审查”，do 是干活的“核心逻辑”，ensure 是对结果的“质量验收”，而 invariant 则是贯穿始终的“宪法”。</p>
</blockquote>
<p>看到这里，你是否感受到了一种秩序之美？</p>
<p>这段代码不仅仅是在“写程序”，它是在<strong>立法</strong>。require 明确了“什么情况下可以调”，ensure 明确了“调用后会发生什么”，而 invariant 则像定海神针一样稳住了对象的状态。</p>
<p><strong>“契约”的核心价值在于信任</strong>：如果每个人都遵守契约，我们就不需要在每一行代码里都写那种偏执的 if (x != null) 检查。代码将变得更干净、更高效、更健壮。</p>
<h2>Go 接口 —— 契约的“鸭子类型”演绎</h2>
<p>Eiffel 选择了<strong>显式</strong>的、强硬的语法来强制契约；而 Go 语言，则选择了一种更为<strong>隐式</strong>、灵活，但也更具工程智慧的方式——<strong>接口 (Interface)</strong>。下面表格直观地展示了在契约这个概念上，Eiffel实现方式与Go的演绎方式上的方式：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/from-eiffel-contract-to-go-interface-3.png" alt="" /></p>
<p>下面我们再具体说一下。</p>
<h3>行为即契约</h3>
<p>Go 的接口设计哲学是：“如果它走起路来像鸭子，叫起来像鸭子，那它就是鸭子。”</p>
<p>在 Go 中，我们不关心一个类型“是谁”（继承了哪个父类），我们只关心它“承诺能做什么”。<strong>这种承诺，就是契约。</strong></p>
<p>以标准库中最经典的 io.Reader 为例：</p>
<pre><code class="go">type Reader interface {
    Read(p []byte) (n int, err error)
}
</code></pre>
<p>这短短三行代码，实际上定义了一个极其强大的契约：</p>
<ul>
<li><strong>前置条件（隐式）</strong>：你需要给我一个切片 p。</li>
<li><strong>后置条件（隐式）</strong>：我会尝试读取数据填入 p，并返回读取的字节数 n 和可能发生的错误 err。如果 n > 0，则 p[0:n] 包含了有效数据。</li>
</ul>
<p>任何一个结构体，无论是 os.File、net.Conn 还是 bytes.Buffer，只要它<strong>签署</strong>（实现）了这个契约，就可以被无缝地替换和复用。这正是 DbC(Design by Contract) 理论中 <strong>Liskov 替换原则</strong> 在 Go 语言中的完美落地。</p>
<h3>强类型的约束</h3>
<p>虽然 Go 没有 require 关键字，但它利用<strong>强类型系统</strong>实施了最基础的契约检查。</p>
<p>在动态语言中，你可能需要写代码检查参数是否为数字。但在 Go 中，如果函数签名是 func Sqrt(x float64)，编译器就是你的契约执行官——它保证了绝不会有字符串类型的“非法移民”混入函数内部。</p>
<h2>在 Go 中实践“契约精神”</h2>
<p>在尝试将 DbC 落地到 Go 语言时，我们必须首先承认一个事实：<strong><a href="https://tonybai.com/2023/03/12/is-go-object-oriented">Go 并非传统的面向对象语言</a>。</strong></p>
<p>Eiffel 是建立在类（Class）和继承（Inheritance）之上的。它的 invariant 依赖于类的状态封闭性，它的 require 和 ensure 依赖于方法重写时的“契约继承”规则（Liskov 替换原则的严格形式）。</p>
<p>而 Go 是基于<strong>组合</strong>和<strong>接口</strong>的。我们没有“类”，只有结构体；我们没有“继承”，只有嵌入。这种范式上的根本差异，注定了我们无法在 Go 中获得 Eiffel 那种“原生级”的契约支持，任何试图在语法层面 1:1 还原 Eiffel 的尝试，都会显得格格不入且笨拙。</p>
<p>但这并不意味着我们可以抛弃 DbC 的思想。相反，一个优秀的 Gopher，应当学会<strong>“神似而形不似”</strong>——利用 Go 的原生特性（Panic, Error, Defer, Testing），手动“编织”出健壮的契约网。</p>
<h3>捍卫前置条件：Panic 还是 Error？</h3>
<p>在 Go 中执行前置条件检查，通常有两种流派：</p>
<ul>
<li>针对编程错误（Bug）—— 使用 panic</li>
</ul>
<p>如果调用者违反了API的<strong>基本使用协议</strong>（例如，传入了一个 nil 的上下文，或者索引越界），这通常意味着调用方代码有 Bug。此时，快速失败（Fail Fast）是最好的选择。</p>
<pre><code class="go">func MustRegister(handler Handler) {
    if handler == nil {
        panic("http: nil handler") // 显式的前置条件检查
    }
    // ...
}
</code></pre>
<ul>
<li>针对运行时错误 —— 返回 error</li>
</ul>
<p>如果前置条件依赖于外部世界（如网络是否连通、文件是否存在），则应返回 error，让调用方决定如何处理。</p>
<h3>验证后置条件：Defer 与测试</h3>
<p>Eiffel 的 ensure 可以在运行时自动检查。在 Go 中，我们可以利用 defer 甚至构建标签（Build Tags）来模拟这种行为，特别是在调试模式下。</p>
<pre><code class="go">// 仅在调试构建中启用的断言逻辑
func (s *Stack) Push(item int) {
    if debug {
        // 捕获旧状态
        oldSize := s.size
        defer func() {
            // 验证后置条件
            if s.size != oldSize + 1 {
                panic("invariant violated: stack size did not increment")
            }
        }()
    }
    // ... 业务逻辑 ...
}
</code></pre>
<p>但更 Go Style 的做法是：<strong>将后置条件的验证移交给单元测试（Unit Test）和模糊测试（Fuzzing）</strong>。Go 强大的测试工具链，本质上就是一个外挂的“契约验证器”。</p>
<h3>守护不变量：“构造函数”与封装</h3>
<p>如何保证对象始终处于合法状态（不变量）？Go 给出的答案是：<strong>封装（Encapsulation）</strong>。</p>
<p>通过将结构体的字段设为私有（小写字母开头），并强制用户通过 New&#8230; 工厂函数来创建对象，我们可以确保对象在<strong>出生那一刻</strong>就是满足不变量的，并且在后续的生命周期中，外部无法破坏它。</p>
<pre><code class="go">package stack

type Stack struct {
    items []int // 私有，外部无法直接修改，保证了数据的安全性
}

// 工厂函数：保证初始状态的不变量
func New() *Stack {
    return &amp;Stack{items: make([]int, 0)}
}
</code></pre>
<h2>示例 —— 一个“契约式”的栈</h2>
<p>让我们把上述思想综合起来，写一个简单的、充满“契约精神”的栈。</p>
<pre><code class="go">package stack

import "errors"

// StackInterface 定义了行为契约
type StackInterface interface {
    Push(v int) error
    Pop() (int, error)
    Size() int
}

type Stack struct {
    items []int
    cap   int
}

// New 创建栈，同时确立初始不变量
func New(capacity int) *Stack {
    if capacity &lt;= 0 { // 前置条件检查
        panic("capacity must be positive")
    }
    return &amp;Stack{
        items: make([]int, 0, capacity),
        cap:   capacity,
    }
}

func (s *Stack) Push(v int) error {
    // 前置条件：栈未满
    if len(s.items) &gt;= s.cap {
        return errors.New("stack overflow")
    }

    s.items = append(s.items, v)

    // 后置条件（隐式）：len 增加了 1，且栈顶元素是 v
    // 在 Go 中，我们通常信任代码逻辑，或通过测试覆盖此条件
    return nil
}

func (s *Stack) Pop() (int, error) {
    // 前置条件：栈不为空
    if len(s.items) == 0 {
        return 0, errors.New("stack underflow")
    }

    v := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return v, nil
}

// 不变量：Size 永远不会超过 Capacity，也不会小于 0
// 这由 Push 和 Pop 的逻辑严密性以及私有字段的封装共同保证。
</code></pre>
<p><strong>进阶思考：并发下的不变量</strong></p>
<p>还有一点不能忽略：Go 是为并发而生的。在单线程模型中，封装或许足以维护不变量。但在 Go 的并发世界里，如果多个 goroutine 同时修改这个 Stack，竞态条件（Race Condition）瞬间就会破坏 count &lt;= capacity 这样的“真理”。</p>
<p>因此，在 Go 的工程实践中，维护不变量往往还需要<strong>同步原语（如 sync.Mutex）</strong>的强力介入。只有配合了锁机制，才能确保对象在并发洪流的冲击下，依然能守住那份“不变”的契约。</p>
<h2>小结：心中的契约</h2>
<p>在结束这次跨越 20 年的时空对话之际，我想特别澄清一点：<strong>本文的目的，绝非鼓励大家在 Go 语言中笨拙地“模拟”一套 Eiffel 的语法糖。</strong></p>
<p>Go 语言有其独特且自洽的设计哲学——简洁、组合、并发。强行在 Go 代码中堆砌 require() 或 ensure() 函数，往往会画虎不成反类犬，破坏 Go 代码原有的流畅性。</p>
<p>我们重温 DbC，是为了<strong>汲取思想的养分</strong>。Bertrand Meyer 教会了我们要对代码的“权利与义务”保持敏感：</p>
<ul>
<li>当你写下一个函数时，你是否想清楚了它的<strong>前置条件</strong>？</li>
<li>你是否通过<strong>单元测试</strong>守护了它的<strong>后置条件</strong>？</li>
<li>你是否通过<strong>封装</strong>维护了对象的<strong>不变量</strong>？</li>
</ul>
<p>这些思考方式，才是 DbC 留给非 DbC 语言(如 Go、Java、Python)最宝贵的遗产。Bertrand Meyer 在 20 年前种下的那颗种子，虽然没有长成 Eiffel 这棵参天大树，但它的花粉却飘散到了整个软件工程的花园里。</p>
<p>Go 语言选择了另一条更务实的道路：<strong>用接口定义契约，用封装保护契约，用测试验证契约。</strong></p>
<p>作为一名 Gopher，当我们写下 type &#8230; interface，或者敲下 if err != nil 时，我们实际上是在履行一份神圣的职责。语言的特性在演进，但软件工程的核心——<strong>信任与责任的管理</strong>——从未改变。</p>
<p>真正的契约，不只写在代码里，更应刻在每一位工程师的心里。</p>
<h2>参考资料</h2>
<ul>
<li>Building bug-free O-O software: An introduction to Design by Contract &#8211; https://archive.eiffel.com/doc/manuals/technology/contract/</li>
<li>Object-Oriented Software Construction(2nd) &#8211; https://book.douban.com/subject/1547078/</li>
<li>Programming “By Contract” &#8211; https://www.cs.usfca.edu/~parrt/course/601/lectures/programming.by.contract.html</li>
</ul>
<hr />
<p><strong>聊聊你心中的“代码契约”</strong></p>
<p>这场跨越20年的思想对话，让我们重新审视了Go接口背后那份深刻的工程哲学。从Eiffel那严谨如“立法”的require/ensure，到Go语言“润物细无声”的interface/error/testing组合，我们看到的是不同时代背景下，对“信任与责任”这一软件工程核心母题的不同解答。</p>
<p><strong>那么，在你日常的Go编程实践中，你是如何理解和贯彻“契约精神”的？</strong></p>
<ul>
<li><strong>你是否也有过因为接口（契约）定义不清，而导致团队协作“踩坑”的经历？</strong></li>
<li><strong>除了文中提到的方法，你还有哪些维护代码“权利与义务”的独门心法？</strong></li>
<li><strong>你认为，Go语言在“契约”的表达上，还有哪些值得改进或探索的方向？</strong></li>
</ul>
<p><strong>非常期待在评论区看到你的故事与真知灼见，让我们一起探讨如何成为更具“契约精神”的工程师！</strong></p>
<p><strong>如果这篇文章让你对Go接口或软件工程的理解更深了一层，别忘了点个【赞】和【在看】，并分享给更多热爱思考的同伴！</strong></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface/feed/</wfw:commentRss>
		<slash:comments>0</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>Go语言gRPC服务Handler单元测试详解</title>
		<link>https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go/</link>
		<comments>https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go/#comments</comments>
		<pubDate>Fri, 24 Nov 2023 23:29:05 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[buf]]></category>
		<category><![CDATA[FakeObject]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httptest]]></category>
		<category><![CDATA[pb]]></category>
		<category><![CDATA[proto]]></category>
		<category><![CDATA[protobuf]]></category>
		<category><![CDATA[protoc]]></category>
		<category><![CDATA[protoc-gen-go]]></category>
		<category><![CDATA[protocol-buffer]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[streaming-rpc]]></category>
		<category><![CDATA[Stub]]></category>
		<category><![CDATA[SUT]]></category>
		<category><![CDATA[unary-rpc]]></category>
		<category><![CDATA[Unittest]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[服务端]]></category>
		<category><![CDATA[远程过程调用]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4055</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go 在云原生时代和微服务架构背景下，HTTP和RPC协议成为服务间通信和与客户端交互的两种主要方式。对于Go语言而言，标准库提供了net/http/httptest包，为开发人员提供了便捷的方式来构建服务端HTTP Handler单元测试的测试脚手架代码，而无需真正建立HTTP服务器，让开发人员可以聚焦于对Handler业务逻辑的测试。比如下面这个示例： // grpc-test-examples/httptest/http_handler_test.go func myHandler(w http.ResponseWriter, r *http.Request) { // 设置响应头 w.Header().Set("Content-Type", "text/plain") // 根据请求方法进行不同的处理 switch r.Method { case http.MethodGet: // 处理GET请求 fmt.Fprint(w, "Hello, World!") ... ... } } func TestMyHandler(t *testing.T) { // 创建一个ResponseRecorder来记录Handler的响应 rr := httptest.NewRecorder() // 创建一个模拟的HTTP请求，可以指定请求的方法、路径、正文等 req, err := http.NewRequest("GET", "/path", nil) if err != nil { [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/grpc-handler-unit-testing-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go">本文永久链接</a> &#8211; https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go</p>
<p>在云原生时代和微服务架构背景下，HTTP和RPC协议成为服务间通信和与客户端交互的两种主要方式。对于Go语言而言，标准库提供了net/http/httptest包，为开发人员提供了便捷的方式来构建服务端HTTP Handler单元测试的测试脚手架代码，而无需真正建立HTTP服务器，让开发人员可以聚焦于对Handler业务逻辑的测试。比如下面这个示例：</p>
<pre><code>// grpc-test-examples/httptest/http_handler_test.go

func myHandler(w http.ResponseWriter, r *http.Request) {
    // 设置响应头
    w.Header().Set("Content-Type", "text/plain")

    // 根据请求方法进行不同的处理
    switch r.Method {
    case http.MethodGet:
        // 处理GET请求
        fmt.Fprint(w, "Hello, World!")
    ... ...
    }
}

func TestMyHandler(t *testing.T) {
    // 创建一个ResponseRecorder来记录Handler的响应
    rr := httptest.NewRecorder()

    // 创建一个模拟的HTTP请求，可以指定请求的方法、路径、正文等
    req, err := http.NewRequest("GET", "/path", nil)
    if err != nil {
        t.Fatal(err)
    }

    // 调用被测试的Handler函数，传入ResponseRecorder和Request对象
    // 这里假设被测试的Handler函数为myHandler
    myHandler(rr, req)

    // 检查响应状态码和内容
    if rr.Code != http.StatusOK {
        t.Errorf("Expected status 200; got %d", rr.Code)
    }
    expected := "Hello, World!"
    if rr.Body.String() != expected {
        t.Errorf("Expected body to be %q; got %q", expected, rr.Body.String())
    }
}
</code></pre>
<blockquote>
<p>注：对http client端的单元测试，也可以<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators">利用httptest的NewServer来构建一个fake的http server</a>。</p>
</blockquote>
<p>然而，对于使用主流的<a href="https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server">gRPC等RPC协议的服务端Handler</a>来说，是否存在类似httptest的测试脚手架生成工具包呢？对gRPC的服务端Handler有哪些单元测试的方法呢？在这篇文章中，我们就一起来探究一下。</p>
<h2>1. 建立被测的gRPC服务端Handler</h2>
<p>我们首先来建立一个涵盖多种gRPC通信模式的服务端Handler集合。</p>
<p>gRPC支持四种通信模式，它们分别为：</p>
<ul>
<li>简单RPC(Simple RPC，也称为Unary RPC)</li>
</ul>
<p>这是最简单的，也是最常用的gRPC通信模式，简单来说就是<strong>一请求一应答</strong>。</p>
<ul>
<li>服务端流RPC(Server-streaming RPC)</li>
</ul>
<p>客户端发来一个请求，服务端通过流返回多个应答。</p>
<ul>
<li>客户端流RPC(Client-streaming RPC)</li>
</ul>
<p>客户端通过流发来多个请求，服务端以一个应答回复。</p>
<ul>
<li>双向流RPC(Bidirectional-Streaming RPC)</li>
</ul>
<p>客户端通过流发起多个请求，服务端也通过流对应返回多个应答。</p>
<blockquote>
<p>注：关于gRPC四种通信方式的详情，可以参考我之前写的《<a href="https://tonybai.com/2021/09/17/those-things-about-grpc-client">gRPC客户端的那些事儿</a>》一文。</p>
</blockquote>
<p>我们这个SUT(被测目标)是包含以上四种通信模式的gRPC服务，它的<a href="https://protobuf.dev/">Protocol Buffers</a>文件如下：</p>
<pre><code>// grpc-test-examples/grpctest/IDL/proto/mygrpc.proto

syntax = "proto3";

package mygrpc;

service MyService {
  // Unary RPC
  rpc UnaryRPC(RequestMessage) returns (ResponseMessage) {}

  // Server-Streaming RPC
  rpc ServerStreamingRPC(RequestMessage) returns (stream ResponseMessage) {}

  // Client-Streaming RPC
  rpc ClientStreamingRPC(stream RequestMessage) returns (ResponseMessage) {}

  // Bidirectional-Streaming RPC
  rpc BidirectionalStreamingRPC(stream RequestMessage) returns (stream ResponseMessage) {}
}

message RequestMessage {
  string message = 1;
}

message ResponseMessage {
  string message = 1;
}
</code></pre>
<p>通过protoc，我们可基于上述proto文件生成MyService桩(Stub)代码，生成的代码放在了mygrpc目录下面：</p>
<pre><code>// grpc-test-examples/grpctest/Makefile

all: gen

gen:
    protoc -I ./IDL/proto mygrpc.proto --gofast_out=plugins=grpc:./mygrpc
</code></pre>
<blockquote>
<p>注：你的环境下需要安装<a href="https://grpc.io/docs/protoc-installation/">protoc</a>和<a href="https://github.com/golang/protobuf/tree/master/protoc-gen-go">protoc-gen-go</a>才能正确执行上面生成命令，具体的安装方法可参考<a href="https://grpc.io/docs/protoc-installation/">protoc安装文档</a>。</p>
<p>注：除了使用经典的<a href="https://grpc.io/docs/protoc-installation/">protoc</a>基于proto文件生成Go源码外，也可以基于Go开发的<a href="https://github.com/bufbuild/buf">buf cli</a>进行代码生成和API管理。buf cLi是现代、快速、高效的Protobuf API管理的终极工具，为基于Protobuf的开发和维护提供了全面的解决方案。等有机会的时候，我在以后的文章中详细说说buf。</p>
</blockquote>
<p>有了生成的桩代码后，我们便可以建立一个gRPC服务器：</p>
<pre><code>// grpc-test-examples/grpctest/main.go

package main

import (
    pb "demo/mygrpc"
    "log"
    "net"

    "google.golang.org/grpc"
)

func main() {
    // 创建 gRPC 服务器
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()

    // 注册 MyService 服务
    pb.RegisterMyServiceServer(s, &amp;server{})

    // 启动 gRPC 服务器
    log.Println("Starting gRPC server...")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
</code></pre>
<p>我们看到：在main函数中，我们创建了一个TCP监听器，并使用grpc.NewServer()创建了一个gRPC服务器。然后，我们通过调用pb.RegisterMyServiceServer()将server类型的实例注册到gRPC服务器上，以处理来自客户端的请求。最后，我们启动gRPC服务器并监听指定的端口。</p>
<p>上面代码中注册到服务器中的server类型就是实现了MyService服务接口的具体类型，它实现了MyService定义的所有方法：</p>
<pre><code>// grpc-test-examples/grpctest/server.go

package main

import (
    "context"
    "fmt"
    "strconv"

    pb "demo/mygrpc"
)

type server struct{}

func (s *server) UnaryRPC(ctx context.Context, req *pb.RequestMessage) (*pb.ResponseMessage, error) {
    message := "Unary RPC received: " + req.Message
    fmt.Println(message)

    return &amp;pb.ResponseMessage{
        Message: "Unary RPC response",
    }, nil
}

func (s *server) ServerStreamingRPC(req *pb.RequestMessage, stream pb.MyService_ServerStreamingRPCServer) error {
    message := "Server Streaming RPC received: " + req.Message
    fmt.Println(message)

    for i := 0; i &lt; 5; i++ {
        response := &amp;pb.ResponseMessage{
            Message: "Server Streaming RPC response " + strconv.Itoa(i+1),
        }
        if err := stream.Send(response); err != nil {
            return err
        }
    }

    return nil
}

func (s *server) ClientStreamingRPC(stream pb.MyService_ClientStreamingRPCServer) error {
    var messages []string

    for {
        req, err := stream.Recv()
        if err != nil {
            return err
        }

        messages = append(messages, req.Message)

        if req.Message == "end" {
            break
        }
    }

    message := "Client Streaming RPC received: " + fmt.Sprintf("%v", messages)
    fmt.Println(message)

    return stream.SendAndClose(&amp;pb.ResponseMessage{
        Message: "Client Streaming RPC response",
    })
}

func (s *server) BidirectionalStreamingRPC(stream pb.MyService_BidirectionalStreamingRPCServer) error {
    for {
        req, err := stream.Recv()
        if err != nil {
            return err
        }

        message := "Bidirectional Streaming RPC received: " + req.Message
        fmt.Println(message)

        response := &amp;pb.ResponseMessage{
            Message: "Bidirectional Streaming RPC response",
        }
        if err := stream.Send(response); err != nil {
            return err
        }
    }
}
</code></pre>
<p>在上面代码中，我们创建了一个server结构体类型，并实现了MyService的所有RPC方法。每个方法都接收相应的请求消息，并返回对应的响应消息。我们的目标仅是演示如何对上述gRPC Handler进行单元测试，所以这里的实现逻辑非常简单。</p>
<p>接下来，我们就来逐一对这些gRPC的Handler方法进行单测，我们先从简单的UnaryRPC方法开始。</p>
<h2>2. Unary RPC Handler的单元测试</h2>
<p>Unary RPC是最简单，也是最容易理解的RPC通信模式，即客户端与服务端采用一请求一应答的模式。server类型的UnaryRPC Handler方法的原型如下：</p>
<pre><code>// grpc-test-examples/grpctest/server.go

func (s *server) UnaryRPC(ctx context.Context, req *pb.RequestMessage) (*pb.ResponseMessage, error)
</code></pre>
<p>就像文章开头做的那个httpserver的handler单测一样，我们肯定不想真实启动一个gRPC server，也不想测试gRPC服务器本身。我们只想测试服务端handler方法的逻辑是否正确。</p>
<p>观察一下这个方法原型，我们发现它仅依赖两个消息结构：RequestMessage和ResponseMessage，这两个消息结构是上面基于proto文件自动生成的，这样我们就可以不借助任何工具包实现对UnaryRPC handler方法的单测，也无需启动真实的gRPC Server：</p>
<pre><code>// grpc-test-examples/grpctest/server_test.go

type server struct{}

func TestServerUnaryRPC(t *testing.T) {
    s := &amp;server{}

    req := &amp;pb.RequestMessage{
        Message: "Test message",
    }

    resp, err := s.UnaryRPC(context.Background(), req)
    if err != nil {
        t.Fatalf("UnaryRPC failed: %v", err)
    }

    expectedResp := &amp;pb.ResponseMessage{
        Message: "Unary RPC response",
    }

    if resp.Message != expectedResp.Message {
        t.Errorf("Unexpected response. Got: %s, Want: %s", resp.Message, expectedResp.Message)
    }
}
</code></pre>
<p>将其改造为<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest/">基于subtest</a>和表驱动的测试也非常easy：</p>
<pre><code>// grpc-test-examples/grpctest/server_test.go

func TestServerUnaryRPCs(t *testing.T) {
    tests := []struct {
        name           string
        requestMessage *pb.RequestMessage
        expectedResp   *pb.ResponseMessage
    }{
        {
            name: "Test Case 1",
            requestMessage: &amp;pb.RequestMessage{
                Message: "Test message",
            },
            expectedResp: &amp;pb.ResponseMessage{
                Message: "Unary RPC response",
            },
        },
        // Add more test cases as needed
    }

    s := &amp;server{}

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            resp, err := s.UnaryRPC(context.Background(), tt.requestMessage)
            if err != nil {
                t.Fatalf("UnaryRPC failed: %v", err)
            }

            if resp.Message != tt.expectedResp.Message {
                t.Errorf("Unexpected response. Got: %s, Want: %s", resp.Message, tt.expectedResp.Message)
            }
        })
    }
}
</code></pre>
<p>如果gRPC handler测试都像UnaryRPC这样简单那就好了，但实际上&#8230;，好吧，我们继续向下看就好了。</p>
<h2>3. 针对Streaming通信模式的单元测试</h2>
<h3>3.1 ServerStreamingRPC的测试</h3>
<p>前面说过，gRPC支持三种Streaming通信模式：Server-Streaming RPC、Client-Streaming RPC和Bidirectional-Streaming RPC。</p>
<p>我们先来看看Server-Streaming RPC的方法原型：</p>
<pre><code>// grpc-test-examples/grpctest/server.go
func (s *server) ServerStreamingRPC(req *pb.RequestMessage, stream pb.MyService_ServerStreamingRPCServer) error
</code></pre>
<p>我们看到除了RequestMessag外，该方法还依赖一个MyService_ServerStreamingRPCServer的类型，这个类型是一个接口类型：</p>
<pre><code>// grpc-test-examples/mygrpc/mygrpc.pb.go

type MyService_ServerStreamingRPCServer interface {
    Send(*ResponseMessage) error
    grpc.ServerStream
}
</code></pre>
<p>到这里，你脑子中可能已经冒出了一个想法：<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/">使用fake object来对ServerStreamingRPC进行单测</a>，这的确是一个可行的方法，我们下面就基于这个思路实现一下。</p>
<blockquote>
<p>注：关于基于fake object进行单测的内容，大家可以看看我以前写的一篇文章《[]单测时尽量用fake object(https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators)》。</p>
</blockquote>
<h3>3.2 基于fake object的测试</h3>
<p>我们首先创建一个实现MyService_ServerStreamingRPCServer的fake object用以代替真实运行RPC服务器时由服务器传入的stream object：</p>
<pre><code>// grpc-test-examples/grpctest/server_with_fakeobject_test.go

import (
    "testing"

    pb "demo/mygrpc"

    "google.golang.org/grpc"
)

type fakeServerStreamingRPCStream struct {
    grpc.ServerStream
    responses []*pb.ResponseMessage
}

func (m *fakeServerStreamingRPCStream) Send(resp *pb.ResponseMessage) error {
    m.responses = append(m.responses, resp)
    return nil
}
</code></pre>
<p>我们看到fakeServerStreamingRPCStream的Send方法只是将收到的ResponseMessage追加到且内部的ResponseMessage切片中。</p>
<p>接下来我们为ServerStreamingRPC编写测试用例：</p>
<pre><code>// grpc-test-examples/grpctest/server_with_fakeobject_test.go

func TestServerServerStreamingRPC(t *testing.T) {
    s := &amp;server{}  

    req := &amp;pb.RequestMessage{
        Message: "Test message",
    }  

    stream := &amp;fakeServerStreamingRPCStream{}  

    err := s.ServerStreamingRPC(req, stream)
    if err != nil {
        t.Fatalf("ServerStreamingRPC failed: %v", err)
    }  

    expectedResponses := []string{
        "Server Streaming RPC response 1",
        "Server Streaming RPC response 2",
        "Server Streaming RPC response 3",
        "Server Streaming RPC response 4",
        "Server Streaming RPC response 5",
    }                                                                                                          

    if len(stream.responses) != len(expectedResponses) {
        t.Errorf("Unexpected number of responses. Got: %d, Want: %d", len(stream.responses), len(expectedResponses))
    }                                                                                                          

    for i, resp := range stream.responses {
        if resp.Message != expectedResponses[i] {
            t.Errorf("Unexpected response at index %d. Got: %s, Want: %s", i, resp.Message, expectedResponses[i])
        }
    }
}
</code></pre>
<p>在这个测试中，ServerStreamingRPC接收一个请求(req)，并通过fake stream object的Send方法返回了5个response，通过与预期的response对比，即可做出测试是否通过的断言。</p>
<p>到这里，我们看到：fake object完全满足对gRPC Server Handler进行测试的要求。不过我们需要针对不同的Handler建立不同的fake object类型，和文初基于httptest创建的测试用例相比，用例间欠缺了一些一致性。</p>
<p>那grpc-go是否提供了类似httptest的工具来帮助我们更一致的实现grpc server handler的测试用例呢？我们继续往下看。</p>
<h3>3.3 利用grpc-go提供的测试工具包</h3>
<p>grpc-go项目在test下提供了bufconn包，可以帮助我们像httptest那样建立用于测试的“虚拟gRPC服务器”，下面是基于bufconn包建立gRPC测试用服务器的代码：</p>
<pre><code>// grpc-test-examples/grpctest/server_with_buffconn_test.go

package main

import (
    "context"
    "log"
    "net"
    "testing"

    pb "demo/mygrpc"

    "google.golang.org/grpc"
    "google.golang.org/grpc/test/bufconn"
)

func newGRPCServer(t *testing.T) (pb.MyServiceClient, func()) {
    // 创建 bufconn.Listener 作为服务器的监听器
    listener := bufconn.Listen(1024 * 1024)

    // 创建 gRPC 服务器
    srv := grpc.NewServer()

    // 注册服务处理程序
    pb.RegisterMyServiceServer(srv, &amp;server{})

    // 在监听器上启动服务器
    go func() {
        if err := srv.Serve(listener); err != nil {
            t.Fatalf("Server failed to start: %v", err)
        }
    }()

    // 创建 bufconn.Dialer 作为客户端连接
    dialer := func(context.Context, string) (net.Conn, error) {
        return listener.Dial()
    }

    // 使用 DialContext 和 bufconn.Dialer 创建客户端连接
    conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(dialer), grpc.WithInsecure())
    if err != nil {
        t.Fatalf("Failed to dial server: %v", err)
    }

    // 创建客户端实例
    client := pb.NewMyServiceClient(conn)
    return client, func() {
        err := listener.Close()
        if err != nil {
            log.Printf("error closing listener: %v", err)
        }
        srv.Stop()
    }
}
</code></pre>
<p>newGRPCServer是一个用于在测试中创建gRPC服务器和客户端的辅助函数，它使用bufconn.Listen创建一个bufconn.Listener作为服务器的监听器。bufconn包提供了一种在内存中模拟网络连接的方法。然后，它使用grpc.NewServer()创建了一个新的gRPC服务器实例，并使用pb.RegisterMyServiceServer将待测的服务实例(这里是server类型实例)注册到gRPC服务器中。接下来，它创建了与该服务器建连的gRPC客户端，由于该客户端要与bufconn.Listener建连，这里用了一个dialer函数，该函数将通过调用listener.Dial()来建立与服务器的连接。之后基于该连接，我们创建了MyServiceClient的客户端实例，并返回，供测试用例使用。</p>
<p>基于newGPRCServer这种方式，我们改造一下UnaryRPC的测试用例：</p>
<pre><code>// grpc-test-examples/grpctest/server_with_buffconn_test.go

func TestServerUnaryRPCWithBufConn(t *testing.T) {
    client, shutdown := newGRPCServer(t)
    defer shutdown()

    tests := []struct {
        name           string
        requestMessage *pb.RequestMessage
        expectedResp   *pb.ResponseMessage
    }{
        {
            name: "Test Case 1",
            requestMessage: &amp;pb.RequestMessage{
                Message: "Test message",
            },
            expectedResp: &amp;pb.ResponseMessage{
                Message: "Unary RPC response",
            },
        },
        // Add more test cases as needed
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            resp, err := client.UnaryRPC(context.Background(), tt.requestMessage)
            if err != nil {
                t.Fatalf("UnaryRPC failed: %v", err)
            }

            if resp.Message != tt.expectedResp.Message {
                t.Errorf("Unexpected response. Got: %s, Want: %s", resp.Message, tt.expectedResp.Message)
            }
        })
    }
}
</code></pre>
<p>我们看到，相对于前面的TestServerUnaryRPCs，两者复杂度在一个层次。如果结合下面的ServerStreamRPC的测试用例，你就能看出这种方式在测试用例一致性方面的优势了：</p>
<pre><code>// grpc-test-examples/grpctest/server_with_buffconn_test.go

func TestServerServerStreamingRPCWithBufConn(t *testing.T) {
    client, shutdown := newGRPCServer(t)
    defer shutdown()

    req := &amp;pb.RequestMessage{
        Message: "Test message",
    }

    stream, err := client.ServerStreamingRPC(context.Background(), req)
    if err != nil {
        t.Fatalf("ServerStreamingRPC failed: %v", err)
    }

    expectedResponses := []string{
        "Server Streaming RPC response 1",
        "Server Streaming RPC response 2",
        "Server Streaming RPC response 3",
        "Server Streaming RPC response 4",
        "Server Streaming RPC response 5",
    }

    gotResponses := []string{}

    for {
        resp, err := stream.Recv()
        if err != nil {
            break
        }
        gotResponses = append(gotResponses, resp.Message)
    }

    if len(gotResponses) != len(expectedResponses) {
        t.Errorf("Unexpected number of responses. Got: %d, Want: %d", len(gotResponses), len(expectedResponses))
    }

    for i, resp := range gotResponses {
        if resp != expectedResponses[i] {
            t.Errorf("Unexpected response at index %d. Got: %s, Want: %s", i, resp, expectedResponses[i])
        }
    }
}
</code></pre>
<p>我们再也无需为每个Server Handler建立各自的fake object了！</p>
<p>由此看到：grpc-go的test/bufconn就是类似httptest的那个grpc server handler的测试脚手架搭建工具。</p>
<h3>3.4 其他Streaming模式的Handler测试</h3>
<p>有了bufconn这一利器，其他Streaming模式的Handler测试实现逻辑就大同小异了。本文示例中的ClientStreamingRPC和BidirectionalStreamingRPC两个Handler的测试用例就作为作业，交给各位读者去完成吧！</p>
<h2>4. 小结</h2>
<p>在本文中，我们详细探讨了如何对gRPC服务端Handler进行单元测试，我们的目标是找到像net/http/httptest包那样的，可以为gRPC服务端handler测试提供脚手架代码帮助的测试方法。</p>
<p>我们按照gRPC的四种通信方式，由简到难的逐一探讨各种Handler的单测方法。UnaryRPC handler测试最为简单，毫无技巧的普通测试逻辑便能应付。</p>
<p>但一旦涉及streaming通信方式的测试，我们就需要借助类似fake object的单测技术了。但fake object也有不足，那就是需要为每个RPC handler建立单独的fake object，费时费力还缺少一致性！</p>
<p>好在，grpc-go项目为我们提供了test/bufconn包，该包可以像net/http/httptest包那样帮助我们快速建立可复用的测试脚手架代码，这样我们便可以为所有服务端RPC Handler建立一致、稳定的单元测试用例了！</p>
<p>当然，服务端RPC Handler的单测方法可能不止文中提及这些，各位读者如果有更好的方法和实践，欢迎在评论区留言！</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/grpc-test-examples">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://medium.com/@johnsiilver/testing-grpc-methods-6a8edad4159d">Testing gRPC methods</a> &#8211; https://medium.com/@johnsiilver/testing-grpc-methods-6a8edad4159d</li>
<li><a href="https://book.douban.com/subject/34796013/">《gRPC Up and Running》</a> &#8211; https://book.douban.com/subject/34796013/</li>
<li><a href="https://rotational.io/blog/mocking-the-universe/">Mocking the Universe: Two Techniques for Testing gRPC with Mocks</a> &#8211; https://rotational.io/blog/mocking-the-universe/</li>
</ul>
<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/11/25/grpc-handler-unit-testing-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.13中值得关注的几个变化</title>
		<link>https://tonybai.com/2019/10/27/some-changes-in-go-1-13/</link>
		<comments>https://tonybai.com/2019/10/27/some-changes-in-go-1-13/#comments</comments>
		<pubDate>Sun, 27 Oct 2019 14:20:27 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[android10]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[Errorf]]></category>
		<category><![CDATA[escape-analysis]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generics]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.num]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.12]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[GO111MODULE]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[godoc]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GONOPROXY]]></category>
		<category><![CDATA[GONOSUMDB]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[GOPRIVATE]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[GOSUMDB]]></category>
		<category><![CDATA[heap]]></category>
		<category><![CDATA[K&R]]></category>
		<category><![CDATA[lock]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[number]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[RWMutex]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[TLS1.3]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[Unittest]]></category>
		<category><![CDATA[Unlock]]></category>
		<category><![CDATA[Unwrap]]></category>
		<category><![CDATA[vgo]]></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=2802</guid>
		<description><![CDATA[2019年对于Go语言来说也是一个重要的年份，因为在2019年的11月10日，Go即将迎来其开源10周年的纪念日。在这个重要日子的前夕，在GopherCon 2019大会后，Go项目组在2019.9.4日发布了Go 1.13版本。 这是自2017年GopherCon大会上Russ Cox做“Toward Go 2&#8243;主题演讲以来Go项目发布的第四个版本（前三个分别是：go 1.10、go 1.11和go 1.12)。 Go2是这两年Go项目的核心主题。Go项目组也一直在摸索着向Go2演化的节奏和过程规范，并已经从Go 1.11版本起做出了实质性的动作：添加go module机制、错误处理优化、泛型讨论和多次草案的发布等。Russ Cox这段时间还在自己的博客上撰写了一系列有关Go proposal流程究竟该如何改进的探索性文章，这与当年vgo“放大招”前的节奏有些相似:)。 回归正题，我们来说Go 1.13这个版本。Go 1.13延续了对之前版本添加的Go2特性：Go module的优化；并且从该版本开始，Go项目组开启了Go2中呼声也很高的错误处理的优化。下面我们详细来看看Go 1.13中值得关注的几个变化。 1. 语言 Go 1.13中，Go语言规范有了一些小变化。 Go在设计伊始就和多数C-Family语言一样继承了C语言关于数字字面量(number literal)的语法形式，和1978年发布的K&#38;R C一样，Go仅支持十进制、八进制、十六进制和十进制形式的浮点数的数字字面量形式，比如： a := 53 //十进制 b := 0700 // 八进制，以"0"开头 c := 0xaabbcc // 十六进制 以"0x"开头 c1 := 0Xddeeff // 十六进制 以"0X"开头 f1 := 10.24 // 十进制浮点数 f2 := [...]]]></description>
			<content:encoded><![CDATA[<p>2019年对于<a href="https://golang.org">Go语言</a>来说也是一个重要的年份，因为在2019年的11月10日，<a href="https://tonybai.com/tag/go">Go</a>即将迎来其<a href="https://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc">开源10周年</a>的纪念日。在这个重要日子的前夕，在GopherCon 2019大会后，Go项目组在2019.9.4日发布了<a href="https://tip.golang.org/doc/go1.13">Go 1.13版本</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.13-release-logo.png" alt="img{512x368}" /></p>
<p>这是自2017年GopherCon大会上<a href="https://swtch.com/~rsc/">Russ Cox</a>做<a href="https://blog.golang.org/toward-go2">“Toward  Go 2&#8243;</a>主题演讲以来Go项目发布的第四个版本（前三个分别是：<a href="https://tonybai.com/2018/02/17/some-changes-in-go-1-10">go 1.10</a>、<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">go 1.11</a>和<a href="https://tonybai.com/2019/03/02/some-changes-in-go-1-12/">go 1.12</a>)。</p>
<p>Go2是这两年Go项目的核心主题。Go项目组也一直在摸索着向Go2演化的节奏和过程规范，并已经从<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>起做出了实质性的动作：添加<a href="https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/">go module机制</a>、<a href="https://tonybai.com/2019/10/18/errors-handling-in-go-1-13/">错误处理优化</a>、<a href="https://github.com/golang/go/issues/15292">泛型讨论和多次草案的发布</a>等。Russ Cox这段时间还在自己的博客上撰写了<a href="https://research.swtch.com/proposals">一系列有关Go proposal流程究竟该如何改进的探索性文章</a>，这与当年<a href="https://github.com/golang/vgo/">vgo</a>“放大招”前的节奏有些相似:)。</p>
<p>回归正题，我们来说<a href="https://tip.golang.org/doc/go1.13">Go 1.13</a>这个版本。Go 1.13延续了对之前版本添加的Go2特性：<a href="https://tonybai.com/2018/07/15/hello-go-module/">Go module</a>的优化；并且从该版本开始，Go项目组开启了Go2中呼声也很高的<a href="https://github.com/golang/proposal/blob/master/design/go2draft-error-handling-overview.md">错误处理</a>的优化。下面我们详细来看看Go 1.13中值得关注的几个变化。</p>
<h2>1. 语言</h2>
<p>Go 1.13中，Go语言规范有了一些小变化。</p>
<p>Go在设计伊始就和多数C-Family语言一样继承了<a href="https://tonybai.com/tag/c">C语言</a>关于<strong>数字字面量(number literal)</strong>的语法形式，和1978年发布的<a href="https://en.wikipedia.org/wiki/K%26R_C">K&amp;R C</a>一样，Go仅支持十进制、八进制、十六进制和十进制形式的浮点数的数字字面量形式，比如：</p>
<pre><code>a := 53        //十进制

b := 0700      // 八进制，以"0"开头
c := 0xaabbcc  // 十六进制 以"0x"开头

c1 := 0Xddeeff // 十六进制 以"0X"开头

f1 := 10.24  // 十进制浮点数
f2 := 1.e+0  // 十进制浮点数
f3 := 31415.e-4 // 十进制浮点数

</code></pre>
<p>这些数字字面量语法应该说是够用的，但是和其他语言在进化过程中添加的其他数字字面量表达形式相比，又显得有些不足。于是Go设计者决定在Go 1.13版本中增加Go对数字字面量的表达能力，在这方面对Go语言做了如下补充：</p>
<ul>
<li>
<p>增加二进制数字字面量，以0b或0B开头</p>
</li>
<li>
<p>在保留以”0&#8243;开头的八进制数字字面量形式的同时，增加以”0o”或”0O”开头的八进制数字字面量形式</p>
</li>
<li>
<p>增加十六进制形式的浮点数字面量，以0x或0X开头的、形式如0&#215;123.86p+2的浮点数</p>
</li>
<li>
<p>为提升可读性，在数字字面量中增加数字分隔符”_”，分隔符可以用来分隔数字(起到分组提高可读性作用，比如每3个数字一组)，也可以用来分隔前缀与第一个数字。</p>
</li>
</ul>
<pre><code>a := 5_3_7
b := 0o700
b1 := 0O700
b2 := 0_700
b3 := 0o_700
c := 0b111
c1 := 0B111
c2 := 0b_111
f1 := 0x10.24p+3
f2 := 0x1.Fp+0
f3 := 0x31_415.p-4

</code></pre>
<blockquote>
<p>注：截至目前，有些第三方工具依然无法识别数字字面量中的分隔符，会误报其为语法错误。</p>
</blockquote>
<p>Go 1.13中关于语言规范方面的另一个变动点是<strong>取消了移位操作(>>的&lt;&lt;)的右操作数仅能是无符号数的限制</strong>，以前必须的强制到uint的转换现在不必要了：</p>
<pre><code>var i int = 5

fmt.Println(2 &lt;&lt; uint(i)) // before go 1.13
fmt.Println(2 &lt;&lt; i)       // in go 1.13 and later version

</code></pre>
<p>不过值得注意的是：<a href="https://tonybai.com/2019/03/02/some-changes-in-go-1-12">go 1.12版本</a>在go.mod文件中增加了一个go version的指示字段，用于指示该module内源码所使用的 go版本。Go 1.13的发布文档强调了<strong>只有在go.mod中的go version指示字段为go 1.13(以及以后版本)时，上述的语言特性变更才会生效</strong>，否则就会报类似下面的错误：</p>
<pre><code>// github.com/bigwhite/experiments/go1.13-examples/number_literal.go

$go run number_literal.go
# command-line-arguments
./number_literal.go:23:7: underscores in numeric literals only supported as of -lang=go1.13
./number_literal.go:24:7: 0o/0O-style octal literals only supported as of -lang=go1.13
./number_literal.go:25:8: 0o/0O-style octal literals only supported as of -lang=go1.13
./number_literal.go:26:8: underscores in numeric literals only supported as of -lang=go1.13
./number_literal.go:27:8: underscores in numeric literals only supported as of -lang=go1.13
./number_literal.go:28:7: binary literals only supported as of -lang=go1.13
./number_literal.go:29:8: binary literals only supported as of -lang=go1.13
./number_literal.go:30:8: underscores in numeric literals only supported as of -lang=go1.13
./number_literal.go:31:8: hexadecimal floating-point literals only supported as of -lang=go1.13
./number_literal.go:32:8: hexadecimal floating-point literals only supported as of -lang=go1.13
./number_literal.go:32:8: too many errors

// github.com/bigwhite/experiments/go1.13-examples/shift_with_signed_operand.go

$go run shift_with_signed_operand.go
# command-line-arguments
./shift_with_signed_operand.go:8:16: invalid operation: 2 &lt;&lt; i (signed shift count type int, only supported as of -lang=go1.13)

</code></pre>
<p>当然，如果repo下没有go.mod或者单独在某个没有go.mod的目录下使用go 1.13编译器运行上面代码，则是无问题的。</p>
<h2>2. Go module机制的继续优化以及行为变化</h2>
<p>Go module自<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>加入Go以来收到了<a href="https://github.com/golang/go/wiki/Modules">Go社区的大量反馈</a>，Go核心团队也针对这些反馈对Go module机制进行了持续地优化。在Go 1.13中，Go module的一些改变如下：</p>
<h3>1) GO111MODULE=auto的行为变化</h3>
<p>在Go 1.12版本中，GO111MODULE默认值为auto，在auto模式下，GOPATH/src下面的repo以及在GOPATH之外的repo依旧使用GOPATH mode，不使用go.mod来管理依赖；在Go 1.13中，module mode优先级提升，GO111MODULE的默认值依然为auto，但在这个auto下，无论是在GOPATH/src下还是GOPATH之外的repo中，只要目录下有go.mod，go编译器都会使用go module来管理依赖。</p>
<h3>2) <a href="https://tonybai.com/2018/11/26/hello-go-module-proxy/">GOPROXY</a>有默认初值并支持设置成多个代理的列表</h3>
<p>之前版本中，GOPROXY这个环境环境变量默认值为空，go编译器都是直接与类似github.com这样的代码托管站点通信并获取相关依赖库的数据的；一些第三方GOPROXY服务发布后，迁移到go module的gopher们发现：大多数情况下通过proxy获取依赖包数据的速度要远高于直接从代码托管站点获取，因此GOPROXY总是会配置上一个值。Go核心团队也希望Go世界能有一个像nodejs那样的中心化的module仓库为大家提供服务，于是在Go 1.13中将https://proxy.golang.org作为GOPROXY环境变量的默认值之一，这也是Go官方提供的GOPROXY服务。</p>
<p>同时GOPROXY支持设置为多个proxy的列表(多个proxy之间采用逗号分隔)，Go编译器会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。</p>
<p>Go 1.13中，GOPROXY的默认值为https://proxy.golang.org,direct。当官方代理返回404或410时，Go编译器会尝试直接连接依赖module的代码托管站点以获取数据。</p>
<p>由于国内无法访问Go官方的proxy，因此我一般会将我的工作环境下的GOPROXY设置为：</p>
<pre><code>export GOPROXY=https://goproxy.cn,自己在国外主机使用athens搭建的代理,direct

</code></pre>
<h3>3) GOSUMDB</h3>
<p>我们知道go会在go module启用时在本地建立一个go.sum文件，用来存储依赖包特定版本的加密校验和。同时，Go维护下载的软件包的缓存，并在下载时计算并记录每个软件包的加密校验和。在正常操作中，go命令对照这些预先计算的校验和去检查某repo下的go.sum文件，而不是在每次命令调用时都重新计算它们。</p>
<p>在日常开发中，特定module版本的校验和永远不会改变。每次运行或构建时，go命令都会通过本地的go.sum去检查其本地缓存副本的校验和是否一致。如果校验和不匹配，则go命令将报告安全错误，并拒绝运行构建或运行。在这种情况下，重要的是找出正确的校验和，确定是go.sum错误还是下载的代码是错误的。如果go.sum中尚未包含已下载的module，并且该模块是公共module，则go命令将查询Go校验和数据库以获取正确的校验和数据存入go.sum。如果下载的代码与校验和不匹配，则go命令将报告不匹配并退出。</p>
<p>Go 1.13提供了GOSUMDB环境变量用于配置Go校验和数据库的服务地址（和公钥），其默认值为”sum.golang.org”，这也是Go官方提供的校验和数据库服务(大陆gopher可以使用sum.golang.google.cn)。</p>
<p>出于安全考虑，建议保持GOSUMDB开启。但如果因为某些因素，无法访问GOSUMDB（甚至是sum.golang.google.cn），可以通过下面命令将其关闭：</p>
<pre><code>go env -w GOSUMDB=off

</code></pre>
<p>GOSUMDB关闭后，仅能使用本地的go.sum进行包的校验和校验了。</p>
<h3>4）面向私有模块的GOPRIVATE</h3>
<p>有了GOPROXY后，公共module的数据获取变得十分easy。但是如果依赖的是企业内部module或托管站点上的private库，通过GOPROXY（默认值）获取显然会得到一个失败的结果，除非你搭建了自己的公私均可的goproxy server并将其设置到GOPROXY中。</p>
<p>Go 1.13提供了GOPRIVATE变量，用于指示哪些仓库下的module是private，不需要通过GOPROXY下载，也不需要通过GOSUMDB去验证其校验和。不过要注意的是GONOPROXY和GONOSUMDB可以override GOPRIVATE中的设置，因此设置时要谨慎，比如下面的例子：</p>
<pre><code>GOPRIVATE=pkg.tonyba.com/private
GONOPROXY=none

GONOSUMDB=none

</code></pre>
<p>GOPRIVATE指示pkg.tonybai.com/private下的包不经过代理下载，不经过SUMDB验证。但GONOPROXY和GONOSUMDB均为none，意味着所有module，不管是公共的还是私有的，都要经过proxy下载，经过sumdb验证。前面提到过了，GONOPROXY和GONOSUMDB会override GOPRIVATE的设置，因此在这样的配置下，所有依赖包都要经过proxy下载，也要经过sumdb验证。不过这个例子中的GOPRIVATE的值也不是一无是处，它可以给其他go tool提供私有module的指示信息。</p>
<h2>3. Go错误处理优化迈出第一步</h2>
<p>Go核心团队早在一年前就提出了关于go错误处理的<a href="https://github.com/golang/proposal/blob/master/design/go2draft-error-handling-overview.md">多个proposal</a>，其中涉及解决if err != nil 大量重复问题的，有解决错误包装(wrap)问题的，有解决error value比较问题的。在Go 1.13中，Go核心团队落实了后两个：</p>
<ul>
<li>
<p>通过标准库增加了errors.Is和As函数来解决error value比较问题</p>
</li>
<li>
<p>增加errors.Unwrap来解决error unwrap问题。</p>
</li>
</ul>
<p>并且Go通过在fmt.Errorf中新增的”%w”动词来协助Gopher快速创建一个包装错误，创建的error变量实现了下面接口：</p>
<pre><code>interface { // 一个匿名接口

    Unwrap() error

}

</code></pre>
<p>关于Go 1.13中错误处理的改进，Go官方发表了一篇博客<a href="https://tonybai.com/2019/10/18/errors-handling-in-go-1-13/">《Go 1.13中的错误处理》</a>给出了十分详尽的说明，这里就不赘述了。</p>
<h2>4.  性能</h2>
<p>个人觉得Go 1.13中能带来性能提升的变动主要有三个：</p>
<p>第一个就是defer的性能提升。</p>
<p>defer语法让Gopher在进行资源(文件、锁)释放的过程变动优雅很多，也不易出错。但在性能敏感的应用中，defer带来的性能负担也是Gopher必须要权衡的问题。在Go 1.13中，Go核心团队对defer性能做了大幅优化，官方给出了在大多数情况下，defer性能提升30%的说法。</p>
<p>这里可以来验证一下：我们使用Go 1.13和Go 1.12.7两个版本运行同一个benchmark(macos 1.6G 8核 16G内存)：</p>
<pre><code>// github.com/bigwhite/experiments/go1.13-examples/defer_benchmark_test.go

package defer_test

import "testing"

func sum(max int) int {
        total := 0
        for i := 0; i &lt; max; i++ {
                total += i
        }

        return total
}

func foo() {
        defer func() {
                sum(10)
        }()

        sum(100)
}

func BenchmarkDefer(b *testing.B) {
        for i := 0; i &lt; b.N; i++ {
                foo()
        }
}

</code></pre>
<p>go 1.13下的benchmark结果：</p>
<pre><code>$go test -bench . defer_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkDefer-8       17341530            67.3 ns/op
PASS
ok      command-line-arguments    1.245s

</code></pre>
<p>go 1.12.7下的benchmark结果：</p>
<pre><code>$go test -bench . defer_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkDefer-8       20000000            76.5 ns/op
PASS
ok      command-line-arguments    1.618s

</code></pre>
<p>我们看到性能的确有提升，但没有到30%这么大幅度，也许这仅仅是一个个例吧。<br />
第二个是优化后的逃逸分析(escape analysis)让编译器在选择究竟将变量分配在stack上还是heap上的时候更加精确。在老版本里分配到heap上的变量，在Go 1.13中可能就会分配到stack上，从而减少内存分配的次数，一定程度上减轻gc的压力，达到性能提升的目的。</p>
<p>第三个是sync包中Mutex、RWMutex的方法的inline化带来的性能提升，官方说法是10%。我们同样来benchmark一下：</p>
<pre><code>// github.com/bigwhite/experiments/go1.13-examples/mutex_benchmark_test.go

package mutex_test

import (
        "sync"
        "testing"
)

func sum(max int) int {
        total := 0
        for i := 0; i &lt; max; i++ {
                total += i
        }

        return total
}

func foo() {
        var mu sync.Mutex
        mu.Lock()
        sum(10)
        mu.Unlock()
}

func BenchmarkMutex(b *testing.B) {
        for i := 0; i &lt; b.N; i++ {
                foo()
        }
}

</code></pre>
<p>Go 1.13下的结果：</p>
<pre><code>$go test -bench . mutex_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkMutex-8       43395768            26.4 ns/op
PASS
ok      command-line-arguments    1.182s

</code></pre>
<p>Go 1.12.7下的结果：</p>
<pre><code>$go test -bench . mutex_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkMutex-8       50000000            28.4 ns/op
PASS
ok      command-line-arguments    1.457s

</code></pre>
<p>从结果看，提升在7%左右，约等于10%吧。</p>
<h2>5. 其他变化</h2>
<p>简单罗列一些我认为值得关注的小变化：</p>
<ul>
<li>
<p>Go 1.13现在支持Android 10了；对MacOS的支持需要至少10.11版本；</p>
</li>
<li>
<p>godoc不再和go、gofmt放入go release版中，需要godoc的，需要单独从golang.org/x/tools/cmd/godoc中下载安装；</p>
</li>
<li>
<p>crypto/tls默认开启tls 1.3支持；</p>
</li>
<li>
<p>unicode包支持的unicode标准从10.0版本升级到<a href="http://www.unicode.org/versions/Unicode11.0.0/">Unicode 11.0版本</a></p>
</li>
</ul>
<h2>6. 小结</h2>
<p>Go 1.13版本的发布标志着Go向着Go2的目标又迈出了坚实的一步，Go的演化节奏也是恰到好处：</p>
<ul>
<li>
<p>go module已经落地成型，逐渐打磨到成熟；</p>
</li>
<li>
<p>错误处理：迈出阶段性的一步，后续持续改进</p>
</li>
<li>
<p>Go generics: 是Go2最大的”挑战”。我们看到在GopherCon 2019大会上，<a href="https://github.com/ianlancetaylor">Ian Lance Taylor</a>带来的有关Go generics的<a href="https://github.com/golang/proposal/blob/master/design/go2draft-contracts.md">proposal的改进</a>正在被越来越多Gopher所认可。</p>
</li>
</ul>
<p>不过按照go team的行事风格，任何一个proposal都会经历”实验，简化和发布”的步骤，Go generics还有很长的路要走，让我们共同期待！</p>
<p>本文中涉及的样例源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.13-examples">这里</a>获取到。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/10/27/some-changes-in-go-1-13/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>写Go代码时遇到的那些问题[第2期]</title>
		<link>https://tonybai.com/2018/01/27/the-problems-i-encountered-when-writing-go-code-issue-2nd/</link>
		<comments>https://tonybai.com/2018/01/27/the-problems-i-encountered-when-writing-go-code-issue-2nd/#comments</comments>
		<pubDate>Fri, 26 Jan 2018 16:47:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Concurrency]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.4]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[Gopkg.lock]]></category>
		<category><![CDATA[Gopkg.toml]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[Setup]]></category>
		<category><![CDATA[teardown]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TestMain]]></category>
		<category><![CDATA[Timer]]></category>
		<category><![CDATA[TOML]]></category>
		<category><![CDATA[Unittest]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[错误处理]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2536</guid>
		<description><![CDATA[第1期的“写Go代码时遇到的那些问题”一经发布后得到了很多Gopher的支持和赞赏，这也是我继续写下去的动力！不过这里依然要强调的是这一系列文章反映的是笔者在实践中对代码编写的认知以及代码的演化过程。这里的代码也许只是“中间阶段”，并不是什么最优的结果，我记录的只是对问题、对代码的一个思考历程。不过，十分欢迎交流与批评指正。 一、dep的日常操作 虽然dep在国内使用依然有init失败率较高（因为一些qiang外的第三方package）的坎儿，但我和主流Gopher社区和项目一样，义无反顾地选择在代码库中使用dep。本周dep刚刚发布了0.4.1版本，与之前版本最大的不同在于dep发布了其官网以及相对完整的文档（以替代原先在github项目主页上的简陋的、格式较low的FAQ），这也是dep继续走向成熟的一个标志。不过关于dep何时能merge到go tools链当中，目前还是未知数。不过dep会在相当长的一段时期继续以独立工具的形式存在，直到merge到Go tools中并被广泛接受。 包依赖管理工具在日常开发中并不需要太多的存在感，我们需要的这类工具特征是功能强大但接口“小”，对开发者体验好，不太需要太关心其运行原理，dep基本符合。dep日常操作最主要的三个命令：dep init、dep ensure和dep status。在《初窥dep》一文中，我曾重点说过dep init原理，这里就不重点说了，我们用一个例子来说说使用dep的日常workflow。 1、dep init empty project 我们可以对一个empty project或一个初具框架雏形的project进行init，这里init一个empty project，作为后续的示例基础： ➜ $GOPATH/src/depdemo $dep init -v Getting direct dependencies... Checked 1 directories for packages. Found 0 direct dependencies. Root project is "depdemo" 0 transitively valid internal packages 0 external packages imported from 0 projects (0) ✓ select (root) ✓ [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/">第1期的“写Go代码时遇到的那些问题”</a>一经发布后得到了很多Gopher的支持和赞赏，这也是我继续写下去的动力！不过这里依然要强调的是这一系列文章反映的是笔者在实践中对代码编写的认知以及代码的演化过程。这里的代码也许只是“中间阶段”，并不是什么最优的结果，我记录的只是对问题、对代码的一个思考历程。不过，十分欢迎交流与批评指正。</p>
<h2>一、dep的日常操作</h2>
<p>虽然<a href="https://github.com/golang/dep">dep</a>在国内使用依然有init失败率较高（因为一些qiang外的第三方package）的坎儿，但我和主流Gopher社区和项目一样，义无反顾地选择在代码库中<a href="http://tonybai.com/2017/06/08/first-glimpse-of-dep/">使用dep</a>。本周dep刚刚发布了<a href="https://golang.github.io/dep/blog/2018/01/23/announce-v0.4.0.html">0.4.1版本</a>，与之前版本最大的不同在于dep发布了其官网以及相对完整的文档（以替代原先在github项目主页上的简陋的、格式较low的FAQ），这也是dep继续走向成熟的一个标志。不过关于dep何时能merge到go tools链当中，目前还是未知数。不过dep会在相当长的一段时期继续以独立工具的形式存在，直到merge到Go tools中并被广泛接受。</p>
<p>包依赖管理工具在日常开发中并不需要太多的存在感，我们需要的这类工具特征是功能强大但接口“小”，对开发者体验好，不太需要太关心其运行原理，dep基本符合。dep日常操作最主要的三个命令：dep init、dep ensure和dep status。在<a href="http://tonybai.com/2017/06/08/first-glimpse-of-dep/">《初窥dep》</a>一文中，我曾重点说过dep init原理，这里就不重点说了，我们用一个例子来说说使用dep的日常workflow。</p>
<h3>1、dep init empty project</h3>
<p>我们可以对一个empty project或一个初具框架雏形的project进行init，这里init一个empty project，作为后续的示例基础：</p>
<pre><code>➜  $GOPATH/src/depdemo $dep init -v
Getting direct dependencies...
Checked 1 directories for packages.
Found 0 direct dependencies.
Root project is "depdemo"
 0 transitively valid internal packages
 0 external packages imported from 0 projects
(0)   ✓ select (root)
  ✓ found solution with 0 packages from 0 projects

Solver wall times by segment:
  select-root: 68.406µs
        other:  9.806µs

  TOTAL: 78.212µs

➜  $GOPATH/src/depdemo $ls
Gopkg.lock    Gopkg.toml    vendor/

➜  $GOPATH/src/depdemo $dep status
PROJECT  CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED

</code></pre>
<p>dep init有三个输出：Gopkg.lock、Gopkg.toml和vendor目录，其中Gopkg.toml（包含example，但注释掉了）和vendor都是空的，Gopkg.lock中仅包含了一些给<a href="https://github.com/golang/dep/tree/master/gps">gps</a>使用的metadata：</p>
<pre><code>➜  $GOPATH/src/depdemo git:(a337d5b) $cat Gopkg.lock
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7"
  solver-name = "gps-cdcl"
  solver-version = 1
</code></pre>
<h3>2、常规操作循环：for { 填代码 -> dep ensure }</h3>
<p>接下来的常规操作就是我们要为project添加代码了。我们先来为工程添加一个main.go文件，源码如下：</p>
<pre><code>// main.go
package main

import "fmt"

func main() {
    fmt.Println("depdemo")
}
</code></pre>
<p>这份代码的依赖只是std库的fmt，并没有使用第三方的依赖，因此当我们通过dep status查看当前状态、使用ensure去做同步时，发现dep并没有什么要做的：</p>
<pre><code>➜  $GOPATH/src/depdemo $dep status
PROJECT  CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED
➜  $GOPATH/src/depdemo $dep ensure -v
Gopkg.lock was already in sync with imports and Gopkg.toml
</code></pre>
<p>好吧。我们再来为main.go添点“有用”的内容：一段读取toml配置文件的代码。</p>
<pre><code>//data.toml
id = "12345678abcdefgh"
name = "tonybai"
city = "shenyang"

// main.go
package main

import (
    "fmt"
    "log"

    "github.com/BurntSushi/toml"
)

type Person struct {
    ID   string
    Name string
    City string
}

func main() {
    p := Person{}
    if _, err := toml.DecodeFile("./data.toml", &amp;p); err != nil {
        log.Fatal(err)
    }

    fmt.Println(p)
}

</code></pre>
<p>之后，再来执行dep status：</p>
<pre><code>➜  $GOPATH/src/depdemo $dep status
Lock inputs-digest mismatch due to the following packages missing from the lock:

PROJECT                     MISSING PACKAGES
github.com/BurntSushi/toml  [github.com/BurntSushi/toml]

This happens when a new import is added. Run `dep ensure` to install the missing packages.
input-digest mismatch
</code></pre>
<p>我们看到dep status检测到项目出现”不同步”的情况（代码中引用的toml包在Gopkg.lock中没有），并建议使用dep ensure命令去做一次sync。</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/2nd-issue/dep-ensure-typical-flow.png" alt="img{512x368}" /></p>
<p>我们来ensure一下(ensure的输入输出见上图)：</p>
<pre><code>$GOPATH/src/depdemo git:(master) $dep ensure -v
Root project is "depdemo"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)

(1)    ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try
(1)        try github.com/BurntSushi/toml@v0.3.0
(1)    ✓ select github.com/BurntSushi/toml@v0.3.0 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
     b-source-exists: 15.821158205s
... ...
  b-deduce-proj-root:       5.453µs

  TOTAL: 16.176846089s

(1/1) Wrote github.com/BurntSushi/toml@v0.3.0

</code></pre>
<p>我们来看看项目中的文件都发生了哪些变化：</p>
<pre><code>$git status
On branch master
Changes not staged for commit:
  (use "git add &lt;file&gt;..." to update what will be committed)
  (use "git checkout -- &lt;file&gt;..." to discard changes in working directory)

    modified:   Gopkg.lock

Untracked files:
  (use "git add &lt;file&gt;..." to include in what will be committed)

    vendor/
</code></pre>
<p>可以看到Gopkg.lock文件和vendor目录下发生了变化：</p>
<pre><code>$git diff

diff --git a/Gopkg.lock b/Gopkg.lock
index bef2d00..c5ae854 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -1,9 +1,15 @@
 # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.

+[[projects]]
+  name = "github.com/BurntSushi/toml"
+  packages = ["."]
+  revision = "b26d9c308763d68093482582cea63d69be07a0f0"
+  version = "v0.3.0"
+
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7"
+  inputs-digest = "25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d"
   solver-name = "gps-cdcl"
   solver-version = 1

$tree -L 2 vendor
vendor
└── github.com
    └── BurntSushi

</code></pre>
<p>可以看到Gopkg.lock中增加了toml包的依赖条目(版本v0.3.0)，input-digest这个元数据字段的值也发生了变更；并且vendor目录下多了toml包的源码，至此项目又到达了“同步”状态。</p>
<h3>3、添加约束</h3>
<p>大多数情况下，我们到这里就算完成了<strong>dep work flow的一次cycle</strong>，但如果你需要为第三方包的版本加上一些约束条件，那么dep ensure -add就会派上用场，比如说：我们要使用toml包的v0.2.x版本，而不是v0.3.0版本，我们需要为github.com/BurntSushi/toml添加一条约束：</p>
<pre><code>$dep ensure -v -add github.com/BurntSushi/toml@v0.2.0
Fetching sources...
(1/1) github.com/BurntSushi/toml@v0.2.0

Root project is "depdemo"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)
(1)    ? attempt github.com/BurntSushi/toml with 1 pkgs; at least 1 versions to try
(1)        try github.com/BurntSushi/toml@v0.3.0
(2)    ✗   github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0:
(2)        ^0.2.0 from (root)
(1)        try github.com/BurntSushi/toml@v0.2.0
(1)    ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
... ...

  TOTAL: 599.252392ms

(1/1) Wrote github.com/BurntSushi/toml@v0.2.0
</code></pre>
<p>add约束后，Gopkg.toml中增加了一条记录：</p>
<pre><code>// Gopkg.toml
[[constraint]]
  name = "github.com/BurntSushi/toml"
  version = "0.2.0"

</code></pre>
<p>Gopkg.lock中的toml条目的版本回退为v0.2.0：</p>
<pre><code>diff --git a/Gopkg.lock b/Gopkg.lock
index c5ae854..a557251 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -4,12 +4,12 @@
 [[projects]]
   name = "github.com/BurntSushi/toml"
   packages = ["."]
-  revision = "b26d9c308763d68093482582cea63d69be07a0f0"
-  version = "v0.3.0"
+  revision = "bbd5bb678321a0d6e58f1099321dfa73391c1b6f"
+  version = "v0.2.0"

 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d"
+  inputs-digest = "9fd144de0cc448be93418c927b5ce2a70e03ec7f260fa7e0867f970ff121c7d7"
   solver-name = "gps-cdcl"
   solver-version = 1

$dep status
PROJECT                     CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED
github.com/BurntSushi/toml  ^0.2.0      v0.2.0   bbd5bb6   v0.2.0  1

</code></pre>
<p>vendor目录下的toml包源码也回退到v0.2.0的源码。关于约束规则的构成语法，可以<a href="https://golang.github.io/dep/docs/Gopkg.toml.html#Version">参考dep文档</a>。</p>
<h3>4、revendor/update vendor</h3>
<p>使用<a href="http://tonybai.com/2015/07/31/understand-go15-vendor/">vendor机制</a>后，由于第三方依赖包修正bug或引入你需要的功能，revendor第三方依赖包版本或者叫update vendor会成为一个周期性的工作。比如：toml包做了一些bugfix，并发布了v0.2.1版本。在我的depdemo中，为了一并fix掉这些bug，我需要重新vendor toml包。之前我们加的constraint是满足升级到v0.2.1版本的，因此我们不需要重新设置constraints，我们只需要单独revendor toml即可，可以使用dep ensure -update 命令：</p>
<pre><code>$dep ensure -v -update github.com/BurntSushi/toml
Root project is "depdemo"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)
(1)    ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try
(1)        try github.com/BurntSushi/toml@v0.3.0
(2)    ✗   github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0:
(2)        ^0.2.0 from (root)
(1)        try github.com/BurntSushi/toml@v0.2.0
(1)    ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
  b-list-versions: 1m18.267880815s
  .... ...
  TOTAL: 1m57.118656393s
</code></pre>
<p>由于真实的toml并没有v0.2.1版本且没有v0.2.x版本，因此我们的dep ensure -update并没有真正获取到数据。vendor和Gopkg.lock都没有变化。</p>
<h3>5、dep日常操作小结</h3>
<p>下面这幅图包含了上述三个dep日常操作，可以直观地看出不同操作后，对项目带来的改变：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/2nd-issue/dep-daily-workflows.png" alt="img{512x368}" /></p>
<p>“工欲善其事，必先利其器”，熟练的掌握dep的日常操作流程对提升开发效率大有裨益。</p>
<h2>二、“超时等待退出”框架的一种实现</h2>
<p>很多时候，我们在程序中都要启动多个goroutine协作完成应用的业务逻辑，比如：</p>
<pre><code>func main() {
    go producer.Start()
    go consumer.Start()
    go watcher.Start()
    ... ...
}
</code></pre>
<p>启动容易停止难！当程序要退出时，最粗暴的方法就是不管三七二十一，main goroutine直接退出；优雅些的方式，也是*nix系统通常的作法是：通知一下各个Goroutine要退出了，然后等待一段时间后再真正退出。粗暴地直接退出的方式可能会导致业务数据的损坏、不完整或丢失。等待超时的方式虽然不能完全避免“损失”，但是它给了各个goroutine一个“挽救数据”的机会，可以尽可能地减少损失的程度。</p>
<p>但这些goroutine形态很可能不同，有些是server，有些可能是client worker或其manager，因此似乎很难用一种统一的框架全面管理他们的启动、运行和退出，于是我们缩窄“交互面”，我们只做“超时等待退出”。我们定义一个interface：</p>
<pre><code>type GracefullyShutdowner interface {
    Shutdown(waitTimeout time.Duration) error
}

</code></pre>
<p>这样，凡是实现了该interface的类型均可在程序退出时得到退出的通知，并有机会做退出前的最后清理工作。这里还提供了一个类似http.HandlerFunc的类型ShutdownerFunc ，用于将普通function转化为实现了GracefullyShutdowner interface的类型实例：</p>
<pre><code>type ShutdownerFunc func(time.Duration) error

func (f ShutdownerFunc) Shutdown(waitTimeout time.Duration) error {
    return f(waitTimeout)
}

</code></pre>
<h3>1、并发退出</h3>
<p>退出也至少有两种类型，一种是并发退出，这种退出方式下各个goroutine的退出先后次序对数据处理无影响；另外一种则是顺序退出，即各个goroutine之间的退出是必须按照一定次序进行的。我们先来说并发退出。上代码！</p>
<pre><code>// shutdown.go
func ConcurrencyShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error {
    c := make(chan struct{})

    go func() {
        var wg sync.WaitGroup
        for _, g := range shutdowners {
            wg.Add(1)
            go func(shutdowner GracefullyShutdowner) {
                shutdowner.Shutdown(waitTimeout)
                wg.Done()
            }(g)
        }
        wg.Wait()
        c &lt;- struct{}{}
    }()

    select {
    case &lt;-c:
        return nil
    case &lt;-time.After(waitTimeout):
        return errors.New("wait timeout")
    }
}
</code></pre>
<p>我们将各个GracefullyShutdowner接口的实现以一个变长参数的形式传入ConcurrencyShutdown函数。ConcurrencyShutdown函数实现也很简单，通过：</p>
<ul>
<li>为每个shutdowner启动一个goroutine实现并发退出，并将timeout参数传入shutdowner的Shutdown方法中；</li>
<li>sync.WaitGroup在外层等待每个goroutine的退出；</li>
<li>通过select一个退出指示<a href="http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/">channel</a>和time.After返回的<a href="http://tonybai.com/2016/12/21/how-to-use-timer-reset-in-golang-correctly/">timer</a> channel来决定到底是正常退出还是超时退出。</li>
</ul>
<p>该函数的具体使用方法可以参考：shutdown_test.go。</p>
<pre><code>//shutdown_test.go
func shutdownMaker(processTm int) func(time.Duration) error {
    return func(time.Duration) error {
        time.Sleep(time.Second * time.Duration(processTm))
        return nil
    }
}

func TestConcurrencyShutdown(t *testing.T) {
    f1 := shutdownMaker(2)
    f2 := shutdownMaker(6)

    err := ConcurrencyShutdown(time.Duration(10)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2))
    if err != nil {
        t.Errorf("want nil, actual: %s", err)
        return
    }

    err = ConcurrencyShutdown(time.Duration(4)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2))
    if err == nil {
        t.Error("want timeout, actual nil")
        return
    }
}
</code></pre>
<h3>2、串行退出</h3>
<p>有了并发退出作为基础，串行退出也很简单了！</p>
<pre><code>//shutdown.go
func SequentialShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error {
    start := time.Now()
    var left time.Duration

    for _, g := range shutdowners {
        elapsed := time.Since(start)
        left = waitTimeout - elapsed

        c := make(chan struct{})
        go func(shutdowner GracefullyShutdowner) {
            shutdowner.Shutdown(left)
            c &lt;- struct{}{}
        }(g)

        select {
        case &lt;-c:
            //continue
        case &lt;-time.After(left):
            return errors.New("wait timeout")
        }
    }

    return nil
}
</code></pre>
<p>串行退出的一个问题是waitTimeout的确定，因为这个超时时间是所有goroutine的退出时间之和。在上述代码里，我把每次的lefttime传入下一个要执行的goroutine的Shutdown方法中，外部select也同样使用这个left作为timeout的值。对照ConcurrencyShutdown，SequentialShutdown更简单，这里就不详细说了。</p>
<h3>3、小结</h3>
<p>这是一个可用的、抛砖引玉式的实现，但还有很多改进空间，比如：可以考虑一下获取每个shutdowner.Shutdown后的返回值(error)，留给大家自行考量吧。</p>
<h2>三、Testcase的setUp和tearDown</h2>
<p>Go语言自带<a href="http://tonybai.com/2014/10/22/golang-testing-techniques/">testing框架</a>，事实证明这是Go语言的一个巨大优势之一，Gopher们也非常喜欢这个testing包。但Testing这个事情比较复杂，有些场景还需要我们自己动脑筋在标准testing框架下实现需要的功能，比如：当测试代码需要访问外部数据库、Redis或连接远端server时。遇到这种情况，很多人想到了Mock，没错。Mock技术在一定程度上可以解决这些问题，但如果使用mock技术，业务代码就得为了test而去做一层抽象，提升了代码理解的难度，在有些时候这还真不如直接访问真实的外部环境。</p>
<p>这里先不讨论这两种方式的好坏优劣，这里仅讨论如果在testing中访问真实环境我们该如何测试。在经典<a href="http://tonybai.com/tag/Unittest">单元测试</a>框架中，我们经常能看到setUp和tearDown两个方法，它们分别用于在testcase执行之前初始化testcase的执行环境以及在testcase执行后清理执行环境，以保证每两个testcase之间都是独立的、互不干扰的。在真实环境下进行测试，我们也可以利用setUp和tearDown来为每个testcase初始化和清理case依赖的真实环境。</p>
<p>setUp和tearDown也是有级别的，有全局级、testsuite级以及testcase级。在Go中，在标准testing框架下，我们接触到的是全局级和testcase级别。Go中对全局级的setUp和tearDown的支持还要追溯到<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4</a>，<a href="https://golang.org/doc/go1.4">Go 1.4</a>引入了TestMain方法，支持在诸多testcase执行之前为测试代码添加自定义setUp，以及在testing执行之后进行tearDown操作，例如：</p>
<pre><code>func TestMain(m *testing.M) {
    err := setup()
    if err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }

    r := m.Run()
    teardown()

    os.Exit(r)
}
</code></pre>
<p>但在testcase级别，Go testing包并没有提供方法上的支持。在2017年的<a href="https://www.gophercon.com/">GopherCon</a>大会上，<a href="https://hashicorp.com/">Hashicorp</a>的创始人<a href="https://github.com/mitchellh">Mitchell Hashimoto</a>做了题为：<a href="https://www.youtube.com/watch?v=8hQG7QlcLBk">“Advanced Testing in Go”</a>的主题演讲，这份资料里提出了一种较为优雅的为testcase进行setUp和teawDown的方法：</p>
<pre><code>//setup-teardown-demo/foo_test.go
package foo_test

import (
    "fmt"
    "testing"
)

func setUp(t *testing.T, args ...interface{}) func() {
    fmt.Println("testcase setUp")
    // use t and args

    return func() {
        // use t
        // use args
        fmt.Println("testcase tearDown")
    }
}

func TestXXX(t *testing.T) {
    defer setUp(t)()
    fmt.Println("invoke testXXX")
}
</code></pre>
<p>这个方案充分利用了函数这个first-class type以及闭包的作用，每个Testcase可以定制自己的setUp和tearDown，也可以使用通用的setUp和tearDown，执行的效果如下：</p>
<pre><code>$go test -v .
=== RUN   TestXXX
testcase setUp
invoke testXXX
testcase tearDown
--- PASS: TestXXX (0.00s)
PASS
ok      github.com/bigwhite/experiments/writing-go-code-issues/2nd-issue/setup-teardown-demo    0.010s

</code></pre>
<h2>四、错误处理</h2>
<p>本来想码一些关于Go错误处理的文字，但发现自己在2015年就写过一篇旧文<a href="http://tonybai.com/2015/10/30/error-handling-in-go/">《Go语言错误处理》</a>，对Go错误处理的方方面面总结的很全面了。即便到今天也不过时，这当然也得益于<a href="https://golang.org/doc/go1compat">Go1兼容规范</a>的存在。因此有兴趣于此的朋友们，请移步到<a href="http://tonybai.com/2015/10/30/error-handling-in-go/">《Go语言错误处理》</a>这篇文章吧。</p>
<p>注：本文所涉及的示例代码，请到<a href="https://github.com/bigwhite/experiments/tree/master/writing-go-code-issues/2nd-issue">这里</a>下载。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/01/27/the-problems-i-encountered-when-writing-go-code-issue-2nd/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Golang测试技术</title>
		<link>https://tonybai.com/2014/10/22/golang-testing-techniques/</link>
		<comments>https://tonybai.com/2014/10/22/golang-testing-techniques/#comments</comments>
		<pubDate>Wed, 22 Oct 2014 07:57:21 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Concurrency]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[Package]]></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>

		<guid isPermaLink="false">http://tonybai.com/?p=1579</guid>
		<description><![CDATA[本篇文章内容来源于Golang核心开发组成员Andrew Gerrand在Google I/O 2014的一次主题分享&#8220;Testing Techniques&#8221;，即介绍使用Golang开发 时会使用到的测试技术（主要针对单元测试），包括基本技术、高级技术（并发测试、mock/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等）等，感觉总结的很全面，这里整理记录下来，希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下：Golang官方站的slide都是以一种特有的golang artical的格式放出的（用这个工具http://go-talks.appspot.com/可以在线观看），没法像pdf那样下载，在国内使用和传播极其不便。 一、基础测试技术 1、测试Go代码 Go语言内置测试框架。 内置的测试框架通过testing包以及go test命令来提供测试功能。 下面是一个完整的测试strings.Index函数的完整测试文件： //strings_test.go (这里样例代码放入strings_test.go文件中) package strings_test import ( &#160;&#160;&#160; &#34;strings&#34; &#160;&#160;&#160; &#34;testing&#34; ) func TestIndex(t *testing.T) { &#160;&#160;&#160; const s, sep, want = &#34;chicken&#34;, &#34;ken&#34;, 4 &#160;&#160;&#160; got := strings.Index(s, sep) &#160;&#160;&#160; if got != want { &#160;&#160;&#160;&#160;&#160;&#160;&#160; t.Errorf(&#34;Index(%q,%q) = %v; want [...]]]></description>
			<content:encoded><![CDATA[<p>本篇文章内容来源于<a href="http://golang.org">Golang</a>核心开发组成员<a href="http://nf.wh3rd.net/">Andrew Gerrand</a>在Google I/O 2014的一次主题分享&ldquo;<a href="https://talks.golang.org/2014/testing.slide#1">Testing Techniques</a>&rdquo;，即介绍使用Golang开发 时会使用到的测试技术（主要针对<a href="http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/"><b>单元测试</b></a>），包括基本技术、高级技术（并发测试、<a href="http://tonybai.com/2010/10/29/lcut-add-mock-support/">mock</a>/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等）等，感觉总结的很全面，这里整理记录下来，希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下：Golang官方站的slide都是以一种特有的<a href="http://godoc.org/code.google.com/p/go.tools/present">golang artical</a>的格式放出的（用这个工具http://go-talks.appspot.com/可以在线观看），没法像pdf那样下载，在国内使用和传播极其不便。</p>
<p><b>一、基础测试技术</b></p>
<p><b>1、测试Go代码</b></p>
<p>Go语言内置测试框架。</p>
<p>内置的测试框架通过<font face="Courier New">testing</font>包以及<font face="Courier New">go test</font>命令来提供测试功能。</p>
<p>下面是一个完整的测试<font face="Courier New">strings.Index</font>函数的完整测试文件：</p>
<p><font face="Courier New">//strings_test.go (这里样例代码放入strings_test.go文件中)</font><br />
	<font face="Courier New">package strings_test</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;strings&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;testing&quot;<br />
	)</font></p>
<p><font face="Courier New">func TestIndex(t *testing.T) {<br />
	&nbsp;&nbsp;&nbsp; const s, sep, want = &quot;chicken&quot;, &quot;ken&quot;, 4<br />
	&nbsp;&nbsp;&nbsp; got := strings.Index(s, sep)<br />
	&nbsp;&nbsp;&nbsp; if got != want {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; t.Errorf(&quot;Index(%q,%q) = %v; want %v&quot;, s, sep, got, want)//<u>注意原slide中</u><u>的got和want写反了</u><br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">$go test -v strings_test.go<br />
	=== RUN TestIndex<br />
	&#8212; PASS: TestIndex (0.00 seconds)<br />
	PASS<br />
	ok&nbsp; &nbsp;&nbsp;&nbsp; command-line-arguments&nbsp;&nbsp;&nbsp; 0.007s</font></p>
<p>go test的-v选项是表示输出详细的执行信息。</p>
<p>将代码中的want常量值修改为3，我们制造一个无法通过的测试：</p>
<p><font face="Courier New">$go test -v strings_test.go<br />
	=== RUN TestIndex<br />
	&#8212; FAIL: TestIndex (0.00 seconds)<br />
	&nbsp;&nbsp;&nbsp; strings_test.go:12: Index(&quot;chicken&quot;,&quot;ken&quot;) = 4; want 3<br />
	FAIL<br />
	exit status 1<br />
	FAIL&nbsp;&nbsp;&nbsp; command-line-arguments&nbsp;&nbsp;&nbsp; 0.008s</font></p>
<p><b>2、表驱动测试</b></p>
<p>Golang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。</p>
<p><font face="Courier New">package strings_test</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;strings&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;testing&quot;<br />
	)</font></p>
<p><font face="Courier New">func TestIndex(t *testing.T) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var tests = []struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sep string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; out int<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&quot;&quot;, &quot;&quot;, 0},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&quot;&quot;, &quot;a&quot;, -1},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&quot;fo&quot;, &quot;foo&quot;, -1},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&quot;foo&quot;, &quot;foo&quot;, 0},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&quot;oofofoofooo&quot;, &quot;f&quot;, 2},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // etc<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for _, test := range tests {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; actual := strings.Index(test.s, test.sep)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if actual != test.out {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; t.Errorf(&quot;Index(%q,%q) = %v; want %v&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test.s, test.sep, actual, test.out)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">$go test -v strings_test.go<br />
	=== RUN TestIndex<br />
	&#8212; PASS: TestIndex (0.00 seconds)<br />
	PASS<br />
	ok&nbsp; &nbsp;&nbsp;&nbsp; command-line-arguments&nbsp;&nbsp;&nbsp; 0.007s</font></p>
<p><b>3、T结构</b></p>
<p><font face="Courier New">*testing.T</font>参数用于错误报告：</p>
<p><font face="Courier New">t.Errorf(&quot;got bar = %v, want %v&quot;, got, want)<br />
	t.Fatalf(&quot;Frobnicate(%v) returned error: %v&quot;, arg, err)<br />
	t.Logf(&quot;iteration %v&quot;, i)</font></p>
<p>也可以用于enable并行测试(parallet test)：<br />
	<font face="Courier New">t.Parallel()</font></p>
<p>控制一个测试是否运行：</p>
<p><font face="Courier New">if runtime.GOARCH == &quot;arm&quot; {<br />
	&nbsp;&nbsp;&nbsp; t.Skip(&quot;this doesn&#39;t work on ARM&quot;)<br />
	}</font></p>
<p><b>4、运行测试</b></p>
<p>我们用<font face="Courier New">go test</font>命令来运行特定包的测试。</p>
<p>默认执行当前路径下包的测试代码。</p>
<p><font face="Courier New">$ go test<br />
	PASS</font></p>
<p><font face="Courier New">$ go test -v<br />
	=== RUN TestIndex<br />
	&#8212; PASS: TestIndex (0.00 seconds)<br />
	PASS</font></p>
<p><font face="Courier New">要运行工程下的所有测试，我们执行如下命令：</font></p>
<p><font face="Courier New">$ go test github.com/nf/&#8230;</font></p>
<p><font face="Courier New">标准库的测试：<br />
	$ go test std</font></p>
<p>注：假设<font face="Courier New">strings_test.go</font>的当前目录为<font face="Courier New">testgo</font>，在testgo目录下执行<font face="Courier New">go test</font>都是OK的。但如果我们切换到testgo的上一级目录执行go test，我们会得到什么结果呢？</p>
<p><font face="Courier New">$go test testgo<br />
	can&#39;t load package: package testgo: cannot find package &quot;testgo&quot; in any of:<br />
	&nbsp;&nbsp;&nbsp; /usr/local/go/src/pkg/testgo (from $GOROOT)<br />
	&nbsp;&nbsp;&nbsp; /Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)</font></p>
<p>提示找不到testgo这个包，go test后面接着的应该是一个包名，go test会在GOROOT和GOPATH下查找这个包并执行包的测试。</p>
<p><b>5、测试覆盖率</b></p>
<p>go tool命令可以报告测试覆盖率统计。</p>
<p>我们在testgo下执行go test -cover，结果如下：</p>
<p><font face="Courier New">go build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo<br />
	FAIL&nbsp;&nbsp;&nbsp; _/Users/tony/Test/Go/testgo [build failed]</font></p>
<p>显然通过cover参数选项计算测试覆盖率不仅需要测试代码，还要有被测对象（一般是函数）的源码文件。</p>
<p>我们将目录切换到$GOROOT/src/pkg/strings下，执行<font face="Courier New">go test -cover</font>：</p>
<p><font face="Courier New">$go test -v -cover<br />
	=== RUN TestReader<br />
	&#8212; PASS: TestReader (0.00 seconds)<br />
	&#8230; &#8230;<br />
	=== RUN: ExampleTrimPrefix<br />
	&#8212; PASS: ExampleTrimPrefix (1.75us)<br />
	PASS<br />
	coverage: 96.9% of statements<br />
	ok&nbsp; &nbsp;&nbsp;&nbsp; strings&nbsp;&nbsp;&nbsp; 0.612s</font></p>
<p>go test可以生成覆盖率的profile文件，这个文件可以被go tool cover工具解析。</p>
<p>在$GOROOT/src/pkg/strings下面执行：</p>
<p><font face="Courier New">$ go test -coverprofile=cover.out</font></p>
<p>会再当前目录下生成cover.out文件。</p>
<p>查看cover.out文件，有两种方法：</p>
<p>a) <font face="Courier New">cover -func=cover.out</font></p>
<p><font face="Courier New">$sudo go tool cover -func=cover.out<br />
	strings/reader.go:24:&nbsp;&nbsp;&nbsp; Len&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 66.7%<br />
	strings/reader.go:31:&nbsp;&nbsp;&nbsp; Read&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 100.0%<br />
	strings/reader.go:44:&nbsp;&nbsp;&nbsp; ReadAt&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 100.0%<br />
	strings/reader.go:59:&nbsp;&nbsp;&nbsp; ReadByte&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 100.0%<br />
	strings/reader.go:69:&nbsp;&nbsp;&nbsp; UnreadByte&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 100.0%<br />
	&#8230; &#8230;<br />
	strings/strings.go:638:&nbsp;&nbsp;&nbsp; Replace&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 100.0%<br />
	strings/strings.go:674:&nbsp;&nbsp;&nbsp; EqualFold&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 100.0%<br />
	total:&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; (statements)&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 96.9%</font></p>
<p>b) 可视化查看</p>
<p>执行go tool cover -html=cover.out命令，会在/tmp目录下生成目录coverxxxxxxx，比如/tmp/cover404256298。目录下有一个 coverage.html文件。用浏览器打开coverage.html，即可以可视化的查看代码的测试覆盖情况。</p>
<pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: 'Droid Sans Mono', 'Courier New', monospace; font-size: 18px; line-height: 24px; letter-spacing: -1px; color: rgb(0, 0, 0);">&nbsp;</pre>
<p>关于go tool的cover命令，我的go version go1.3 darwin/amd64默认并不自带，需要通过go get下载。</p>
<p><font face="Courier New">$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover</font></p>
<p>下载后，cover安装在<font face="Courier New">$GOROOT/pkg/tool/darwin_amd64</font>下面。</p>
<p><b>二、高级测试技术</b></p>
<p><b>1、一个例子程序</b></p>
<p>outyet是一个web服务，用于宣告某个特定Go版本是否已经打标签发布了。其获取方法：</p>
<p><font face="Courier New">go get github.com/golang/example/outyet</font></p>
<p>注：<br />
	go get执行后，<font face="Courier New">cd $GOPATH/src/github.com/golang/example/outyet</font>下，执行<font face="Courier New">go run main.go</font>。然后用浏览器打开<font face="Courier New">http://localhost:8080</font>即可访问该Web服务了。</p>
<p><b>2、测试Http客户端和服务端</b></p>
<p>net/http/httptest包提供了许多帮助函数，用于测试那些发送或处理Http请求的代码。</p>
<p><b>3、httptest.Server</b></p>
<p>httptest.Server在本地回环网口的一个系统选择的端口上listen。它常用于端到端的HTTP测试。</p>
<p><font face="Courier New">type Server struct {<br />
	&nbsp;&nbsp;&nbsp; URL&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string // base URL of form http://ipaddr:port with no trailing slash<br />
	&nbsp;&nbsp;&nbsp; Listener net.Listener</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // TLS is the optional TLS configuration, populated with a new config<br />
	&nbsp;&nbsp;&nbsp; // after TLS is started. If set on an unstarted server before StartTLS<br />
	&nbsp;&nbsp;&nbsp; // is called, existing fields are copied into the new config.<br />
	&nbsp;&nbsp;&nbsp; TLS *tls.Config</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // Config may be changed after calling NewUnstartedServer and<br />
	&nbsp;&nbsp;&nbsp; // before Start or StartTLS.<br />
	&nbsp;&nbsp;&nbsp; Config *http.Server<br />
	}</font></p>
<p><font face="Courier New">func NewServer(handler http.Handler) *Server</font></p>
<p><font face="Courier New">func (*Server) Close() error</font></p>
<p><b>4、httptest.Server实战</b></p>
<p>下面代码创建了一个临时Http Server，返回简单的Hello应答：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Fprintln(w, &quot;Hello, client&quot;)<br />
	&nbsp;&nbsp;&nbsp; }))<br />
	&nbsp;&nbsp;&nbsp; defer ts.Close()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; res, err := http.Get(ts.URL)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatal(err)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; greeting, err := ioutil.ReadAll(res.Body)<br />
	&nbsp;&nbsp;&nbsp; res.Body.Close()<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatal(err)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%s&quot;, greeting)</font></p>
<p><b>5、httptest.ResponseRecorder</b></p>
<p>httptest.ResponseRecorder是http.ResponseWriter的一个实现，用来记录变化，用在测试的后续检视中。</p>
<p><font face="Courier New">type ResponseRecorder struct {<br />
	&nbsp;&nbsp;&nbsp; Code&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // the HTTP response code from WriteHeader<br />
	&nbsp;&nbsp;&nbsp; HeaderMap http.Header&nbsp;&nbsp; // the HTTP response headers<br />
	&nbsp;&nbsp;&nbsp; Body&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to<br />
	&nbsp;&nbsp;&nbsp; Flushed&nbsp;&nbsp; bool<br />
	}</font></p>
<p><b>6、httptest.ResponseRecorder实战</b></p>
<p>向一个HTTP handler中传入一个ResponseRecorder，通过它我们可以来检视生成的应答。</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; handler := func(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http.Error(w, &quot;something failed&quot;, http.StatusInternalServerError)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; req, err := http.NewRequest(&quot;GET&quot;, &quot;http://example.com/foo&quot;, nil)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatal(err)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; w := httptest.NewRecorder()<br />
	&nbsp;&nbsp;&nbsp; handler(w, req)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%d &#8211; %s&quot;, w.Code, w.Body.String())</font></p>
<p><b>7、竞争检测(race detection)</b></p>
<p>当两个goroutine并发访问同一个变量，且至少一个goroutine对变量进行写操作时，就会发生数据竞争（data race）。</p>
<p>为了协助诊断这种bug，Go提供了一个内置的数据竞争检测工具。</p>
<p>通过传入-race选项，go tool就可以启动竞争检测。</p>
<p><font face="Courier New">$ go test -race mypkg&nbsp;&nbsp;&nbsp; // to test the package<br />
	$ go run -race mysrc.go&nbsp; // to run the source file<br />
	$ go build -race mycmd&nbsp;&nbsp; // to build the command<br />
	$ go install -race mypkg // to install the package</font></p>
<p>注：一个数据竞争检测的例子</p>
<p>例子代码：</p>
<p><font face="Courier New">//testrace.go</font></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;<br />
	import &quot;time&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var i int = 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i++<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;subroutine: i = &quot;, i)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(1 * time.Second)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i++<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;mainroutine: i = &quot;, i)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(1 * time.Second)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">$go run -race testrace.go<br />
	mainroutine: i =&nbsp; 1<br />
	==================<br />
	<b>WARNING: DATA RACE</b><br />
	Read by goroutine 5:<br />
	&nbsp; main.func&middot;001()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /Users/tony/Test/Go/testrace.go:10 +0&#215;49</font></p>
<p><font face="Courier New">Previous write by main goroutine:<br />
	&nbsp; main.main()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /Users/tony/Test/Go/testrace.go:17 +0xd5</font></p>
<p><font face="Courier New">Goroutine 5 (running) created at:<br />
	&nbsp; main.main()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /Users/tony/Test/Go/testrace.go:14 +0xaf<br />
	==================<br />
	subroutine: i =&nbsp; 2<br />
	mainroutine: i =&nbsp; 3<br />
	subroutine: i =&nbsp; 4<br />
	mainroutine: i =&nbsp; 5<br />
	subroutine: i =&nbsp; 6<br />
	mainroutine: i =&nbsp; 7<br />
	subroutine: i =&nbsp; 8</font></p>
<p><b>8、测试并发</b><b>（testing with concurrency)</b></p>
<p>当测试并发代码时，总会有一种使用sleep的冲动。大多时间里，使用sleep既简单又有效。</p>
<p>但大多数时间不是&rdquo;总是&ldquo;。</p>
<p>我们可以使用Go的并发原语让那些奇怪不靠谱的sleep驱动的测试更加值得信赖。</p>
<p><b>9、</b><b>使用静态分析工具vet查找错误</b></p>
<p>vet工具用于检测代码中程序员犯的常见错误：<br />
	&nbsp;&nbsp;&nbsp; &#8211; 错误的printf格式<br />
	&nbsp;&nbsp;&nbsp; &#8211; 错误的构建tag<br />
	&nbsp;&nbsp;&nbsp; &#8211; 在闭包中使用错误的range循环变量<br />
	&nbsp;&nbsp;&nbsp; &#8211; 无用的赋值操作<br />
	&nbsp;&nbsp;&nbsp; &#8211; 无法到达的代码<br />
	&nbsp;&nbsp;&nbsp; &#8211; 错误使用mutex<br />
	&nbsp;&nbsp;&nbsp; 等等。</p>
<p>使用方法：<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; go vet [package]</font></p>
<p><b>10、</b><b>从内部测试</b></p>
<p>golang中大多数测试代码都是被测试包的源码的一部分。这意味着测试代码可以访问包种未导出的符号以及内部逻辑。就像我们之前看到的那样。</p>
<p>注：比如$GOROOT/src/pkg/path/path_test.go与path.go都在path这个包下。</p>
<p><b>11、从外部测试</b></p>
<p>有些时候，你需要从被测包的外部对被测包进行测试，比如测试代码在package foo_test下，而不是在package foo下。</p>
<p>这样可以打破依赖循环，比如：</p>
<p>&nbsp;&nbsp;&nbsp; &#8211; testing包使用fmt<br />
	&nbsp;&nbsp;&nbsp; &#8211; fmt包的测试代码还必须导入testing包<br />
	&nbsp;&nbsp;&nbsp; &#8211; 于是，fmt包的测试代码放在fmt_test包下，这样既可以导入testing包，也可以同时导入fmt包。</p>
<p><b>12、Mocks和fakes</b></p>
<p>通过在代码中使用interface，Go可以避免使用mock和fake测试机制。</p>
<p>例如，如果你正在编写一个文件格式解析器，不要这样设计函数：</p>
<p><font face="Courier New">func Parser(f *os.File) error</font></p>
<p>作为替代，你可以编写一个接受interface类型的函数:</p>
<p><font face="Courier New">func Parser(r io.Reader) error</font></p>
<p>和<font face="Courier New">bytes.Buffer、strings.Reader</font>一样，*os.File也实现了<font face="Courier New">io.Reader</font>接口。</p>
<p><b>13、子进程测试</b></p>
<p>有些时候，你需要测试的是一个进程的行为，而不仅仅是一个函数。例如：</p>
<p><font face="Courier New">func Crasher() {<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Going down in flames!&quot;)<br />
	&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	}</font></p>
<p>为了测试上面的代码，我们将测试程序本身作为一个子进程进行测试：</p>
<p><font face="Courier New">func TestCrasher(t *testing.T) {<br />
	&nbsp;&nbsp;&nbsp; if os.Getenv(&quot;BE_CRASHER&quot;) == &quot;1&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Crasher()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; cmd := exec.Command(os.Args[0], &quot;-test.run=TestCrasher&quot;)<br />
	&nbsp;&nbsp;&nbsp; cmd.Env = append(os.Environ(), &quot;BE_CRASHER=1&quot;)<br />
	&nbsp;&nbsp;&nbsp; err := cmd.Run()<br />
	&nbsp;&nbsp;&nbsp; if e, ok := err.(*exec.ExitError); ok &amp;&amp; !e.Success() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; t.Fatalf(&quot;process ran with err %v, want exit status 1&quot;, err)<br />
	}</font></p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/22/golang-testing-techniques/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>代码是怎么腐化的</title>
		<link>https://tonybai.com/2013/11/12/how-code-corrupt/</link>
		<comments>https://tonybai.com/2013/11/12/how-code-corrupt/#comments</comments>
		<pubDate>Tue, 12 Nov 2013 15:52:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[思考控]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Coding-Review]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Refactoring]]></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">http://tonybai.com/?p=1443</guid>
		<description><![CDATA[新三年，旧三年，修修补补又三年。 &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160;&#160; &#8212; 中国俗语。 上面的这句俗语用来形容很多遗留软件系统(legacy software system)的现状是再合适不过了。 今天下午做了一下午的代码评审，对象是一个运行了7年的遗留系统。会上除了几处明显的代码逻辑错误我发言指了出来外，涉及业务流程以及代码设计的问题，我 大多保持沉默。因为我清楚，即便我明确指出问题，可能也得不到修正。也许参与评审代码的其他同事也都知晓这些问题，只是觉得现在还不能去改&#8230;。 为何不能去改？回想当年第一版代码出炉时，它是那么的&#8220;出淤泥而不染&#8221;，而如今它身材臃肿，满身赘肉，千疮百孔，让人无处下手，看起来都不舒服。虽然它依旧能坚持工作，但终会有一天，它会轰然倒地，酿成大错。为何代码会腐化到如此地步？是怎么腐化的？这里谈几点体会。 * 第一版的设计和实现水准 我们知道：设计再完美的系统也终会有腐化的一天，时间才是最可怕的武器，它是一把无情的刻刀，不仅能使人老去，逻辑抽象世界的程序也不能幸免。但是你的程 序设计和实现的越完美，这个衰老和腐化的周期就会越长。一个糟糕的初始设计和实现，只会让系统更早的发霉、腐败直至被替换。我们的这个legecy系统在 最初的代码设计上就给后人留下了许多&#8220;糟糕的参照物&#8221;，在后人无以伦比的&#8220;复制粘贴&#8221;的技能下，这些糟糕的设计和实现就像癌细胞一样扩散到系统机体的每个 角落，让你无法重新建立其免疫系统。 * 没有测试做保障，不敢改 系统从诞生的那天起就使用&#8220;人肉测试&#8221;的方式，而且是粗粒度的系统功能测试。没有白盒的、可重复使用的、自动执行的单元测试集做保障，以致没有人敢对其轻举妄动，一旦出了问题，后果自负。另外人肉测试，精力和成本双重消耗，测试人员&#8220;耗&#8221;不起啊。 * 缺少崇尚&#8220;美&#8221;的文化 记得组织第一版企业文化中&#8220;美&#8221;这个成分还有立足之地的，但后期改版后，我们就再也看不到&#8220;美&#8221;了。码农们也是一样，在缺少了&#8220;俱美&#8221;之风的吹拂下，大家 都变得有些&#8220;丑陋&#8221;了，大家都开始摒弃&#8220;美&#8221;，认为代码只要能工作即可，美不美不打紧，于是工作就变成了修修补补，系统变得日益臃肿，臭气熏天。 * 进度之忧高悬 很多人会说：&#8220;如果给我充足的时间，我会让系统获得新生。但我没有时间，客户那边催的急，只能下次再重构、完善和优化了吧&#8221;。从这里不知大家是否看出了：&#8220;改善是没有的，下次却是永恒的&#8221;这一&#8220;道理&#8221;。在进度堪忧的情况下，我们多数时候选择了屈服，而不是自己的原则。 * 成本，老板们不得不面对的 &#8220;能用即可，新三年，旧三年，修修补补又三年&#8221;。如果你和老板谈重构、谈优化，那么这句话就是老板最好的拖词儿。成本永远是悬在老板头上的一把宝剑，老板们不能视而不见。因此，如果你要和老板谈重写遗留系统，那得需要多么强大的魄力和坚定的意志啊。 * 拷贝粘贴，最好的伙伴 or 最大的敌人 相信发明复制、粘贴以及剪切板的那位仁兄无论如何也没有想到，自己送给世界的礼物，竟然被码农们操练的如此熟练，应用在各种场合，尤其是Coding中。代码中充斥着大量重复代码信息，都是拷贝、复制和粘贴的结果，于是乎代码腐败的最大敌人就此出现了。 牢记：&#8220;千里大堤，溃于蝼蚁&#8221;。下一步该怎么办？去闻闻，你的代码腐化了没有！ &#169; 2013, bigwhite. 版权所有.]]></description>
			<content:encoded><![CDATA[<p><i>新三年，旧三年，修修补补又三年。<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; &#8212; 中国俗语。</i></p>
<p>上面的这句俗语用来形容很多<a href="http://en.wikipedia.org/wiki/Legacy_system‎">遗留软件系统</a>(legacy software system)的现状是再合适不过了。</p>
<p>今天下午做了一下午的<a href="http://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/">代码评审</a>，对象是一个运行了7年的遗留系统。会上除了几处明显的代码逻辑错误我发言指了出来外，涉及业务流程以及代码设计的问题，我 大多保持沉默。因为我清楚，即便我明确指出问题，可能也得不到修正。也许参与评审代码的其他同事也都知晓这些问题，只是觉得现在还不能去改&#8230;。</p>
<p>为何不能去改？回想当年第一版代码出炉时，它是那么的&ldquo;出淤泥而不染&rdquo;，而如今它身材臃肿，满身赘肉，千疮百孔，让人无处下手，看起来都不舒服。虽然它依旧能坚持工作，但终会有一天，它会轰然倒地，酿成大错。为何代码会腐化到如此地步？是怎么腐化的？这里谈几点体会。</p>
<p><b>* 第一版的设计和实现水准</b></p>
<p>我们知道：设计再完美的系统也终会有腐化的一天，时间才是最可怕的武器，它是一把无情的刻刀，不仅能使人老去，逻辑抽象世界的程序也不能幸免。但是你的程 序设计和实现的越完美，这个衰老和腐化的周期就会越长。一个糟糕的初始设计和实现，只会让系统更早的发霉、腐败直至被替换。我们的这个legecy系统在 最初的代码设计上就给后人留下了许多&ldquo;糟糕的参照物&rdquo;，在后人无以伦比的&ldquo;复制粘贴&rdquo;的技能下，这些糟糕的设计和实现就像癌细胞一样扩散到系统机体的每个 角落，让你无法重新建立其免疫系统。</p>
<p><b>* 没有测试做保障，不敢改</b></p>
<p>系统从诞生的那天起就使用&ldquo;人肉测试&rdquo;的方式，而且是粗粒度的系统功能测试。没有白盒的、可重复使用的、自动执行的<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/">单元测试</a>集做保障，以致没有人敢对其轻举妄动，一旦出了问题，后果自负。另外人肉测试，精力和成本双重消耗，测试人员&ldquo;耗&rdquo;不起啊。</p>
<p><b>* 缺少崇尚&ldquo;美&rdquo;的文化</b></p>
<p>记得组织第一版企业文化中&ldquo;美&rdquo;这个成分还有立足之地的，但后期改版后，我们就再也看不到&ldquo;美&rdquo;了。码农们也是一样，在缺少了&ldquo;俱美&rdquo;之风的吹拂下，大家 都变得有些&ldquo;丑陋&rdquo;了，大家都开始摒弃&ldquo;美&rdquo;，认为代码只要能工作即可，美不美不打紧，于是工作就变成了修修补补，系统变得日益臃肿，臭气熏天。</p>
<p><b>* 进度之忧高悬</b></p>
<p>很多人会说：&ldquo;如果给我充足的时间，我会让系统获得新生。但我没有时间，客户那边催的急，只能下次再重构、完善和优化了吧&rdquo;。从这里不知大家是否看出了：&ldquo;改善是没有的，下次却是永恒的&rdquo;这一&ldquo;道理&rdquo;。在进度堪忧的情况下，我们多数时候选择了屈服，而不是自己的原则。</p>
<p><b>* 成本，老板们不得不面对的</b></p>
<p>&ldquo;能用即可，新三年，旧三年，修修补补又三年&rdquo;。如果你和老板谈重构、谈优化，那么这句话就是老板最好的拖词儿。成本永远是悬在老板头上的一把宝剑，老板们不能视而不见。因此，如果你要和老板谈重写遗留系统，那得需要多么强大的魄力和坚定的意志啊。</p>
<p><b>* 拷贝粘贴，最好的伙伴 or 最大的敌人</b></p>
<p>相信发明复制、粘贴以及剪切板的那位仁兄无论如何也没有想到，自己送给世界的礼物，竟然被码农们操练的如此熟练，应用在各种场合，尤其是Coding中。代码中充斥着大量重复代码信息，都是拷贝、复制和粘贴的结果，于是乎代码腐败的最大敌人就此出现了。</p>
<p><span style="line-height: 1.6em;">牢记：&ldquo;</span><b style="line-height: 1.6em;">千里大堤，溃于蝼蚁</b><span style="line-height: 1.6em;">&rdquo;。下一步该怎么办？去闻闻，你的代码腐化了没有！</span></p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/11/12/how-code-corrupt/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>lcut 0.3.0版本发布</title>
		<link>https://tonybai.com/2012/04/10/lcut-0-3-0-release/</link>
		<comments>https://tonybai.com/2012/04/10/lcut-0-3-0-release/#comments</comments>
		<pubDate>Tue, 10 Apr 2012 13:15:27 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[autoconf]]></category>
		<category><![CDATA[automake]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cmockery]]></category>
		<category><![CDATA[Configure]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[LCUT]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Unittest]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=853</guid>
		<description><![CDATA[lcut单元测试框架在我的项目中应用已经有一段时间了，项目组的同事对lcut的使用也是越来越熟悉，这不今天一位同事还提出了一个新需求，需求大致是这样的。 &#160; 在实际项目中，经常遇到这类情况： &#160; int bar(&#8230;) { &#160; &#160; int ret; &#160; &#160; &#160; ret = foo(&#8230;); &#160; &#160; /* assert ret */ &#160; &#160; &#8230; &#160; &#160; &#160; ret = foo(&#8230;); &#160; &#160; /* assert ret */ &#160; &#160; &#8230; &#160; &#160; &#160; ret = foo(&#8230;); &#160; &#160; /* assert ret */ &#160; &#160; [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://code.google.com/p/lcut">lcut</a>单元测试框架在我的项目中应用已经有一段时间了，项目组的同事对<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/">lcut</a>的使用也是越来越熟悉，这不今天一位同事还提出了一个新需求，需求大致是这样的。</p>
<div>&nbsp;</div>
<div>在实际项目中，经常遇到这类情况：</div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">int bar(&#8230;) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; int ret;</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; ret = foo(&#8230;);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; /* assert ret */</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; ret = foo(&#8230;);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; /* assert ret */</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; ret = foo(&#8230;);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; /* assert ret */</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div>上述代码中被测函数接口bar的实现中多次调用了某函数foo。这样当我们用<a href="http://tonybai.com/2010/10/29/lcut-add-mock-support/">mock</a>方式测试bar这个函数时，可能需要多次重复设置foo的返回值以及输出函数的值，就像这样：</div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">void tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_RETV_RETURN(foo, 0);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_RETV_RETURN(foo, 0);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_RETV_RETURN(foo, 0);</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_ARG_RETURN(foo, 1);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_ARG_RETURN(foo, 1);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_ARG_RETURN(foo, 1);</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_INT_EQUAL(tc, 0, bar(&#8230;));</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &#8230;</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div>我的同事希望lcut能提供一个接口：支持一次调用，设置多次mock obj的返回值或输出参数，使用这样的接口后，上述代码就可以简化为：</div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">void tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_RETV_RETURN_COUNT(foo, 0, 3);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_ARG_RETURN_COUNT(foo, 1, 3);</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_INT_EQUAL(tc, 0, bar(&#8230;));</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div>这个需求提的非常好，看起来更像是一种语法糖(<a href="http://en.wikipedia.org/wiki/Syntactic_sugar">syntactic sugar</a>)，用于简化代码编写。于是乎下午我就为lcut增加了两个有用的宏：LCUT_RETV_RETURN_COUNT和LCUT_ARG_RETURN_COUNT。</div>
<div>&nbsp;</div>
<div>正如上面所说，这两个宏可在一次调用中多次设置某个mock obj的返回值和输出参数值，两个宏的原型如下：</div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">#define LCUT_RETV_RETURN_COUNT(fcname, value, count) do { \</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &nbsp; &nbsp; lcut_mock_obj_return(#fcname, (void*)value, __FUNCTION__, __LINE__, __FILE__, MOCK_RETV, count); \</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; } while(0);</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">#define LCUT_ARG_RETURN_COUNT(fcname, value, count) do { \</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; &nbsp; &nbsp; lcut_mock_obj_return(#fcname, (void*)value, __FUNCTION__, __LINE__, __FILE__, MOCK_ARG, count); \</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; } while(0);</span></div>
<div>&nbsp;</div>
<div>只是比之前提供的LCUT_RETV_RETURN和LCUT_ARG_RETURN多了一个宏参数count。count用于指出对mocked obj进行多少次返回值或输出参数的设置。</div>
<div>&nbsp;</div>
<div>另外当count传入-1时，其语义为无论mocked object被使用多少次，其返回值或输出参数值都是一样的，即使用LCUT_RETV_RETURN_COUNT或LCUT_ARG_RETURN_COUNT时设置的那个值，直到下一次调用这两个宏进行重新设置时，值才会发生变化。例如上面的例子我们也可以改写为：</div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">void tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_RETV_RETURN_COUNT(foo, 0, -1);</span></div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_ARG_RETURN_COUNT(foo, 1, -1);</span></div>
<div>&nbsp;</div>
<div><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; LCUT_INT_EQUAL(tc, 0, bar(&#8230;));</span></div>
<div><span style="font-family:courier new,courier,monospace;">}</span></div>
<div>&nbsp;</div>
<div>这样无论后续再调用多少次bar函数，foo的返回值总是0，输出参数也总是1。</div>
<div>&nbsp;</div>
<div>增加了这两个宏后，lcut的版本号也小升了一位，最新版本是<a href="http://lcut.googlecode.com/files/lcut-0.3.0-rc1.tar.gz">lcut-0.3.0-rc1</a>，其中还增加了一个针对lcut mock功能的example &#8211; mock_test.c。同时Google Code上的<a href="http://code.google.com/p/lcut/wiki/lcut_user_guide_cn">lcut guide</a>也做了更新，对新增的宏的用法进行了简要说明。</div>
<div>&nbsp;</div>
<div>就这样，lcut 0.3.0版本算是发布了，后续还会经过内部的细致测试，如果没有什么问题，就会去掉rc。</div>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/04/10/lcut-0-3-0-release/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>lcut增加对mock的支持</title>
		<link>https://tonybai.com/2010/10/29/lcut-add-mock-support/</link>
		<comments>https://tonybai.com/2010/10/29/lcut-add-mock-support/#comments</comments>
		<pubDate>Fri, 29 Oct 2010 14:47:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[autoconf]]></category>
		<category><![CDATA[automake]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cmockery]]></category>
		<category><![CDATA[Configure]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[LCUT]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Unittest]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2010/10/29/lcut%e5%a2%9e%e5%8a%a0%e5%af%b9mock%e7%9a%84%e6%94%af%e6%8c%81/</guid>
		<description><![CDATA[<p>记得恰好是在一个月前的今天，我发布了lcut<br />
(轻量级C语言单元测试框架)0.1.0版本<br />
。由于发布仓促，文档没能及时跟上。在stackoverflow<br />
的一个关于单元测试的帖子<br />
上，一位叫Craig McQueen<br />
的朋友也给出了建议：&#34;Some documentation would be helpful. Project b...</p>]]></description>
			<content:encoded><![CDATA[<p>记得恰好是在一个月前的今天，我<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/" target="_blank">发布了lcut</a>(轻量级<a href="http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/" target="_blank">C语言单元测试框架</a>)<a href="http://code.google.com/p/lcut/" target="_blank">0.1.0版本</a><br />
	。由于发布仓促，文档没能及时跟上。在<a href="http://stackoverflow.com" target="_blank">stackoverflow</a>的一个<a href="http://stackoverflow.com/questions/65820/unit-testing-c-code" target="_blank">关于单元测试的帖子</a><br />
	上，一位叫<a href="http://craig.mcqueen.id.au/" target="_blank">Craig McQueen</a>的朋友也给出了建议：&quot;Some documentation would be helpful. Project background and goals, a features list, advantages over existing alternatives, etc would be helpful for people who are checking it out for the first time.&quot; 看完这个建议后心里那个汗啊！不过一想到用E文编写文档心里就有些打怵。就这样在这一个月里文档依旧没有改观:(。不过，lcut本身还是有一些进步的，这两天一直规划着为lcut增加<a href="http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/" target="_blank">mock</a>的支持，今天终于将这个功能加进了lcut，并发布了<a href="http://code.google.com/p/lcut/" target="_blank">lcut-0.2.0-beta</a>版，欢迎大家试用，并提出意见和建议。</p>
<p>之前在单元测试过程中使用<a href="http://tonybai.com/2009/08/22/introduce-cmockery-for-c-unit-test/" target="_blank">cmockery</a>中提供的mock功能，cmockery也是lcut的mock功能的直接灵感来源。与cmockery不同的是lcut将对输出参数的mock和对函数返回值的mock区分开来，这样用起来更加直观。</p>
<p>这里用一个简单的例子(完整代码在lcut包product_database_test.c文件中)来说明一下lcut的mock功能如何使用：</p>
<p>/* product_database.c */<br />
	int get_total_count_of_employee() {<br />
	&nbsp;&nbsp;&nbsp; database_conn *conn = NULL;<br />
	&nbsp;&nbsp;&nbsp; int retv = -1;<br />
	&nbsp;&nbsp;&nbsp; int total_count = -1;</p>
<p>&nbsp;&nbsp;&nbsp; conn = connect_to_database(&quot;tonybai&quot;, &quot;tonybai&quot;, &quot;mysql&quot;);<br />
	&nbsp;&nbsp;&nbsp; if (!conn)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;</p>
<p>&nbsp;&nbsp;&nbsp; retv = table_row_count(conn, &quot;EMPLOYEE_TABLE&quot;, &#038;total_count);<br />
	&nbsp;&nbsp;&nbsp; if (retv &lt; 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; return total_count;<br />
	}</p>
<p>/* product_database_test.c */<br />
	database_conn *connect_to_database(const char *user,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const char *passwd,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const char *serviceid) {<br />
	&nbsp;&nbsp;&nbsp; return (database_conn*)LCUT_MOCK_RETV();<br />
	}</p>
<p>int table_row_count(const database_conn *conn,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const char *table_name,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int *total_count) {<br />
	&nbsp;&nbsp;&nbsp; (*total_count) = (int)LCUT_MOCK_ARG();<br />
	&nbsp;&nbsp;&nbsp; return (int)LCUT_MOCK_RETV();<br />
	}</p>
<p>void tc_get_total_count_of_employee_ok(lcut_tc_t *tc, void *data) {<br />
	&nbsp;&nbsp;&nbsp; LCUT_RETV_RETURN(connect_to_database, 0&#215;1234);<br />
	&nbsp;&nbsp;&nbsp; LCUT_ARG_RETURN(table_row_count, 5);<br />
	&nbsp;&nbsp;&nbsp; LCUT_RETV_RETURN(table_row_count, 0);</p>
<p>&nbsp;&nbsp;&nbsp; LCUT_INT_EQUAL(tc, 5, get_total_count_of_employee());<br />
	}</p>
<p>被mock的函数多为系统API或执行代价较高的第三方库函数，我们在业务代码更关心的是这些函数的接口行为，而C语言中函数的接口行为表现为：返回值和输出参数。我们需要通过控制被mock函数的接口行为来达到测试我们业务代码的目的，所以我们需要mock这些函数的返回值和输出参数。上面例子中connect_to_database和table_row_count就是两个被mock了的库函数。我们通过LCUT_MOCK_RETV来mock函数的返回值，通过LCUT_MOCK_ARG来mock函数的输出参数。在测试代码tc_get_total_count_of_employee_ok中，我们分别通过LCUT_RETV_RETURN和LCUT_ARG_RETURN来控制前面两个被mock的函数中mock obj的返回值和输出参数: LCUT_RETV_RETURN(connect_to_database, 0&#215;1234)告诉connect_to_database返回(int)0&#215;1234，相应的，LCUT_ARG_RETURN(table_row_count, 5)则告诉table_row_count执行后其输出参数*total_count的值为5。有了这些设定的mock obj我们就可以专注于我们业务层代码的白盒逻辑单元测试了，一旦connect_to_database和table_row_count的外部行为被控制后，业务层的代码get_total_count_of_employee的行为也就是可预期的了，我们用断言测试即可。</p>
<p>由于实现原理限制，如果你的函数输出参数类型或返回值类型为double*/float*，那么这个函数还不能使用lcut的mock功能，否则会编译出错。但绝大多数软件开发领域都很少使用浮点计算，所以lcut的mock还是可以满足大多数需要的。</p>
<p>题外话：<br />
	在公司使用代理上网，svn无法直接访问google code，这个问题一直困扰着我，直到今天才知道可以为svn客户端设置代理，设置步骤如下：<br />
	-&gt; vi ~/.subversion/servers<br />
	-&gt; 增加如下设置：<br />
	&nbsp;&nbsp; [global]<br />
	&nbsp;&nbsp; http-proxy-host = 你的代理主机域名或ip<br />
	&nbsp;&nbsp; http-proxy-port = 端口<br />
	&nbsp;&nbsp; http-proxy-username = 你的用户名<br />
	&nbsp;&nbsp; http-proxy-password = 你的密码<br />
	设置后，svn立马就可以连上google code的svn server了。</p>
<p style='text-align:left'>&copy; 2010, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2010/10/29/lcut-add-mock-support/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
