<?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; DSL</title>
	<atom:link href="http://tonybai.com/tag/dsl/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sat, 04 Apr 2026 00:51:31 +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>Martin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！</title>
		<link>https://tonybai.com/2025/06/26/non-deterministic-abstraction/</link>
		<comments>https://tonybai.com/2025/06/26/non-deterministic-abstraction/#comments</comments>
		<pubDate>Wed, 25 Jun 2025 23:02:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[fortran]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HLL]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[MartinFowler]]></category>
		<category><![CDATA[Prompt]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Ruby]]></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=4850</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/26/non-deterministic-abstraction 大家好，我是Tony Bai。 在软件开发领域，Martin Fowler 的名字几乎等同于思想的灯塔。他的每一篇文章、每一次演讲，都能为我们揭示行业发展的深层脉络。最近，Fowler 大师又发布了一篇简短但引人深思的博文——《LLMs bring new nature of abstraction》，再次精准地捕捉到了一个正在发生的、可能颠覆我们认知和工作方式的巨大变革。 Fowler 认为，大型语言模型（LLM）的出现，对软件开发的影响，堪比从汇编语言到首批高级编程语言（HLLs）的飞跃。但关键在于，LLM 带来的不仅仅是又一个“更高层次”的抽象，它正在从根本上改变编程的“本质”——迫使我们思考，用“非确定性工具”进行编程究竟意味着什么。 在这篇文章中，我们就来简单解读一下。 从“确定性”的阶梯到“非确定性”的岔路 回顾编程语言的发展史，我们一直在追求更高层次的抽象，以提升生产力、降低复杂度： 汇编语言 vs. 机器指令： 汇编让我们用助记符替代了 0 和 1，但仍需关注特定机器的寄存器和指令集。 高级语言 (HLLs) vs. 汇编： Fortran、COBOL 等早期 HLLs 让我们能用语句、条件、循环来思考，而不用关心数据如何在寄存器间移动。Fowler 回忆道，他用 Fortran IV 编程时，虽然有诸多限制（如 IF 没有 ELSE，整数变量名必须以 I-N 开头），但这已经是巨大的进步。 现代语言、框架、DSL vs. 早期 HLLs： Ruby、Go、Python 等现代语言，以及各种框架和领域特定语言（DSL），进一步提升了抽象层次。我们现在可以本能地将函数作为数据传递，使用丰富的库和模式，而不用从头编写大量底层代码。 Fowler 指出，尽管这些发展极大地提升了抽象层次和生产力，但它们并没有从根本上改变“编程的性质”。我们仍然是在与机器进行一种“确定性”的对话：给定相同的输入和代码，我们期望得到相同的输出。错误（Bug）也是可复现的。 然而，LLM 的介入，打破了这一基本假设。 Fowler [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/non-deterministic-abstraction-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/26/non-deterministic-abstraction">本文永久链接</a> &#8211; https://tonybai.com/2025/06/26/non-deterministic-abstraction</p>
<p>大家好，我是Tony Bai。</p>
<p>在软件开发领域，Martin Fowler 的名字几乎等同于思想的灯塔。他的每一篇文章、每一次演讲，都能为我们揭示行业发展的深层脉络。最近，Fowler 大师又发布了一篇简短但引人深思的博文——《<a href="https://martinfowler.com/articles/2025-nature-abstraction.html">LLMs bring new nature of abstraction</a>》，再次精准地捕捉到了一个正在发生的、可能颠覆我们认知和工作方式的巨大变革。</p>
<p>Fowler 认为，大型语言模型（LLM）的出现，对软件开发的影响，堪比从汇编语言到首批高级编程语言（HLLs）的飞跃。但关键在于，LLM 带来的不仅仅是又一个“更高层次”的抽象，它正在从根本上改变编程的“本质”——<strong>迫使我们思考，用“非确定性工具”进行编程究竟意味着什么。</strong></p>
<p>在这篇文章中，我们就来简单解读一下。</p>
<h2>从“确定性”的阶梯到“非确定性”的岔路</h2>
<p>回顾编程语言的发展史，我们一直在追求更高层次的抽象，以提升生产力、降低复杂度：</p>
<ul>
<li><strong>汇编语言 vs. 机器指令：</strong> 汇编让我们用助记符替代了 0 和 1，但仍需关注特定机器的寄存器和指令集。</li>
<li><strong>高级语言 (HLLs) vs. 汇编：</strong> Fortran、COBOL 等早期 HLLs 让我们能用语句、条件、循环来思考，而不用关心数据如何在寄存器间移动。Fowler 回忆道，他用 Fortran IV 编程时，虽然有诸多限制（如 IF 没有 ELSE，整数变量名必须以 I-N 开头），但这已经是巨大的进步。</li>
<li><strong>现代语言、框架、DSL vs. 早期 HLLs：</strong> Ruby、Go、Python 等现代语言，以及各种框架和领域特定语言（DSL），进一步提升了抽象层次。我们现在可以本能地将函数作为数据传递，使用丰富的库和模式，而不用从头编写大量底层代码。</li>
</ul>
<p>Fowler 指出，尽管这些发展极大地提升了抽象层次和生产力，但它们并没有从根本上改变“编程的性质”。我们仍然是在与机器进行一种“确定性”的对话：<strong>给定相同的输入和代码，我们期望得到相同的输出。错误（Bug）也是可复现的。</strong></p>
<p>然而，LLM 的介入，打破了这一基本假设。</p>
<p>Fowler 写道：“用提示词与机器对话，其差异之大，犹如 Ruby 之于 Fortran，Fortran 之于汇编”。</p>
<p>更重要的是，这不仅仅是抽象层次的巨大飞跃。当 Fowler 用 Fortran 写一个函数，他可以编译一百次，结果中的 Bug 依然是那个 Bug。但 <strong>LLM 引入的是一种“非确定性”的抽象 (non-deterministic abstraction)</strong>。</p>
<p>这意味着，即使我们把精心设计的 Prompt 存储在 Git 中，也不能保证每次运行都会得到完全相同的行为。正如他的同事 Birgitta Böckeler 精辟总结的那样：</p>
<blockquote>
<p>我们并非仅仅在抽象层级上“向上”移动，我们同时也在“横向”移入非确定性的领域。</p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/2025/non-deterministic-abstraction-2.png" alt="" /></p>
<p>Fowler 文章中的配图非常形象地展示了这一点：传统的编程语言、编译器、字节码是一条清晰的、自上而下的抽象路径；而模型/DSL、代码生成器、低代码、框架是其上的不同抽象层次。<strong>自然语言（通过 LLM）则像一条从旁边切入的、直接通往“半结构化/接近人类思维”的道路，这条路本身就带有模糊和不确定性。</strong></p>
<h2>“非确定性”编程时代的挑战与启示</h2>
<p>这种“非确定性”的本质，对我们 Gopher，乃至所有软件开发者，都带来了前所未有的挑战和需要重新思考的问题：</p>
<ol>
<li><strong>版本控制与可复现性：</strong> 当 Prompt 不能保证结果一致时，我们如何管理和版本化我们的“AI辅助代码”？如何确保开发、测试、生产环境的一致性，或者至少是可接受的差异性？仅仅版本化 Prompt 可能不够，我们还需要版本化模型、参数（如 temperature）甚至是一些关键的种子（seed）吗？</li>
<li><strong>测试与调试：</strong> 如何测试一个输出不完全固定的“组件”？传统的单元测试、集成测试方法是否依然有效？我们可能需要引入新的测试策略，例如基于属性的测试、对输出结果的统计验证、或者更侧重于行为和意图的验证。当 LLM 生成的代码出现问题，调试的难度是否会指数级增加？</li>
<li><strong>可靠性与契约：</strong> 在一个包含非确定性AI组件的系统中，如何定义和保证整体的可靠性？服务间的“契约”又该如何描述和强制执行？</li>
<li><strong>思维模式的转变：</strong> 我们习惯了对代码的精确控制，追求逻辑的严密和行为的可预测。现在，我们可能需要学会与“模糊”和“概率”共存，从“指令下达者”转变为“意图沟通者”和“结果筛选者”。</li>
</ol>
<h2>这对我们 Gopher 意味着什么？</h2>
<p>Go 语言以其明确性、强类型、简洁的并发模型以及相对可预测的行为，深受开发者喜爱。当我们尝试将 LLM 融入 Go 的生态和开发流程时，这些“非确定性”的特性会带来新的思考：</p>
<ul>
<li><strong>AI 生成 Go 代码：</strong> 当我们使用 LLM 生成 Go 代码片段、单元测试，甚至整个模块时，如何确保生成的代码符合 Go 的最佳实践、是高效且安全的？如何对生成的代码进行有效的审查和集成？</li>
<li><strong>用 Go 构建与 LLM 交互的工具/Agent：</strong> 如果我们用 Go 开发与 LLM 交互的后端服务或智能体（Agent），我们需要在架构设计上充分考虑 LLM 的非确定性，设计更鲁棒的错误处理、重试机制，以及对 LLM 输出结果的验证和筛选逻辑。</li>
<li><strong>利用 LLM 理解复杂 Go 系统：</strong> LLM 或许能帮助我们理解遗留的复杂 Go 代码库，但其解释的准确性和一致性也需要我们审慎评估。</li>
</ul>
<p>Fowler 在文末表达了他对这一变革的兴奋之情：“这种改变是戏剧性的，也让我颇为兴奋。我相信我会为一些失去的东西感到悲伤，但我们也将获得一些我们中很少有人能理解的东西。”</p>
<h2>小结：拥抱不确定，探索新大陆</h2>
<p>Martin Fowler 的这篇文章，为我们揭示了 LLM 时代编程范式可能发生的深刻转变。它不再仅仅是工具的进化，更是与机器协作方式的本质性变革。</p>
<p>作为 Gopher，作为软件工程师，我们需要开始认真思考这种“非确定性”带来的影响，积极探索与之共存、甚至利用其特性创造价值的新方法。这无疑是一个充满挑战但也充满机遇的新大陆。</p>
<p>你如何看待 Fowler 的这个观点？你认为 LLM 带来的“非确定性”会对你的日常开发工作产生哪些具体影响？欢迎在评论区分享你的看法！</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/06/26/non-deterministic-abstraction/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>当一切皆可用Python：Go这样的通用语言与DSL的未来价值何在？</title>
		<link>https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm/</link>
		<comments>https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm/#comments</comments>
		<pubDate>Thu, 19 Jun 2025 00:16:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[大模型]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[胶水代码]]></category>
		<category><![CDATA[领域特定语言]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4833</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm 大家好，我是Tony Bai。 大型语言模型 (LLM) 的浪潮正以前所未有的速度和深度席卷软件开发领域。从代码生成、Bug 修复到文档撰写，AI 似乎正成为每一位开发者身边无所不能的“副驾驶”。在这股浪潮中，一个略显“刺耳”但又无法回避的论调开始浮现，正如一篇引人深思的博文《Programming Language Design in the Era of LLMs: A Return to Mediocrity?》中所指出的那样：“一切都更容易用 Python 实现 (Everything is Easier in Python)”——当然，这里指的是在 LLM 的强力辅助下。 这并非危言耸听。文章中展示的图表（来源于论文 “Knowledge Transfer from High-Resource to Low-Resource Programming Languages for Code LLMs“）清晰地揭示了一个趋势：LLM 在那些训练数据量巨大的“高资源”语言（如 Python, JavaScript, Java, C# 等）上，代码生成和任务解决的效能显著高于像 Go、Rust 这样的“低资源”语言： 如果 LLM 能够如此轻松地用 Python（或其他高资源语言）根据自然语言需求生成大部分“胶水代码”甚至核心逻辑，那么我们不禁要问： 精心设计和构建领域特定语言 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/language-design-in-the-era-of-llm-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm">本文永久链接</a> &#8211; https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm</p>
<p>大家好，我是Tony Bai。</p>
<p>大型语言模型 (LLM) 的浪潮正以前所未有的速度和深度席卷软件开发领域。从代码生成、Bug 修复到文档撰写，AI 似乎正成为每一位开发者身边无所不能的“副驾驶”。在这股浪潮中，一个略显“刺耳”但又无法回避的论调开始浮现，正如一篇引人深思的博文《<a href="https://kirancodes.me/posts/log-lang-design-llms.html">Programming Language Design in the Era of LLMs: A Return to Mediocrity?</a>》中所指出的那样：<strong>“一切都更容易用 Python 实现 (Everything is Easier in Python)”</strong>——当然，这里指的是在 LLM 的强力辅助下。</p>
<p>这并非危言耸听。文章中展示的图表（来源于论文 “<a href="https://dl.acm.org/doi/abs/10.1145/3689735">Knowledge Transfer from High-Resource to Low-Resource Programming Languages for Code LLMs</a>“）清晰地揭示了一个趋势：LLM 在那些训练数据量巨大的“高资源”语言（如 Python, JavaScript, Java, C# 等）上，代码生成和任务解决的效能显著高于像 Go、Rust 这样的“低资源”语言：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/language-design-in-the-era-of-llm-2.png" alt="" /></p>
<p>如果 LLM 能够如此轻松地用 Python（或其他高资源语言）根据自然语言需求生成大部分“胶水代码”甚至核心逻辑，那么我们不禁要问：</p>
<ul>
<li>精心设计和构建<strong>领域特定语言 (DSL)</strong> 的价值还剩下多少？当消除冗余、封装领域知识这些 DSL 的核心优势，似乎可以被 LLM+通用语言轻易取代时，DSL 的未来是否会因此停滞？</li>
<li>对于像 <strong>Go 这样以简洁、高效、工程化著称的通用语言</strong>，当其在 LLM 训练数据中的“声量”不及 Python 时，它的核心竞争力又将面临怎样的挑战与机遇？</li>
</ul>
<p>今天，我们就来聊聊在 LLM 时代，DSL 和像 Go 这样的通用语言，其未来的价值究竟何在。</p>
<h2>DSL 的黄昏？当 LLM 成为“万能代码生成器”</h2>
<p>领域特定语言 (DSL) 的核心价值在于<strong>“专为特定领域而生”</strong>。通过精心设计的语法和语义，DSL 能够：</p>
<ul>
<li><strong>提升表达力：</strong> 让领域专家或开发者能用更接近自然语言或领域术语的方式描述问题。</li>
<li><strong>消除样板代码：</strong> 将领域内的通用模式和“常识性规则”编码到语言自身。</li>
<li><strong>降低认知负荷：</strong> 开发者可以更专注于问题的“有趣”部分，而非底层实现细节。</li>
<li><strong>减少错误面：</strong> 通过语言层面的约束，使得编写出不正确的程序变得更加困难。</li>
</ul>
<p>文章中那个视频游戏对话的例子就非常典型：从繁琐的 API 调用序列</p>
<pre><code># example code for a VN
character.draw("alice", character.LEFT, 0.1)
character.draw("bob", character.RIGHT, 0.1)
character.say("alice", "hello there!")
character.say("bob", "hi!")
character.state("alice", "sad")
character.say("alice", "did you hear the news?")
</code></pre>
<p>到简洁的 DSL 描述</p>
<pre><code># example DSL for dialog
[ alice @ left in 0.1, bob @right in 0.1  ]
alice: hello there!
bob: hi!
alice[sad]: did you hear the news?...
</code></pre>
<p>DSL 的优势一目了然。</p>
<p>然而，LLM 的出现，似乎正在侵蚀 DSL 的这些传统护城河。当开发者可以用自然语言向 Copilot 或 ChatGPT 描述“我想要一个能让 Alice 和 Bob 在屏幕两侧对话的场景”，并且 LLM 能够直接生成 Python 或 JavaScript 代码来实现这个功能时，我们不禁要问：<strong>为什么还要费心去学习、设计、构建和推广一个全新的 DSL 呢？</strong></p>
<p>这里隐含的“机会成本”的问题非常现实：</p>
<ul>
<li>DSL 的学习与生态位：使用一个“小众”的 DSL，意味着开发者可能要放弃使用 LLM 在主流语言上生成代码的巨大便利。LLM 在小众 DSL 上的表现（如果未经专门微调）几乎可以预见会非常糟糕。</li>
<li>DSL 的构建成本：设计和实现一个高质量的 DSL 本身就需要巨大的投入。在 LLM 时代，这个投入的“性价比”似乎正在下降。</li>
</ul>
<p>这引发了一个令人担忧的趋势：<strong>DSL 的发展是否会因此停滞不前？语言设计的多样性是否会因此受到冲击，最终导致“人人皆写 Python (在 LLM 辅助下)”的局面？</strong></p>
<h2>Go 语言：在 LLM 时代的“低资源”挑战与独特优势</h2>
<p><a href="https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis">Go语言虽然在全球拥有数百万开发者</a>，并且在云原生、后端开发等领域占据主导地位，但在 LLM 的训练数据占比上，相较于 Python、JavaScript 等拥有更长历史和更广泛应用场景（尤其是 Web 前端、数据科学等产生大量开源代码的领域）的语言，仍然处于“低资源”状态。</p>
<p>这意味着，<strong>LLM 在直接生成高质量、复杂 Go 代码方面的能力，目前可能还无法与它在 Python 等语言上的表现相媲美。</strong> 这对 Go 社区和开发者来说，既是挑战，也是反思和寻求新机遇的契机。</p>
<p><strong>挑战：</strong></p>
<ul>
<li>如果 LLM 生成 Go 代码的效率和质量暂时落后，可能会降低新手或寻求快速原型验证的开发者选择 Go 的意愿。</li>
<li>Go 社区可能需要投入更多精力来构建 LLM 友好的工具、库和高质量的训练数据。</li>
</ul>
<p>然而，Go 语言的独特优势在 LLM 时代或许会更加凸显：</p>
<ul>
<li>简洁性与明确性对 LLM 的“友好”：
<ul>
<li>Go 语言语法精炼，关键字少，没有复杂的继承和隐式转换。这种“所见即所得”的特性，可能使得 LLM 更容易理解 Go 代码的结构和语义。</li>
<li>Go 的强类型系统和明确的错误处理机制 (if err != nil)，虽然在手动编码时有时显得冗余，但在 LLM 生成或分析代码时，这些明确的信号可能有助于 LLM 生成更健壮、更易于验证的代码。</li>
</ul>
</li>
<li>强大的标准库与工程化特性：
<ul>
<li>Go 丰富的标准库覆盖了网络、并发、编解码等常见场景。LLM 在生成 Go 代码时，可以更多地依赖这些经过充分测试和优化的标准组件，减少对第三方库的复杂依赖。</li>
<li>Go 内置的测试、性能分析、代码格式化等工具，以及其对模块化的良好支持，有助于对 LLM 生成的代码进行有效的质量控制和集成。</li>
</ul>
</li>
<li>并发模型与性能优势的不可替代性：
<ul>
<li>Go 的 Goroutine 和 Channel 提供的轻量级并发模型，在构建高并发网络服务和分布式系统方面具有独特优势。这部分逻辑的复杂性和对性能的极致要求，可能难以完全由 LLM 在 Python 等语言中通过简单生成来完美复制。</li>
<li>Go 编译后的静态二进制文件和高效的执行性能，在许多后端和基础设施场景中依然是硬核需求。</li>
</ul>
</li>
<li>Go 作为“基础设施”语言的潜力：
<ul>
<li>LLM 本身就需要强大的基础设施来训练和运行。Go 在构建这些大规模、高并发的 AI 基础设施方面，已经扮演了重要角色（如 Ollama 等项目）。</li>
<li>Go 的简洁性和安全性，也使其成为定义和执行 AI Agent 行为、编排复杂 AI 工作流的理想语言。</li>
</ul>
</li>
</ul>
<h2>LLM 时代，语言设计（DSL 与通用语言）的破局之路</h2>
<p>面对大型语言模型（LLM）带来的挑战，编程语言的设计（无论是领域特定语言（DSL）还是通用语言如 Go）并非只能被动应对。学术界正在探索一些富有前景的新方向，旨在实现语言设计与 LLM 的协同进化，而非零和博弈。</p>
<p>首先，有研究提出教会 LLM 理解 DSL 的方法，核心思路是利用 LLM 擅长的语言（如 Python 的受限子集）来表达核心逻辑。由于 LLM 对特定 DSL 的理解和生成能力有限，开发者可以设计工具或方法，将这些 Python 表达式“提升”或自动翻译到目标 DSL 中。这一思路启示未来的 DSL 设计者应考虑为其语言提供一个 LLM 友好的“语义映射层”，例如用 Python 或其他高资源语言来描述其核心概念和操作。</p>
<p>其次，在 DSL 中弥合“形式化”与“非形式化”的鸿沟也是一个重要方向。开发者在编写复杂系统内核时，往往需要精确控制每一行代码，此时 LLM 的帮助有限。然而，在编写不常用的“一次性”脚本时，LLM 能够根据自然语言描述生成“胶水代码”，使得开发者只需关注核心的“有趣”部分。因此，未来的 DSL 设计可以探索如何无缝集成“非形式化”自然语言描述，作为规范、注释，甚至直接融入代码中。与此同时，是否可以从 DSL 的类型系统或静态分析结果中，自动生成高质量的自然语言规范，反过来帮助 LLM 更好地理解和生成 DSL 代码，值得深入研究。</p>
<p>最后，面向 LLM 辅助验证的语言设计也成为一种趋势。研究者们不再满足于 LLM 生成“能运行”的代码，而是期望 LLM 能生成带有形式化规约（specifications）的代码，并利用验证语言（如 Dafny、Boogie）来证明这些代码的正确性。这一趋势对 DSL 和通用语言（如 Go）的设计提出了新要求，开发者需要考虑如何更好地支持“规约即代码”和“验证即开发”的模式。例如，Go 语言的强类型和接口设计，为形式化验证提供了一定的基础，未来的改进可以在此基础上进一步发展。</p>
<p>通过以上几个方向的探索，编程语言设计有望与 LLM 实现更为紧密的协同进化，推动软件开发的进步和创新。</p>
<h2>小结：挑战之下，价值重塑</h2>
<p>LLM 的崛起，无疑对整个编程语言生态带来了深刻的冲击和前所未有的挑战。那种“学会一门语言，用好一个框架，就能高枕无忧”的时代可能正在远去。</p>
<p>“一切皆可用 Python (在 LLM 辅助下)”的论调，虽然略显夸张，但也点出了一个事实：<strong>对于那些仅仅是为了减少样板代码、提供简单抽象的 DSL，或者在表达力和生态丰富度上不及 Python 的通用语言，其生存空间确实受到了挤压。</strong></p>
<p>然而，这并不意味着语言设计本身会走向“平庸化”或消亡。相反，LLM 可能会迫使我们重新思考编程语言的核心价值：</p>
<ul>
<li>对于 <strong>DSL</strong>，未来可能需要更高的“门槛”——它们必须提供真正深刻的领域洞察和远超通用语言的表达效率与安全性，才能证明其存在的必要性。同时，与 LLM 的协同将是关键。</li>
<li>对于像 <strong>Go 这样的通用语言</strong>，其价值将更多地体现在那些难以被 LLM 轻易复制的领域：极致的工程效率、经过实战检验的并发模型、强大的底层控制能力、以及构建大规模、高可靠系统的综合实力。Go 需要继续打磨其核心优势，并积极拥抱 AI，成为 AI 时代不可或缺的基石。</li>
</ul>
<p>最终，技术的浪潮会淘汰掉不适应变化的，也会催生出新的、更强大的生命体。对于我们开发者而言，保持学习的热情，理解不同工具的本质和边界，拥抱变化，或许才是应对这个“AI 定义一切”时代的不二法门。</p>
<p>你认为 LLM 会如何改变你使用的编程语言？Go 和 DSL 的未来将走向何方？欢迎在评论区留下你的真知灼见！</p>
<hr />
<p><strong>精进有道，更上层楼</strong></p>
<p><a href="https://mp.weixin.qq.com/s/GWGWTfCRCsOJ_4Pk-pxpHA">极客时间《Go语言进阶课》上架刚好一个月</a>，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。</p>
<p>现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。</p>
<p>这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、<br />
更高效、更可靠的生产级代码！</p>
<p>扫描下方二维码，立即开启你的 Go 语言进阶之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<p><strong>感谢阅读！</strong></p>
<p>如果这篇文章让你对AI时代的DSL和通用语言设计和未来有了新的认识，请帮忙<strong>转发</strong>，让更多朋友一起学习和进步！</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/06/19/language-design-in-the-era-of-llm/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go x/exp/xiter提案搁浅背后：社区的选择与深度思考</title>
		<link>https://tonybai.com/2025/05/29/xiter-declined/</link>
		<comments>https://tonybai.com/2025/05/29/xiter-declined/#comments</comments>
		<pubDate>Thu, 29 May 2025 02:58:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Adapter]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[AustinClements]]></category>
		<category><![CDATA[Concat]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[equal]]></category>
		<category><![CDATA[filter]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.22]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[iter]]></category>
		<category><![CDATA[Iterator]]></category>
		<category><![CDATA[LINQ]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[merge]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[reduce]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[Seq]]></category>
		<category><![CDATA[Seq2]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sort]]></category>
		<category><![CDATA[xiter]]></category>
		<category><![CDATA[zip]]></category>
		<category><![CDATA[迭代器]]></category>
		<category><![CDATA[链式调用]]></category>
		<category><![CDATA[领域特定语言]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4761</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/29/xiter-declined 大家好，我是Tony Bai。 随着 Go 1.22 中 range over func 实验性特性的引入，以及在 Go 1.23 中该特性的最终落地（#61405），Go 社区对迭代器（Iterators）的讨论达到了新的高度。在这一背景下，一项旨在提供标准迭代器适配器（Adapters）的提案 x/exp/xiter (Issue #61898) 应运而生，曾被寄予厚望，期望能为 Go 开发者带来一套便捷、统一的迭代器操作工具集。然而，经过社区的广泛讨论和官方团队的审慎评估，该提案最终被标记为“婉拒并撤回 (declined as retracted)”。本文将对 x/exp/xiter 提案的核心内容做个简单解读，说说社区围绕它的主要争论点，以及最终导致其搁浅的关键因素，并简单谈谈这一决策对 Go 语言生态的潜在影响与启示。 x/exp/xiter：构想与核心功能 x/exp/xiter 提案由 Russ Cox (rsc) 发起，旨在 golang.org/x/exp/xiter 包中定义一系列迭代器适配器。这些适配器主要服务于 Go 1.23 中引入的 range over func 特性，提供诸如数据转换 (Map)、过滤 (Filter)、聚合 (Reduce)、连接 (Concat)、并行处理 (Zip) 等常用功能。 其核心目标是： 提供标准化的迭代器操作工具： 帮助开发者以更声明式的方式处理序列数据。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/xiter-declined-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/29/xiter-declined">本文永久链接</a> &#8211; https://tonybai.com/2025/05/29/xiter-declined</p>
<p>大家好，我是Tony Bai。</p>
<p>随着 Go 1.22 中 range over func 实验性特性的引入，以及在 Go 1.23 中该特性的最终落地（#61405），Go 社区对迭代器（Iterators）的讨论达到了新的高度。在这一背景下，一项旨在提供标准迭代器适配器（Adapters）的提案 x/exp/xiter (Issue #61898) 应运而生，曾被寄予厚望，期望能为 Go 开发者带来一套便捷、统一的迭代器操作工具集。然而，经过社区的广泛讨论和官方团队的审慎评估，该提案最终被标记为“婉拒并撤回 (declined as retracted)”。本文将对 x/exp/xiter 提案的核心内容做个简单解读，说说社区围绕它的主要争论点，以及最终导致其搁浅的关键因素，并简单谈谈这一决策对 Go 语言生态的潜在影响与启示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>x/exp/xiter：构想与核心功能</h2>
<p>x/exp/xiter 提案由 Russ Cox (rsc) 发起，旨在 golang.org/x/exp/xiter 包中定义一系列迭代器适配器。这些适配器主要服务于 Go 1.23 中引入的 range over func 特性，提供诸如数据转换 (Map)、过滤 (Filter)、聚合 (Reduce)、连接 (Concat)、并行处理 (Zip) 等常用功能。</p>
<p>其核心目标是：</p>
<ul>
<li><strong>提供标准化的迭代器操作工具：</strong> 帮助开发者以更声明式的方式处理序列数据。</li>
<li><strong>探索迭代器在 Go 中的惯用法：</strong> 将其置于 x/exp 目录下，意在收集社区反馈，探讨这些适配器如何融入现有的 Go 代码风格，以及是否最终适合进入标准库 iter 包。</li>
</ul>
<p>提案中包含了一系列具体的函数定义，例如：</p>
<ul>
<li>Concat / Concat2: 连接多个序列。</li>
<li>Filter / Filter2: 根据条件过滤序列元素。</li>
<li>Map / Map2: 对序列中的每个元素应用一个函数。</li>
<li>Reduce / Reduce2: 将序列中的元素聚合成单个值。</li>
<li>Zip / Zip2: 并行迭代两个序列。</li>
<li>Limit / Limit2: 限制序列的长度。</li>
<li>Equal / Equal2 (及 EqualFunc 版本): 比较两个序列是否相等。</li>
<li>Merge / Merge2 (及 MergeFunc 版本): 合并两个有序序列。</li>
</ul>
<p>值得注意的是，许多函数都提供了针对 iter.Seq[V]（单值序列）和 iter.Seq2[K, V]（键值对序列）的两个版本，这导致了 API 数量上的成倍增加。</p>
<p>以下是一个简单的设想用法示例：</p>
<pre><code class="go">package main

import (
    "fmt"
    "iter"
    // 假设 xiter 包已存在且包含提案中的函数
    // "golang.org/x/exp/xiter"
)

// 假设的 Filter 函数
func Filter[V any](f func(V) bool, seq iter.Seq[V]) iter.Seq[V] {
    return func(yield func(V) bool) {
        for v := range seq {
            if f(v) &amp;&amp; !yield(v) {
                return
            }
        }
    }
}

// 假设的 Map 函数
func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] {
    return func(yield func(Out) bool) {
        for in := range seq {
            if !yield(f(in)) {
                return
            }
        }
    }
}

func main() {
    numbers := func(yield func(int) bool) {
        for i := 1; i &lt;= 5; i++ {
            if !yield(i) {
                return
            }
        }
    }

    // 设想：筛选偶数，然后平方
    evenSquares := Map(
        func(n int) int { return n * n },
        Filter(
            func(n int) bool { return n%2 == 0 },
            numbers,
        ),
    )

    for sq := range evenSquares {
        fmt.Println(sq) // 预期输出: 4, 16
    }
}
</code></pre>
<h2>社区热议：挑战与权衡</h2>
<p>x/exp/xiter 提案引发了社区成员的广泛讨论，焦点集中在 API 设计、易用性、与 Go 语言既有哲学的契合度等多个方面。</p>
<h3>API 设计与易用性</h3>
<ul>
<li><strong>链式调用 vs. 嵌套函数调用:</strong> 一些开发者指出，与 Java Streams 或 C# LINQ 那样的流畅链式调用（seq.Map(&#8230;).Filter(&#8230;)）相比，Go 中基于顶层函数的嵌套调用（Filter(Map(seq, &#8230;))）在可读性和编写顺序上存在不足。然而，实现链式调用需要泛型方法，而 Russ Cox指出泛型方法在 Go 中面临巨大的实现挑战（动态代码生成、性能问题、接口检查复杂性等），因此短期内不太可能实现。</li>
<li><strong>函数参数顺序:</strong> 关于 Filter, Map, Reduce 等函数中，回调函数 f 与序列 seq 的参数顺序，社区存在不同看法。
<ul>
<li>benhoyt认为回调函数应置于末尾，以符合 Go 标准库中如 sort.Slice 等多数函数的习惯，便于使用内联函数字面量。</li>
<li>aarzilli 和 Russ Cox 则倾向于将回调函数置于首位（如 Map(f, seq)），理由是这更利于函数组合时的阅读顺序（从内到外或从后往前阅读），并且与 Lisp, Python, Haskell 等语言的类似库保持一致。Russ Cox 最终在提案更新中将 Reduce 的函数参数也移至首位。</li>
</ul>
</li>
<li><strong>匿名函数冗余:</strong> DeedleFake等人指出，在没有更简洁的匿名函数语法（如 #21498 提案）的情况下，使用这些适配器时，匿名函数的类型签名显得冗余和笨拙，降低了代码的简洁性。</li>
</ul>
<h3>Seq vs. Seq2 的双重性</h3>
<p>提案中大量函数针对 iter.Seq[V] 和 iter.Seq2[K, V] 提供了两个版本（例如 Map 和 Map2），这直接导致了 API 接口数量的翻倍。虽然 Russ Cox 认为这只是“重复而非复杂性”，因为学习了 Foo 形式后，Foo2 形式只是一个简单的规则，但仍有社区成员担忧这会使包显得臃肿，影响开发者体验，并随着未来可能增加更多适配器而使问题恶化。</p>
<h3>Zip 的语义之争</h3>
<p>提案中的 Zip 函数设计为当一个序列耗尽后，仍会继续迭代另一个序列，并在 Zipped 结构体中通过 Ok1/Ok2 标志位标示元素是否存在。这与 Python 等语言中 zip 在最短序列结束时即停止的行为不同，更类似于 zip_longest。社区开发者就此展开讨论，认为应提供传统意义上的 Zip（返回 Seq2[V1, V2] 并在短序列结束时停止）和行为类似 zip_longest 的版本（如 ZipAll 或将提案中的 Zip 重命名为 ZipLongest）。</p>
<h3>标准库的边界与 Go 的哲学</h3>
<ul>
<li><strong>“Go 风格”与“过度抽象”:</strong> 一些开发者对引入这类高度函数式的适配器表示担忧，认为它们可能与 Go 语言简洁、直接、偏向过程式循环的既有风格不符，可能导致“过度抽象”。Russ Cox 也承认存在这类担忧，并指出提案的初衷是补充而非取代传统的 for 循环。</li>
<li><strong>x/exp 的定位:</strong> Russ Cox强调，x/exp 仓库并非随意尝试新事物的试验场，而是存放那些被认为是标准库潜在候选者的地方，因为即使是 x/exp 中的包，也需要长期支持。</li>
<li>DSL (领域特定语言) 的可能性: 有开发者提出了借鉴 jq 或 C# LINQ 的思路，通过 DSL 来解决迭代器链式操作的易用性问题。但 Russ Cox 认为这不符合 Go 当前的目标，且可能带来性能和复杂性问题。</li>
</ul>
<h2>最终的抉择：为何搁置？</h2>
<p>在 Go 1.23 发布一段时间后，经过充分的讨论和实践反馈，Russ Cox 和 Austin Clements 代表提案审查小组，宣布将此提案标记为<strong>“婉拒并撤回 (declined as retracted)”</strong>。</p>
<p>主要原因可以归纳为：</p>
<ol>
<li><strong>缺乏广泛共识与“过度抽象”的担忧:</strong> 官方团队认为，对于将这些适配器加入标准库并鼓励其广泛使用，社区并未形成足够强的共识。许多情况下，直接使用 for 循环可能更为清晰和符合 Go 的惯用法，而这些适配器可能导致“过度抽象”。</li>
<li><strong>实际使用体验与语法限制:</strong> 许多开发者在实际使用迭代器后发现，由于当前 Go 语言匿名函数语法的冗余以及缺乏流畅的链式调用机制，这些适配器的使用体验并不理想，甚至不如手写循环或自定义辅助函数来得直接。</li>
<li><strong>为第三方库发展留出空间:</strong> 官方认为，与其在标准库中提供一套可能不完美或引发争议的工具集，不如将这部分探索和创新留给社区和第三方库。撤回官方提案可以为第三方迭代器工具库的涌现和发展创造更有利的环境。</li>
<li><strong>迭代器特性尚年轻:</strong> Go 中的迭代器特性相对较新，社区和官方都需要更多时间来积累使用经验，观察哪些模式和辅助函数真正被广泛需要和接受。未来可能会基于更充分的数据和实践，提出更具针对性的小型提案。</li>
</ol>
<h2>展望与启示</h2>
<p>x/exp/xiter 提案的搁浅，并不意味着 Go 语言在迭代器支持上的停滞。相反，它反映了 Go 团队在语言发展上一贯的审慎和务实态度。</p>
<p>对 Go 开发者而言，这意味着：</p>
<ul>
<li><strong>range over func 依然强大:</strong> Go 1.23 提供的原生迭代器机制是核心，开发者可以充分利用它来构建高效、灵活的数据处理逻辑。</li>
<li><strong>自定义与第三方库是当前主流:</strong> 对于迭代器的转换、过滤、聚合等操作，目前主要依赖开发者自行编写辅助函数，或选用社区中涌现的第三方迭代器工具库（如 deedles.dev/xiter, github.com/bobg/seqs, github.com/jub0bs/iterutil 等在讨论中被提及的个人项目）。</li>
<li><strong>关注语言本身的演进:</strong> 诸如更简洁的匿名函数语法 (#21498) 等相关语言特性的提案，如果未来能被接受，可能会极大地改善函数式编程风格在 Go 中的体验，并可能为官方再次考虑标准化迭代器工具铺平道路。</li>
<li><strong>Go 的哲学不变:</strong> 清晰、简洁、可读性以及避免不必要的复杂性，仍然是 Go 语言设计的核心考量。任何新特性或库的引入，都将在此框架下被严格审视。</li>
</ul>
<p>x/exp/xiter 的讨论过程本身就是一次宝贵的社区实践，它汇集了众多 Go 开发者的智慧与经验，即便提案未被接纳，其间的深入思考和论证也为 Go 语言迭代器生态的未来发展指明了方向，并留下了丰富的参考。我们期待看到 Go 社区在迭代器领域持续探索，涌现出更多符合 Go 风格且能切实解决开发者痛点的优秀工具与实践。</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/29/xiter-declined/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用TLA+形式化验证Go并发程序</title>
		<link>https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus/</link>
		<comments>https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus/#comments</comments>
		<pubDate>Sun, 04 Aug 2024 23:12:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Abstraction]]></category>
		<category><![CDATA[behavior]]></category>
		<category><![CDATA[Claude]]></category>
		<category><![CDATA[consumer]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[deadlock]]></category>
		<category><![CDATA[DigitalSystem]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherConEurope]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[latex]]></category>
		<category><![CDATA[LeslieLamport]]></category>
		<category><![CDATA[liveness]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[modeling]]></category>
		<category><![CDATA[Next]]></category>
		<category><![CDATA[paxos]]></category>
		<category><![CDATA[pingcap]]></category>
		<category><![CDATA[producer]]></category>
		<category><![CDATA[properties]]></category>
		<category><![CDATA[raft]]></category>
		<category><![CDATA[safety]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[State]]></category>
		<category><![CDATA[TemporalLogic]]></category>
		<category><![CDATA[THEOREM]]></category>
		<category><![CDATA[TLA+]]></category>
		<category><![CDATA[tla-plus]]></category>
		<category><![CDATA[TLC-Checker]]></category>
		<category><![CDATA[vscode]]></category>
		<category><![CDATA[分布式事务]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[图灵奖]]></category>
		<category><![CDATA[安全属性]]></category>
		<category><![CDATA[属性]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[建模]]></category>
		<category><![CDATA[形式化验证]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[数字系统]]></category>
		<category><![CDATA[时序逻辑]]></category>
		<category><![CDATA[最终一致性]]></category>
		<category><![CDATA[活性属性]]></category>
		<category><![CDATA[消费者]]></category>
		<category><![CDATA[状态]]></category>
		<category><![CDATA[状态机]]></category>
		<category><![CDATA[生产者]]></category>
		<category><![CDATA[离散事件]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[蕴含]]></category>
		<category><![CDATA[行为]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4240</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus Writing is nature&#8217;s way of letting you know how sloppy your thinking is &#8211; Guindon 在2024年6月份举办的GopherCon Europe Berlin 2024上，一个叫Raghav Roy的印度程序员(听口音判断的)分享了Using Formal Reasoning to Build Concurrent Go Systems，介绍了如何使用形式化验证工具TLA+来验证Go并发程序的设计正确性。 TLA+是2013年图灵奖获得者、美国计算机科学家和数学家、分布式系统奠基性大神、Paxos算法和Latex的缔造者Leslie B. Lamport设计的一种针对数字系统(Digital Systems)的高级(high-level)建模语言，TLA+诞生于1999年，一直低调演进至今。 TLA+不仅可以对系统建模，还可以与模型验证工具，比如：TLC model checker，结合使用，对被建模系统的行为进行全面的验证。我们可以将TLA+看成一种专门用于数字系统建模和验证的DSL语言。 注：TLA是Temporal Logic of Actions的首字母缩写，Temporal Logic，即时序逻辑，是一种用于描述和推理系统行为随时间变化的逻辑框架，由Arthur Prior在1950年代后期引入逻辑学。在后面对TLA+的进一步介绍中，大家可能就会逐渐理解为什么Lamport给这门语言命名为TLA+了。 这不是我第一次接触TLA+，去年就花过一些时间了解过TLA+的资料，可能是因为姿势不够正确，没有在本博客留下只言片语，而这次我打算写点有关TLA+的东西。 1. 为什么需要TLA+ 从1999年Lamport发表的论文“Specifying Concurrent Systems with TLA+”以及他2014年在微软的演讲“Thinking Above the Code”中 ，我们大致可以得到Lamport在20多年前设计TLA+的朴素的动机：期望程序员能像科学家一样思考，在编码之前用一种精确的形式化的语言写出目标系统的spec，这个过程类似于建筑架构师在建筑施工之前编制建筑的蓝图(blueprint)。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus">本文永久链接</a> &#8211; https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus</p>
<blockquote>
<p>Writing is nature&#8217;s way of letting you know how sloppy your thinking is &#8211; Guindon</p>
</blockquote>
<p>在2024年6月份举办的<a href="https://www.youtube.com/playlist?list=PLtoVuM73AmsIf99_fXLq_ehe2tpGVJQiF">GopherCon Europe Berlin 2024</a>上，一个叫Raghav Roy的印度程序员(听口音判断的)分享了<a href="https://www.youtube.com/watch?v=yiVOJqXTWfc">Using Formal Reasoning to Build Concurrent Go Systems</a>，介绍了如何使用形式化验证工具<a href="https://en.wikipedia.org/wiki/TLA%2B">TLA+</a>来验证Go并发程序的设计正确性。</p>
<p>TLA+是2013年图灵奖获得者、美国计算机科学家和数学家、分布式系统奠基性大神、<a href="https://en.wikipedia.org/wiki/Paxos_algorithm">Paxos算法</a>和<a href="https://en.wikipedia.org/wiki/LaTeX">Latex</a>的缔造者<a href="https://en.wikipedia.org/wiki/Leslie_Lamport">Leslie B. Lamport</a>设计的一种针对数字系统(Digital Systems)的高级(high-level)建模语言，TLA+诞生于1999年，一直低调演进至今。</p>
<p>TLA+不仅可以对系统建模，还可以与模型验证工具，比如：TLC model checker，结合使用，对被建模系统的行为进行全面的验证。我们可以将TLA+看成一种专门用于数字系统建模和验证的<a href="https://tonybai.com/tag/dsl">DSL语言</a>。</p>
<blockquote>
<p>注：TLA是Temporal Logic of Actions的首字母缩写，<a href="https://en.wikipedia.org/wiki/Temporal_logic">Temporal Logic</a>，即时序逻辑，是一种用于描述和推理系统行为随时间变化的逻辑框架，由Arthur Prior在1950年代后期引入逻辑学。在后面对TLA+的进一步介绍中，大家可能就会逐渐理解为什么Lamport给这门语言命名为TLA+了。</p>
</blockquote>
<p>这不是我第一次接触TLA+，去年就花过一些时间了解过TLA+的资料，可能是因为姿势不够正确，没有在本博客留下只言片语，而这次我打算写点有关TLA+的东西。</p>
<h2>1. 为什么需要TLA+</h2>
<p>从1999年Lamport发表的论文“<a href="https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Specifying-Concurrent-Systems-with-TLA.pdf">Specifying Concurrent Systems with TLA+</a>”以及他2014年在微软的演讲“<a href="https://www.youtube.com/watch?v=-4Yp3j_jk8Q">Thinking Above the Code</a>”中 ，我们大致可以得到Lamport在20多年前设计TLA+的朴素的动机：<strong>期望程序员能像科学家一样思考，在编码之前用一种精确的形式化的语言写出目标系统的spec，这个过程类似于建筑架构师在建筑施工之前编制建筑的蓝图(blueprint)</strong>。</p>
<p>为什么要编写目标系统的spec呢？</p>
<p>综合来自Lamport的相关资料，大致可以梳理出以下两点：</p>
<ul>
<li>从程序员的角度来看，在开始编码之前，先在抽象的层面思考系统行为，而不是过早地陷入编程语言的具体语法中。并且先写下规格说明，可以帮助程序员明确需求，认知系统，发现潜在问题，并为后续的编码和维护提供指导。 </li>
<li>从系统复杂性的角度来看，对于日益复杂的并发和分布式系统，仅靠直觉思考很难保证正确性，传统的测试方法也已经不足以发现所有问题。这时候写spec(规格说明)并用配套的检查工具进行验证就变得非常必要。</li>
</ul>
<p>那为什么要新设计TLA+来写spec呢，而不是使用像C++这类编程语言，或是其他已存在的形式化语言来编写spec呢？</p>
<p>Lamport给出的理由有以下几个：</p>
<ul>
<li>
<p>编程语言的局限性：像C++这样的编程语言主要是为了实现而设计的，而不是为了spec。它们往往过于关注实现细节，而不是高层次的系统行为，缺乏描述并发和分布式系统所需的抽象能力，不适合表达系统的时序性质和不变量。</p>
</li>
<li>
<p>已有形式化语言的不足：当时存在的其他形式化语言大多存在要么过于学术化，难以在实际工程中应用，要么难以自然地表达并发和分布式系统的特性等问题；并且缺少工具支持，不具备spec验证功能。</p>
</li>
<li>
<p>数学建模的局限：纯粹的数学公式虽然精确，但对非数学背景的工程师来说难以理解和使用，缺乏工具支持，难以自动化验证，难以直接映射到系统设计和实现。</p>
</li>
</ul>
<p>Lamport设计的TLA+是建立在坚实的数学基础之上，这使得它能够支持严格的数学推理和证明与自动化验证工具（如TLC模型检查器）无缝集成。TLA+被设计为在高度抽象的层面描述系统，不会像编程语言那样受实现细节的束缚。此外，结合时序逻辑和状态机，TLA+可以描述并发和分布式系统，并在设计层面验证系统的正确性。</p>
<p>根据<a href="https://lamport.azurewebsites.net/tla/industrial-use.html">Lamport的不完全统计</a>，TLA+在Intel、Amazon、Microsoft等大厂都有应用，一些知名的算法以及开源项目也使用TLA+进行了形式化验证，比如Raft算法的作者就给出了<a href="https://github.com/ongardie/raft.tla">Raft算法的TLA+ spec</a>，国内分布式数据库厂商<a href="https://github.com/pingcap/tla-plus">pingcap也在项目中使用TLA+对raft算法以及分布式事务做了形式化的验证</a>。</p>
<p>在这些应用案例中，AWS的案例是典型代表。AWS也将应用TLA+过程中积累的经验以paper的形式发表了，其<a href="https://cacm.acm.org/magazines/2015/4/184701-how-amazon-web-services-uses-formal-methods/fulltext">论文集合</a>也被Lamport放置在其个人主页上了。从这些论文内容来看，AWS对TLA+的评价是很正面的：AWS使用TLA+对10个大型复杂的真实系统进行建模和验证，的确发现了多个难以通过其他方法发现的微妙错误。同时，通过精确描述设计，TLA+迫使工程师更清晰地思考，消除了“看似合理的含糊之处”。此外，AWS工程师认为TLA+ spec也是一种很好的文档形式，可以提供精确、简洁、可测试的设计描述，有助于新人快速理解系统。</p>
<p>铺垫了这么多，TLA+究竟是什么？它是如何在高级抽象层面对分布式系统和并发系统进行描述和验证的？接下来，我们就来看一下。</p>
<h2>2. Lamport对TLA+的定义</h2>
<p>在Lamport的论文、书籍以及一些演讲资料中，他是这么定义TLA+的：<strong>A language for high-level modeling digital systems</strong>。对于这个定义，我们可以“分段”来理解一下。</p>
<ul>
<li>Digital System</li>
</ul>
<p>什么是TLA+眼中的数字系统(Digital System)？Lamport认为数字系统包括算法(Algorithms)、程序(Programs)和计算机系统(Computer system)，它们有一个共同特点，那就是可以抽象为一个按离散事件序列(sequence of discrete events)进行持续执行和演进的物理系统，这是TLA+后续描述(specify)数字系统的基础。随着多核和云计算的兴起，并发程序和分布式的关键(critical)系统成为了TLA+的主要描述对象，这样的系统最复杂，最难正确实现，价值也最高，值得使用TLA+对其进行形式化的验证。</p>
<ul>
<li>High Level</li>
</ul>
<p>TLA+面向设计层面，在代码实现层面之上，实施于编写任何实现代码之前。此外，High Level也意味着可以忽略那些系统中不是很关键(less-critical)的部分以及低层次的实现细节。</p>
<p>去除细节进行简化的过程就是抽象（Abstraction），它是工程领域最重要的环节。抽象可以让我们理解复杂的系统，如果不了解系统，我们就无法对系统进行正确的建模并实现它。</p>
<p>而使用TLA+编写系统spec其实就是一个学习对系统进行抽象的过程，学会抽象思考，可以帮助工程师提高设计能力。</p>
<ul>
<li>Modeling</li>
</ul>
<p>TLA+是通过描述系统的行为(behavior)来对数字系统进行建模的。那么什么是系统的行为呢？如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-2.png" alt="" /><br />
<center>此图由claude sonnet 3.5根据我的prompt生成</center></p>
<p>行为被Lamport定义为一系列的状态（Sequence of States），这些状态仍然按顺序排列，表示系统随时间的演变。而状态本身则是对变量的赋值。状态之间的转换由动作(action)描述，而系统的正确性由属性(properties)指定。</p>
<p>这种方法特别适合建模并发和分布式系统，因为它允许我们精确地描述系统的所有可能行为，包括不同组件之间的交互和可能的竞争条件，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-3.png" alt="" /></p>
<p>在TLA+中，属性(properties)是用来描述系统应该满足的条件或特性，它们在验证系统行为的正确性方面起着关键作用。我们所说的系统工作正常就是指这些在执行过程中的属性都得到了满足。</p>
<p>在TLA+中，有两类属性是我们特别需要关注的，一类是安全属性（Safety Properties），一类则是活性属性（Liveness Properties）。前者确保“坏事永远不会发生”，比如使用不变量在并发系统中确保两个进程不会同时进入临界区；后者则是确保“好事最终会发生”，在分布式系统中的最终一致性（eventual consistency）是一个活性属性，它保证系统最终会达到一致的状态。TLA+允许我们精确地指定这些属性，然后使用TLC模型检查器来验证系统是否满足这些属性。这种方法特别适合于复杂的并发和分布式系统，因为它能够发现在传统测试中难以发现的微妙错误。</p>
<blockquote>
<p>注：关于TLA+可以用来形式化描述(specify)和验证(check)数字系统的底层数学理论，可以参考Lamport老爷子那本最新尚未完成的书籍<a href="https://lamport.azurewebsites.net/tla/science.pdf">A Science of Concurrent Programs(2024.6.7版)</a>。</p>
</blockquote>
<p>接下来，我们就来看看TLA+究竟如何编写。不过直接介绍TLA+语法比较抽象和枯燥，在我读过的TLA+语法资料中，Lamport在<a href="https://www.youtube.com/playlist?list=PLWAv2Etpa7AOAwkreYImYt0gIpOdWQevD">The TLA+ Video Course</a>第二讲中将一个C示例程序一步一步像数学推导一样转换为TLA+语法的讲解对我帮助非常大，我觉得有必要将这个示例放到这篇文章中。</p>
<h2>3. 从C代码到TLA+：转换步骤详解</h2>
<p>Lamport的这个过程展示了如何从一个具体的编程语言实现(以C代码为例)逐步抽象到一个数学化的、更加通用的系统描述。每一步都增加了抽象级别，最终得到一个可以用于形式化验证的TLA+规范(spec)。以下是这个演进过程的主要阶段：</p>
<h3>3.1 初始C程序分析</h3>
<p>下面是这个示例的原始C代码：</p>
<pre><code>int i;
void main() {
    i = someNumber();
    i = i + 1;
}
</code></pre>
<p>这不是一个并发程序，它只有一个执行路线(execution)，前面说过，一个行为(execution)是一个状态序列，我们就来定义这个状态序列以及它们之间的转换关系。</p>
<p>我们先识别出程序的状态变量：i以及引入的控制状态变量（PC），PC变量来表示程序的执行位置。接下来我们就来描述一个可以代码该程序所有状态的“状态机”。</p>
<h3>3.2 状态机描述</h3>
<p>该程序可以划分为三个状态：</p>
<ul>
<li>初始状态：i = 0, PC = “start”</li>
<li>中间状态：i in {0, 1, &#8230;, 1000}(这里限定了someNumber函数返回的数值范围), PC = “middle”</li>
<li>结束状态：i = i + 1, PC = “done”</li>
</ul>
<p>下面用自然语言描述一下上述状态的转换关系：</p>
<pre><code>if current value of pc equals "start"
    then next value of i in {0, 1, ..., 1000}
         next value of pc equals "middle"
    else if current value of pc equals "middle"
            then next value of i equals current value of i + 1
                 next value of pc equals "done"
            else no next values
</code></pre>
<p>接下来，我们就来将上述对于状态转换的描述变换一下，尽量用数学来表示。</p>
<h3>3.3 转换为数学表示</h3>
<p>这里的转换分为几步，我们逐一来看。</p>
<ul>
<li>换掉”current value of”</li>
</ul>
<pre><code>if pc equals "start"
    then next value of i in {0, 1, ..., 1000}
         next value of pc equals "middle"
    else if pc equals "middle"
            then next value of i equals i + 1
                 next value of pc equals "done"
            else no next values
</code></pre>
<p>替换后，pc即the current value of pc，i即current value of i。</p>
<ul>
<li>换掉”next value of”</li>
</ul>
<p>我们用i&#8217;换掉”next value of i”, 用pc&#8217;换掉”next value of pc”，结果如下：</p>
<pre><code>if pc equals "start"
    then i' in {0, 1, ..., 1000}
         pc' equals "middle"
    else if pc equals "middle"
            then i' equals i + 1
                 pc' equals "done"
            else no next values
</code></pre>
<ul>
<li>用”=”符号换掉equals</li>
</ul>
<p>替换的结果如下：</p>
<pre><code>if pc = "start"
    then i' in {0, 1, ..., 1000}
         pc' = "middle"
    else if pc = "middle"
            then i' = i + 1
                 pc' = "done"
            else no next values
</code></pre>
<ul>
<li>将in换为数学符号∈ </li>
</ul>
<pre><code>if pc = "start"
    then i' ∈ {0, 1, ..., 1000}
         pc' = "middle"
    else if pc = "middle"
            then i' = i + 1
                 pc' = "done"
            else no next values
</code></pre>
<h3>3.4 TLA+语法转换</h3>
<ul>
<li>将集合表示换为正式的数学符号</li>
</ul>
<p>{0, 1, &#8230;, 1000}并非数学表示集合的方式，替换后，结果如下：</p>
<pre><code>if pc = "start"
    then i' ∈ 0..1000
         pc' = "middle"
    else if pc = "middle"
            then i' = i + 1
                 pc' = "done"
            else no next values
</code></pre>
<p>这里0..1000使用了TLA+的集合表示语法。</p>
<ul>
<li>转换为单一公式(formula)</li>
</ul>
<p>将C代码转换为上面的最新代码后，你不要再按照C的语义去理解上述转换后的代码了。新代码并非是像C那样为了进行好一些计算而编写的一些指令，新代码是<strong>一个关于i、pc、i&#8217;和pc&#8217;的公式(formula)</strong>，这是理解从C带TLA+的最为关键的环节，即<strong>上述这段代码整体就是一个公式</strong>！</p>
<p>上述代码的意思并非if pc = “start”为真，然后执行then部分，否则执行else部分。其真正含义是如果pc = “start”为真，那么上述整个公式将等于then这个公式的值，否则整个公式将等于else公式的值。</p>
<p>不过我们看到在上面的then子句中存在两个独立的公式，以第一个then为例，两个独立公式分别为i&#8217; ∈ 0..1000和pc&#8217; = “middle”。这两个独立的公式之间是and的关系，我们需要将其转换为一个公式。TLA+中使用”/\”表示and连接，下面是使用”/\”将公式连接后的结果：</p>
<pre><code>if pc = "start"
    then (i' ∈ 0..1000) /\
         (pc' = "middle")
    else if pc = "middle"
            then (i' = i + 1) /\
                 (pc' = "done")
            else no next values
</code></pre>
<ul>
<li>改造else公式</li>
</ul>
<p>问题来了! 当存在某个状态，使得整个公式等于最后一个else公式的值时，我们发现这个值为”no next values”，而前面的then、else if then公式的值都为布尔值TRUE或FALSE。这里最后的ELSE公式，它的值应该为FALSE，无论i、pc、i&#8217;和pc&#8217;的值为什么，因此这里直接将其改造为FALSE：</p>
<pre><code>if pc = "start"
    then (i' ∈ 0..1000) /\
         (pc' = "middle")
    else if pc = "middle"
            then (i' = i + 1) /\
                 (pc' = "done")
            else FALSE
</code></pre>
<ul>
<li>TLA+的关键字为大写且TLA+源码为ASCII码</li>
</ul>
<p>if、then、else 这些都是TLA+的关键字，而TLA+的关键字通常为大写，并且TLA+源码为ASCII码，∈需换成\in。这样改变后的结果如下：</p>
<pre><code>IF pc = "start"
    THEN (i' \in 0..1000) /\
         (pc' = "middle")
    ELSE IF pc = "middle"
            THEN (i' = i + 1) /\
                 (pc' = "done")
            ELSE FALSE
</code></pre>
<p>到这里，我们就得到了一个美化后的的TLA+公式了!</p>
<h3>3.5 干掉if else</h3>
<p>前面说过，我们将C代码改造为了一个公式，但公式中依然有if else总是感觉有些格格不入，是不是可以干掉if else呢！我们来试一下！</p>
<p>我们先用A、B替换掉then语句中的两个公式:</p>
<pre><code>IF pc = "start"
    THEN A
    ELSE IF pc = "middle"
            THEN B
            ELSE FALSE
</code></pre>
<p>如果整个公式为TRUE，需要(pc = “start”)和A都为TRUE，或(pc = “middle”)和B都为TRUE。TLA+引入一个操作符\/表示or，这样整个公式为TRUE的逻辑就可以表示为：</p>
<pre><code>   ((pc = "start") /\ A)
\/ ((pc = "middle") /\ B)
</code></pre>
<p>好了，现在我们再把A和B换回到原先的公式：</p>
<pre><code>   ((pc = "start") /\
    (i' \in 0..1000) /\
    (pc' = "middle"))
\/ ((pc = "middle") /\
    (i' = i+1 ) /\
    (pc' = "done"))
</code></pre>
<p>你是不是感觉不够美观啊！TLA+提供了下面等价的、更美观的形式：</p>
<pre><code>\/ /\ pc = "start"
   /\ i' \in 0..1000
   /\ pc' = "middle"
\/ /\ pc = "middle"
   /\ i' = i+1
   /\ pc' = "done"
</code></pre>
<p>这种形式完全去掉了括号，并可以像列表一样表达公式！并且无论是/\还是\/都是可交换的(commutative)，顺序不影响公式的最终结果。</p>
<h3>3.6 完整的TLA+ spec</h3>
<p>从数学层面，上面C代码将被拆分为两个公式，一个是初始状态公式，一个是下个状态的公式：</p>
<pre><code>初始状态公式：(i = 0) /\ (pc = "start")
下一状态公式：
              \/ /\ pc = "start"
                 /\ i' \in 0..1000
                 /\ pc' = "middle"
              \/ /\ pc = "middle"
                 /\ i' = i+1
                 /\ pc' = "done"
</code></pre>
<p>但对于一个完整的TLA+ spec来说，还需要额外补充些内容：</p>
<pre><code>---- MODULE SimpleProgram ----

EXTENDS Integers
VARIABLES i, pc

Init == (pc = "start") /\ (i = 0)
Next == \/ /\ pc = "start"
           /\ i' \in 0..1000
           /\ pc' = "middle"
        \/ /\ pc = "middle"
           /\ i' = i + 1
           /\ pc' = "done"
====
</code></pre>
<p>一个完整的TLA+ spec是放在一个module中的，上面例子中module为SimpleProgram。TLA toolkit要求tla文件名要与module名相同，这样上面代码对应的tla文件应为SimpleProgram.tla。</p>
<p>EXTENDS会导入TLA+内置的标准module，这里的Integers就提供了基础的算术运算符，比如+和..。</p>
<p>VARIABLES声明了状态变量，比如这里的i和pc。变量加上&#8217;即表示该变量的下一个状态的值。</p>
<p>接下来便是公式的定义。Init和Next并非固定公式名字，你可以选择任意名字，但使用Init和Next是惯用法。</p>
<p>“====”用于标识一个module的Body内容的结束。</p>
<p>对于上面简单的C程序，这样的spec是可以的。但在实际使用中，spec中的Next一般会很长，一个好的实践是对其进行拆分。比如这里我们就将Next拆分为两个子公式：Pick和Add1：</p>
<pre><code>---- MODULE SimpleProgram ----

EXTENDS Integers
VARIABLES i, pc

Init == (pc = "start") /\ (i = 0)
Pick == /\ pc = "start"
        /\ i' \in 0..1000
        /\ pc' = "middle"
Add1 == /\ pc = "middle"
        /\ i' = i + 1
        /\ pc' = "done"
Next == Pick \/ Add1
====
</code></pre>
<h2>4. 使用TLA+ Toolkit验证spec</h2>
<p>Lamport提供了TLA+的Module Checker，我们可以从其主页提供的<a href="https://lamport.azurewebsites.net/tla/toolbox.html">工具包下载链接</a>下载TLA+ Toolkit。</p>
<p>先将上面的TLA+ spec存入一个名为SimpleProgram.tla的文件。然后打开TLA+ Toolkit，选择File -> Open spec -> Add New Spec&#8230;，然后选择你本地的SimpleProgram.tla即可加载该spec：</p>
<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-4.png" alt="" /></p>
<p>之后，我们可以点击菜单项“TLC Model Checker” -> New Model，便可以为该tla建立一个model配置(去掉deadlock)，运行check后，你能看到下面结果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-5.png" alt="" /></p>
<p>我们看到model check一共检查了2003个不同的状态。</p>
<blockquote>
<p>注：TLA+还提供了一个<a href="https://github.com/tlaplus/vscode-tlaplus">Visual Studio Code的扩展</a>，也可以用来specify和check model。</p>
</blockquote>
<h2>5. 使用TLA+验证Go并发程序</h2>
<p>Go语言因其强大的并发编程能力而备受青睐。然而，Go的并发方案虽然简单，但也并非银弹。随着并发程序复杂性的增加，开发者常常面临着难以发现和调试的错误，如死锁和竞态条件。这些问题不仅影响程序的正确性，还可能导致严重的系统故障。对于Go开发的并发系统的关键部分，采用TLA+进行形式化的验证是一个不错的提高系统正确性和可靠性的方法。</p>
<p>接下来，我们就建立一个生产者和消费者的Go示例，然后使用TLA+为其建模并check。理论上应该是先有设计思路，再TLA+验证设计，再进行代码实现，这里的Go代码主要是为了“描述”该并发程序的需求和行为逻辑。</p>
<pre><code>// go-and-tla-plus/producer-consumer/main.go
package main

import (
    "fmt"
    "sync"
)

func producer(ch chan&lt;- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i &lt; 5; i++ {
        ch &lt;- i
    }
    close(ch)
}

func consumer(ch &lt;-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for num := range ch {
        fmt.Println("Consumed:", num)
    }
}

func main() {
    ch := make(chan int)
    var wg sync.WaitGroup
    wg.Add(2)

    go producer(ch, &amp;wg)
    go consumer(ch, &amp;wg)

    wg.Wait()
}
</code></pre>
<p>任何Go初学者都可以很容易读懂上面的程序逻辑：Producer生产0到4四个数，每生成一个就通过unbuffered channel发出，consumer从channel接收数字并消费。Producer生产完毕后，关闭channel。Consumer消费完所有数字后，退出，程序终止。</p>
<p>下面是使用TLA+编写的ProducerConsumer的完整Spec：</p>
<pre><code>// go-and-tla-plus/producer-consumer/ProducerConsumer.tla

---- MODULE ProducerConsumer ----
EXTENDS Integers, Sequences

VARIABLES
    ch,           \* 通道内容
    produced,     \* 已生产的消息数
    consumed,     \* 已消费的消息数
    closed        \* 通道是否关闭

TypeOK ==
    /\ ch \in Seq(0..4)
    /\ produced \in 0..5
    /\ consumed \in 0..5
    /\ closed \in BOOLEAN

Init ==
    /\ ch = &lt;&lt;&gt;&gt;
    /\ produced = 0
    /\ consumed = 0
    /\ closed = FALSE

Produce ==
    /\ produced &lt; 5
    /\ ch = &lt;&lt;&gt;&gt;
    /\ ~closed
    /\ ch' = Append(ch, produced)
    /\ produced' = produced + 1
    /\ UNCHANGED &lt;&lt;consumed, closed&gt;&gt;

Close ==
    /\ produced = 5
    /\ ch = &lt;&lt;&gt;&gt;
    /\ ~closed
    /\ closed' = TRUE
    /\ UNCHANGED &lt;&lt;ch, produced, consumed&gt;&gt;

Consume ==
    /\ ch /= &lt;&lt;&gt;&gt;
    /\ ch' = Tail(ch)
    /\ consumed' = consumed + 1
    /\ UNCHANGED &lt;&lt;produced, closed&gt;&gt;

Next ==
    \/ Produce
    \/ Close
    \/ Consume

Fairness ==
    /\ SF_&lt;&lt;ch, produced, closed&gt;&gt;(Produce)
    /\ SF_&lt;&lt;produced, closed&gt;&gt;(Close)
    /\ SF_&lt;&lt;ch&gt;&gt;(Consume)

Spec == Init /\ [][Next]_&lt;&lt;ch, produced, consumed, closed&gt;&gt; /\ Fairness

THEOREM Spec =&gt; []TypeOK

ChannelEventuallyEmpty == &lt;&gt;(ch = &lt;&lt;&gt;&gt;)
AllMessagesProduced == &lt;&gt;(produced = 5)
ChannelEventuallyClosed == &lt;&gt;(closed = TRUE)
AllMessagesConsumed == &lt;&gt;(consumed = 5)

====
</code></pre>
<p>这个Spec不算长，但也不短，你可能看不大懂，没关系，接下来我们就来说说从main.go到ProducerConsumer.tla的建模过程，并重点解释一下上述TLA+代码中的重要语法。</p>
<p>针对main.go中体现出来的Producer和Consumer的逻辑，我们首先需要识别关键组件：生产者、消费者和一个通道(channel)，然后我们需要确定状态变量，包括：通道内容(ch)、已生产消息数(produced)、已消费消息数(consumed)、通道是否关闭(closed)。</p>
<p>接下来，我们就要定义action，即导致状态变化的step，包括Produce、Consume和Close。</p>
<p>最后，我们需要设置初始状态Init和下一个状态Next，并定义安全属性(TypeOK)和一些活性属性(如AllMessagesConsumed等)</p>
<p>现在，我们结合上述TLA+的代码，来说一下上述这些逻辑是如何在TLA+中实现的：</p>
<pre><code>---- MODULE ProducerConsumer ----
</code></pre>
<p>这一行定义了模块名称，模块名称与文件名字(ProducerConsumer.tla)要一致，否则TLA+ Toolkit在Open Spec时会报错。</p>
<pre><code>EXTENDS Integers, Sequences
</code></pre>
<p>这行会导入整数和序列模块，以使用相关运算符。</p>
<pre><code>VARIABLES
    ch,           \* 通道内容
    produced,     \* 已生产的消息数
    consumed,     \* 已消费的消息数
    closed        \* 通道是否关闭
</code></pre>
<p>这里使用VARIBALES关键字定义了四个状态变量，整个TLA+程序的函数逻辑就围绕这四个变量进行，TLC Model check也是基于这些状态变量对TLA+ module进行验证。</p>
<pre><code>TypeOK ==
    /\ ch \in Seq(0..4)
    /\ produced \in 0..5
    /\ consumed \in 0..5
    /\ closed \in BOOLEAN
</code></pre>
<p>定义不变量，确保变量状态在系统的所有行为过程中始终保持在合理范围内，该TypeOK不变量即是整个程序的安全属性。</p>
<pre><code>Init ==
    /\ ch = &lt;&lt;&gt;&gt;
    /\ produced = 0
    /\ consumed = 0
    /\ closed = FALSE
</code></pre>
<p>这是初始状态的公式，对应了四个变量的初始值。</p>
<pre><code>Produce ==
    /\ produced &lt; 5
    /\ ch = &lt;&lt;&gt;&gt;
    /\ ~closed
    /\ ch' = Append(ch, produced)
    /\ produced' = produced + 1
    /\ UNCHANGED &lt;&lt;consumed, closed&gt;&gt;
</code></pre>
<p>这里定义了生产操作的公式，只有在produced &lt; 5，ch为空且closed不为true时，才会生产下一个数字。这里设定ch为空作为前提条件，主要是为了体现Channel的unbuffered的性质。</p>
<pre><code>Close ==
    /\ produced = 5
    /\ ch = &lt;&lt;&gt;&gt;
    /\ ~closed
    /\ closed' = TRUE
    /\ UNCHANGED &lt;&lt;ch, produced, consumed&gt;&gt;
</code></pre>
<p>这里定义了关闭操作的公式，这里的ch = &lt;&lt;>>子公式的目的是等消费完之后再关闭channel，当然这里与Go的机制略有差异。</p>
<pre><code>Consume ==
    /\ ch /= &lt;&lt;&gt;&gt;
    /\ ch' = Tail(ch)
    /\ consumed' = consumed + 1
    /\ UNCHANGED &lt;&lt;produced, closed&gt;&gt;
</code></pre>
<p>这里定义了消费操作的公式，只有channel不为空，才进行消费。</p>
<pre><code>Next ==
    \/ Produce
    \/ Close
    \/ Consume
</code></pre>
<p>这里基于三个操作公式定义了下一个状态(Next)的公式，使用\/运算符将这三个操作连接起来，表示下一步可以执行其中任意一个操作。</p>
<pre><code>Fairness ==
    /\ SF_&lt;&lt;ch, produced, closed&gt;&gt;(Produce)
    /\ SF_&lt;&lt;produced, closed&gt;&gt;(Close)
    /\ SF_&lt;&lt;ch&gt;&gt;(Consume)
</code></pre>
<p>这里定义了公平性条件，确保各操作最终会被执行。</p>
<pre><code>Spec == Init /\ [][Next]_&lt;&lt;ch, produced, consumed, closed&gt;&gt; /\ Fairness
</code></pre>
<p>这里定义了整个并发程序的规范，包括初始条件Init和下一步动作约束以及Fairness条件。/\连接的第二段Next表示系统的每一步都必须符合Next定义的可能动作，并且不会改变 &lt;&lt;ch, produced, consumed, closed>> 元组中变量之外的其他变量。Fairness 表示系统必须满足前面定义的 Fairness 条件。</p>
<pre><code>THEOREM Spec =&gt; []TypeOK
</code></pre>
<p>这是一个定理，表示如果系统满足Spec规范，则一定会满足TypeOK这个不变量。其中的”=>”是蕴含的意思，A => B表示如果A为真，那么B必然为真。用一个例子可以解释这点，如果x > 3为真，那么 x > 1 必为真，我们可以将其写为：x > 3 => x > 1。</p>
<pre><code>ChannelEventuallyEmpty == &lt;&gt;(ch = &lt;&lt;&gt;&gt;)
AllMessagesProduced == &lt;&gt;(produced = 5)
ChannelEventuallyClosed == &lt;&gt;(closed = TRUE)
AllMessagesConsumed == &lt;&gt;(consumed = 5)
</code></pre>
<p>这里定义了四个活性属性，用于在TLC Model check时验证最终状态使用，其中：ChannelEventuallyEmpty表示最终消息队列 ch 一定会为空；AllMessagesProduced表示最终一定会生产5条消息；ChannelEventuallyClosed表示最终消息队列一定会被关闭；AllMessagesConsumed表示最终一定会消费5条消息。</p>
<p>接下来，我们可以使用前面提到的TLA+ Toolbox来check该spec，下面是model的设置和model check的结果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-6.png" alt="" /><br />
<center>model设置</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-7.png" alt="" /><br />
<center>check结果</center></p>
<blockquote>
<p>注：在VSCode中使用TLA+插件的Checker对上述tla进行check，会出现不满足活性属性的error结果。</p>
</blockquote>
<h2>6. 小结</h2>
<p>在这篇文章中，我们从Lamport提供的C语言代码示例出发，一步步介绍了如何将其转换为TLA+ spec，并使用TLA+ Toolkit进行验证。然后我们又以一个Go语言的生产者-消费者并发程序为例，展示了如何使用TLA+对其进行建模和验证。</p>
<p>不过我必须承认，TLA+这种形式化验证语言是极小众的。对大多数程序员来说，可能没什么实际帮助。即便是在大厂，真正使用TLA+对分布式系统进行形式化验证的案例也很少。</p>
<p>但是，我认为TLA+仍然有其独特的价值：</p>
<ul>
<li>它迫使我们用更抽象和精确的方式思考系统设计，有助于发现潜在的问题。</li>
<li>对于一些关键的分布式系统组件，使用TLA+进行验证可以极大地提高可靠性。</li>
<li>学习TLA+的过程本身就是一次提升系统设计能力的过程。</li>
</ul>
<p>当然，形式化方法并非万能。比如它无法解决性能退化等问题，也不能验证代码是否正确实现了设计。我们应该将其视为系统设计和验证的补充工具，而不是替代品。</p>
<p>总之，虽然TLA+可能不适合所有人，但对于那些构建复杂分布式系统的工程师来说，它仍然是一个值得学习和使用的强大工具。我希望这篇文章能为大家了解和入门TLA+提供一些帮助。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/blob/master/go-and-tla-plus">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/blob/master/go-and-tla-plus</p>
<p>本文部分源代码由claude 3.5 sonnet生成。</p>
<h2>7. 参考资料</h2>
<ul>
<li><a href="https://lamport.azurewebsites.net/tla/tla.html">The TLA+ Home Page</a> &#8211; https://lamport.azurewebsites.net/tla/tla.html</li>
<li>《<a href="https://book.douban.com/subject/30348788/">Practical TLA+：Planning Driven Development</a>》- https://book.douban.com/subject/30348788/</li>
<li><a href="https://www.learntla.com/">Learn TLA+</a> &#8211; https://www.learntla.com/</li>
<li>《[A Science of Concurrent Programs]》(https://lamport.azurewebsites.net/tla/science.pdf) &#8211; https://lamport.azurewebsites.net/tla/science.pdf</li>
<li>《<a href="https://book.douban.com/subject/3752446/">Specifying Systems: The TLA+ Language and Tools for Hardware and Software Engineers</a>》- https://book.douban.com/subject/3752446/</li>
<li><a href="https://www.linuxfoundation.org/press/linux-foundation-launches-tlafoundation">Linux Foundation Announces Launch of TLA+ Foundation</a> &#8211; https://www.linuxfoundation.org/press/linux-foundation-launches-tlafoundation</li>
<li><a href="https://foundation.tlapl.us/">TLA+ Foundation</a> &#8211; https://foundation.tlapl.us/</li>
<li><a href="https://github.com/pingcap/tla-plus">TLA+ in TiDB</a> &#8211; https://github.com/pingcap/tla-plus</li>
<li><a href="https://will62794.github.io/tla-web">TLA+ Web Explorer</a> &#8211; https://will62794.github.io/tla-web</li>
<li><a href="https://github.com/tlaplus/vscode-tlaplus">TLA+ language support for Visual Studio Code</a> &#8211; https://github.com/tlaplus/vscode-tlaplus</li>
<li><a href="https://lamport.azurewebsites.net/tla/formal-methods-amazon.pdf">Use of Formal Methods at Amazon Web Services</a> &#8211; https://lamport.azurewebsites.net/tla/formal-methods-amazon.pdf</li>
<li><a href="https://www.youtube.com/playlist?list=PLWAv2Etpa7AOAwkreYImYt0gIpOdWQevD">Leslie Lamport&#8217;s The TLA+ Video Course</a> &#8211; https://www.youtube.com/playlist?list=PLWAv2Etpa7AOAwkreYImYt0gIpOdWQevD</li>
<li><a href="https://lamport.azurewebsites.net/video/videos.html">Leslie Lamport&#8217;s The TLA+ Video Course homepage</a> &#8211; https://lamport.azurewebsites.net/video/videos.html</li>
<li><a href="https://lamport.azurewebsites.net/video/video1-script.pdf">Introduction to TLA+</a> &#8211; https://lamport.azurewebsites.net/video/video1-script.pdf</li>
<li><a href="https://groups.google.com/g/tlaplus">TLA+ Google Group</a> &#8211; https://groups.google.com/g/tlaplus</li>
<li><a href="https://www.hillelwayne.com/">HILLEL WAYNE Blog</a> &#8211; https://www.hillelwayne.com/</li>
<li><a href="https://www.youtube.com/watch?v=-4Yp3j_jk8Q">Leslie Lamport: Thinking Above the Code</a> &#8211; https://www.youtube.com/watch?v=-4Yp3j_jk8Q</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</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/08/05/formally-verify-concurrent-go-programs-using-tla-plus/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>有效表达软件架构的最小图集</title>
		<link>https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture/</link>
		<comments>https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture/#comments</comments>
		<pubDate>Wed, 06 Dec 2023 13:29:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ADL]]></category>
		<category><![CDATA[API网关]]></category>
		<category><![CDATA[Arc42]]></category>
		<category><![CDATA[Architect]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[C4]]></category>
		<category><![CDATA[C4Model]]></category>
		<category><![CDATA[component]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[diagram]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[mermaid]]></category>
		<category><![CDATA[MVC]]></category>
		<category><![CDATA[PlantUML]]></category>
		<category><![CDATA[RUP]]></category>
		<category><![CDATA[Structurizr]]></category>
		<category><![CDATA[Swagger]]></category>
		<category><![CDATA[tradeoff]]></category>
		<category><![CDATA[UML]]></category>
		<category><![CDATA[View]]></category>
		<category><![CDATA[websocket]]></category>
		<category><![CDATA[代码视图]]></category>
		<category><![CDATA[团队协作]]></category>
		<category><![CDATA[图形化]]></category>
		<category><![CDATA[场景视图]]></category>
		<category><![CDATA[容器视图]]></category>
		<category><![CDATA[序列图]]></category>
		<category><![CDATA[开发视图]]></category>
		<category><![CDATA[形式化]]></category>
		<category><![CDATA[控制流图]]></category>
		<category><![CDATA[数据流图]]></category>
		<category><![CDATA[架构]]></category>
		<category><![CDATA[架构师]]></category>
		<category><![CDATA[架构设计]]></category>
		<category><![CDATA[流程图]]></category>
		<category><![CDATA[消费者]]></category>
		<category><![CDATA[物理视图]]></category>
		<category><![CDATA[生产者]]></category>
		<category><![CDATA[用例图]]></category>
		<category><![CDATA[类图]]></category>
		<category><![CDATA[组件视图]]></category>
		<category><![CDATA[结构化]]></category>
		<category><![CDATA[统一建模语言]]></category>
		<category><![CDATA[自然语言]]></category>
		<category><![CDATA[草图]]></category>
		<category><![CDATA[语境图]]></category>
		<category><![CDATA[进程视图]]></category>
		<category><![CDATA[逻辑视图]]></category>
		<category><![CDATA[金字塔写作]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4065</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture 无论你是专职的软件架构师，还是在团队内兼职充当软件架构师角色的开发人员，一旦你处在软件架构师这个位置上，你自然就会遇到软件架构设计的三个困惑： 如何更深刻地理解业务； 如何更正确地取舍(包括技术性和业务性的)； 如何更有效地表达软件架构。 以上每个困惑展开来写都够写一本书的。而在这篇文章中，我仅聚焦最后一个困惑，聊聊我心目中表达软件架构的有效方式 &#8212; 最小图集(Minimum Diagram Set)。 1. 为什么软件架构需要有效表达 众所周知，软件架构承载着系统关键的技术决策和业务约束，指导着复杂软件的构建与演进，是实现软件系统的蓝图。但并不是说有了好的软件架构就一定可以做出好的软件系统，软件系统最终还是要经由开发人员来实现。 如果说架构师是软件架构的生产者，那么开发人员可以理解为是软件架构的消费者。但和一件普通商品一样，往往消费者很难Get到产品设计者的全部idea，产品越复杂，消费者Get到的比例越低，于是商品的生产者就会绞尽脑汁地制作产品说明书、功能演示视频等，目的就是想从不同角度更多、更有效的表达自己的商品的特性。对于普通商品而言，消费者Get程度低顶多是少用几个功能特性；但对于架构师生产的“产品”：架构设计成果而言，如果其消费者开发人员Get的程度低，那影响就会很严重，甚至可能会导致软件系统的开发彻底失败。 并且更不幸的是：我们的软件系统都是“复杂产品”。这样，如何表达和解读软件架构，弥合生产者与消费者之间的Gap，让开发者更多更深刻的理解软件架构这件“产品”便成为了架构师的困惑，日常架构设计工作中的难题，也是业界探索的重要课题。 架构设计是架构师与开发者之间的协议，只有有效的、充分的表达，协议才能被共识理解和忠实执行。业界在有效表达软件架构这条路上摸索了很多年，下面简单说说架构设计表达的演进历程。 2. 软件架构表达方式演进简史 软件架构表达的目的就是要直观地传达架构设计人员的思想和意图，使开发团队可以达成对架构设计的一致理解，促进各个团队协作，并作为开发人员编写代码以及管理人员推进项目的重要指导与参考。 2.1 自然语言描述 在软件工程的早期阶段，软件架构设计通常使用自然语言（如英语）进行描述。架构师会使用文档、规范和书面记录来表达架构设计的概念、原则、结构、组件和交互。然而，自然语言描述存在歧义性、解释性不足、理解起来较慢的问题，可能导致误解和沟通障碍。 2.2 图形化表达 人类大脑中传输的信息90%是视觉信息，其处理图形的速度要比处理文字的速度快上万倍。于是随着软件架构的复杂性增加，人们开始采用更直观、更易理解的图形化方法来描述架构设计(并辅以自然语言的文字描述)。 提到图形化表达，最简单的方法就是使用一支笔+一张白纸，基于自己“创造”的符号绘制草图(Sketch，以下草图来自c4model.com)： 这种非规范的框线草图虽然提供了灵活性，但付出的代价却是一致性，因为大家都在创造自己的制图符号，而不是使用统一的标准。 2.3 结构化的图形表达 结构化图是在设计表达迈向标准化方面走出的重要一步。结构化图包括数据流图、控制流图、层次图、组件图等，用于可视化表示系统的组件、模块、依赖关系和交互流程等(下图中元素来自维基百科)。 作为一种可以直观可视化描述与沟通架构设计的方式，结构化图形成为了表达架构设计的常见方法之一。不过，早期结构化表达的类型有限，无法涵盖所有环节，有的也没有形成标准，为了提高标准化程度，满足架构设计表达的全部需求，人们在二十世纪末推出了大一统的图形化建模语言UML。 2.4 统一建模语言(UML) 统一建模语言（Unified Modeling Language，UML）是一种通用的标准化、图形化建模语言，广泛用于软件架构和设计的表示，在软件架构表达方法方面具有里程碑意义： UML第一次在规范层面对图形表示进行了标准化，它提供了一组规范化的图形符号，用于描述系统的结构、行为和交互。在那个Rational统一过程（RUP）以及面向对象设计方法如日中天的时代，人们每每进行设计时，言必称使用UML。UML在图形化、标准化表达设计图方面走到了至今为止都无人企及的高峰。 但是，20多年后的今天，UML并没有成为当时标准出品方期望的那个样子，没能成为表达软件系统设计的主流符号系统。也许是它的复杂性阻碍了有效沟通，让人们看到它的spec后就“望而却步”了。不过UML并没有死掉，它依然活着，UML规范中的一些图(Diagram)依然被大家常用，比如：序列图(Sequence Diagram)、用例图(Use Case Diagram)、类图(Class Diagram)等。 2.5 形式化表达 业界在寻求图形化表达标准化的同时，也有一个分支在寻求用自然语言的“标准化”表达方法，这就是软件架构设计的形式化表达，在这个领域形成的语言被称为架构描述语言(ADL)。ADL提供了一组特定的语法和语义规则，用于定义系统的组件、接口、依赖关系、行为和性能特征。ADL使架构师能够使用精确的语言来表达和分析架构设计，支持自动化的验证和分析工具，在学术研究这个小众领域还是很有受众的。不过，显然在大多数工程化淋雨，形式化表达门槛太高，对于软件架构在团队内快速有效建立共识起不到什么作用。 下面是一些ADL的实现，感兴趣的童鞋可以了解一下： xArch/xADL ACME AADL 2.6 多视角的表达 有了UML这个前车之鉴后，人们似乎也放弃了在图记号“标准化”之路上的继续探索了，而是回归问题本源：怎么有效，就怎么来。 在工程实践中，人们认清了一个事实：很难在一张大图(Diagram)中进行软件架构设计的有效表达。于是大家开始采用“盲人摸象”的策略，将一个架构按不同视角表达为不同的图(Diagram)，这样当开发人员将多个视角形成的图都理解后，也就理解了整个架构设计。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture">本文永久链接</a> &#8211; https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture</p>
<p>无论你是专职的软件架构师，还是在团队内兼职充当软件架构师角色的开发人员，一旦你处在软件架构师这个位置上，你自然就会遇到软件架构设计的三个困惑：</p>
<ul>
<li>如何更深刻地理解业务；</li>
<li>如何更正确地取舍(包括技术性和业务性的)；</li>
<li>如何更有效地表达软件架构。</li>
</ul>
<p>以上每个困惑展开来写都够写一本书的。而在这篇文章中，我仅聚焦最后一个困惑，聊聊我心目中表达软件架构的有效方式 &#8212; 最小图集(Minimum Diagram Set)。</p>
<h2>1. 为什么软件架构需要有效表达</h2>
<p>众所周知，软件架构承载着系统关键的技术决策和业务约束，指导着复杂软件的构建与演进，是实现软件系统的蓝图。但并不是说有了好的软件架构就一定可以做出好的软件系统，<strong>软件系统最终还是要经由开发人员来实现</strong>。</p>
<p>如果说架构师是软件架构的生产者，那么开发人员可以理解为是软件架构的消费者。但和一件普通商品一样，往往消费者很难Get到产品设计者的全部idea，产品越复杂，消费者Get到的比例越低，于是商品的生产者就会绞尽脑汁地制作产品说明书、功能演示视频等，目的就是想从不同角度更多、更有效的表达自己的商品的特性。对于普通商品而言，消费者Get程度低顶多是少用几个功能特性；但对于架构师生产的“产品”：架构设计成果而言，如果其消费者开发人员Get的程度低，那影响就会很严重，甚至可能会导致软件系统的开发彻底失败。</p>
<p>并且更不幸的是：我们的软件系统都是“复杂产品”。这样，如何表达和解读软件架构，弥合生产者与消费者之间的Gap，让开发者更多更深刻的理解软件架构这件“产品”便成为了架构师的困惑，日常架构设计工作中的难题，也是业界探索的重要课题。</p>
<p>架构设计是架构师与开发者之间的协议，只有有效的、充分的表达，协议才能被共识理解和忠实执行。业界在有效表达软件架构这条路上摸索了很多年，下面简单说说架构设计表达的演进历程。</p>
<h2>2. 软件架构表达方式演进简史</h2>
<p>软件架构表达的目的就是要直观地传达架构设计人员的思想和意图，使开发团队可以达成对架构设计的一致理解，促进各个团队协作，并作为开发人员编写代码以及管理人员推进项目的重要指导与参考。</p>
<h3>2.1 自然语言描述</h3>
<p>在软件工程的早期阶段，软件架构设计通常使用自然语言（如英语）进行描述。架构师会使用文档、规范和书面记录来表达架构设计的概念、原则、结构、组件和交互。然而，自然语言描述存在歧义性、解释性不足、理解起来较慢的问题，可能导致误解和沟通障碍。</p>
<h3>2.2 图形化表达</h3>
<p>人类大脑中传输的信息90%是视觉信息，其处理图形的速度要比处理文字的速度快上万倍。于是随着软件架构的复杂性增加，人们开始采用更直观、更易理解的图形化方法来描述架构设计(并辅以自然语言的文字描述)。</p>
<p>提到图形化表达，最简单的方法就是使用<strong>一支笔+一张白纸</strong>，基于自己“创造”的符号绘制草图(Sketch，以下草图来自c4model.com)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-2.png" alt="" /></p>
<p>这种非规范的框线草图虽然提供了灵活性，但付出的代价却是一致性，因为大家都在创造自己的制图符号，而不是使用统一的标准。</p>
<h3>2.3 结构化的图形表达</h3>
<p>结构化图是在设计表达迈向标准化方面走出的重要一步。结构化图包括数据流图、控制流图、层次图、组件图等，用于可视化表示系统的组件、模块、依赖关系和交互流程等(下图中元素来自维基百科)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-3.png" alt="" /></p>
<p>作为一种可以直观可视化描述与沟通架构设计的方式，结构化图形成为了表达架构设计的常见方法之一。不过，早期结构化表达的类型有限，无法涵盖所有环节，有的也没有形成标准，为了提高标准化程度，满足架构设计表达的全部需求，人们在二十世纪末推出了大一统的图形化建模语言UML。</p>
<h3>2.4 <a href="https://www.uml.org">统一建模语言(UML)</a></h3>
<p>统一建模语言（Unified Modeling Language，UML）是一种通用的标准化、图形化建模语言，广泛用于软件架构和设计的表示，在软件架构表达方法方面具有里程碑意义：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-4.jpeg" alt="" /></p>
<p>UML第一次在规范层面对图形表示进行了标准化，它提供了一组规范化的图形符号，用于描述系统的结构、行为和交互。在那个Rational统一过程（RUP）以及面向对象设计方法如日中天的时代，人们每每进行设计时，言必称使用UML。UML在图形化、标准化表达设计图方面走到了至今为止都无人企及的高峰。</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-5.png" alt="" /></p>
<p>但是，20多年后的今天，UML并没有成为当时标准出品方期望的那个样子，没能成为表达软件系统设计的主流符号系统。也许是<strong>它的复杂性阻碍了有效沟通</strong>，让人们看到它的spec后就“望而却步”了。不过UML并没有死掉，它依然活着，UML规范中的一些图(Diagram)依然被大家常用，比如：<a href="https://www.uml-diagrams.org/sequence-diagrams.html">序列图(Sequence Diagram)</a>、<a href="https://www.uml-diagrams.org/use-case-diagrams.html">用例图(Use Case Diagram)</a>、<a href="https://www.uml-diagrams.org/class-diagrams-overview.html">类图(Class Diagram)</a>等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-6.png" alt="" /></p>
<h3>2.5 形式化表达</h3>
<p>业界在寻求图形化表达标准化的同时，也有一个分支在寻求用自然语言的“标准化”表达方法，这就是软件架构设计的形式化表达，在这个领域形成的语言被称为<a href="https://en.wikipedia.org/wiki/Architecture_description_language">架构描述语言(ADL)</a>。ADL提供了一组特定的语法和语义规则，用于定义系统的组件、接口、依赖关系、行为和性能特征。ADL使架构师能够使用精确的语言来表达和分析架构设计，支持自动化的验证和分析工具，在学术研究这个小众领域还是很有受众的。不过，显然在大多数工程化淋雨，形式化表达门槛太高，对于软件架构在团队内快速有效建立共识起不到什么作用。</p>
<p>下面是一些ADL的实现，感兴趣的童鞋可以了解一下：</p>
<ul>
<li><a href="http://isr.uci.edu/projects/xarchuci/">xArch/xADL</a> </li>
<li><a href="http://www.cs.cmu.edu/~able/">ACME</a></li>
<li><a href="http://www.aadl.info/">AADL</a></li>
</ul>
<h3>2.6 多视角的表达</h3>
<p>有了UML这个前车之鉴后，人们似乎也放弃了在图记号“标准化”之路上的继续探索了，而是回归问题本源：<strong>怎么有效，就怎么来</strong>。</p>
<p>在工程实践中，人们认清了一个事实：<strong>很难在一张大图(Diagram)中进行软件架构设计的有效表达</strong>。于是大家开始采用“盲人摸象”的策略，将一个架构按不同视角表达为不同的图(Diagram)，这样<strong>当开发人员将多个视角形成的图都理解后，也就理解了整个架构设计</strong>。</p>
<p>按照这个多视角表达的思路(也被称为是一种<strong>软件架构建模</strong>思路)，业界先后出现了：</p>
<ul>
<li><a href="https://arxiv.org/abs/2006.04975">Kruchten 4+1 views</a></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-7.png" alt="" /></p>
<p>逻辑视图（Logical View）关注系统的功能和功能模块，描述系统中各个模块之间的关系、接口和行为。它展示了系统的静态结构和动态行为，以及模块之间的通信和信息流。</p>
<p>进程视图（Process View）描述系统的并发和分布式特性，关注系统中的进程、线程、任务以及它们之间的关系和通信。该视图展示了系统的并发性、性能、可伸缩性等方面。</p>
<p>物理视图（Physical View）描述系统在硬件和软件环境中的部署和分布情况，包括物理设备、网络拓扑、软件组件的部署位置等。它关注系统的部署架构、可靠性、安全性等方面。</p>
<p>开发视图（Development View）关注系统的软件开发过程和组织结构，描述软件模块的组织、构建、测试和部署过程。它展示了软件开发团队的组织结构、开发工具、版本控制等方面。</p>
<p>场景视图（Scenario View）描述系统在特定使用情境下的行为和交互，以用户场景、用例或故事来说明系统的功能和行为。它帮助验证和验证系统架构的正确性和适应性。</p>
<ul>
<li><a href="https://c4model.com">C4 model</a></li>
</ul>
<p>C4模型是一种简洁、易于理解的软件架构建模方法，由Simon Brown提出。它通过四个层次的视图来描述软件系统的不同方面，包括语境视图(Context Diagram，这里借鉴了《<a href="https://book.douban.com/subject/26248182/">程序员必读之软件架构</a>》)一书中对Context的翻译)、容器视图(Container Diagram)、组件视图(Component Diagram)和代码视图(Code Diagram)，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-8.png" alt="" /></p>
<p>语境视图是最高层级的视图，用于描述软件系统与外部实体之间的关系和交互。它展示了系统所处的环境和与外部实体（如用户、其他系统、第三方服务等）的关系，以及它们之间的交互方式。</p>
<p>容器视图关注系统内部的软件容器及其之间的关系和交互。容器可以是物理的、虚拟的或逻辑的，它们承载着系统中的组件或服务。容器可以是应用程序、数据库、消息队列、Web服务等。容器视图描述了系统的主要部件，以及它们之间的依赖关系和通信方式。</p>
<p>组件视图进一步展开容器视图中的组件，描述系统内部的组件及其之间的关系和交互。组件视图展示了系统的模块、类、库或其他可重用的软件单元，并显示它们之间的依赖关系、接口和通信方式。</p>
<p>代码视图是最底层的视图，关注具体的代码实现细节。它用于描述系统中的类、函数、方法等代码单元的结构、关系和实现细节。代码视图可以是面向对象的类图、模块图或其他代码组织结构的表示方式，用于帮助开发人员理解和浏览源代码。</p>
<p>下面示意图可以更直观的展示出语境、容器、组件以及代码之间这种逐渐“展开”的层次关系：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-9.png" alt="" /></p>
<p>通过C4模型的这四个层次的视图，架构师可以逐渐深入地描述和表达软件系统的不同层次和组成部分，从整体到细节，帮助团队成员和利益相关者更好地理解和沟通软件架构。</p>
<ul>
<li><a href="https://arc42.org/">Arc42</a> </li>
</ul>
<p>Arc42是一种用于软件架构文档化的模板和方法，它提供了一套规范和指导原则来描述软件系统的架构。下面是Arc42的全景图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-10.png" alt="" /></p>
<p>我们看到：Arc42模板也包含了多个视图，每个视图都关注系统架构的不同方面，包括Context、Building Block View、Runtime View以及Deployment View等。</p>
<p>Context View：描述系统与其外部环境之间的关系和交互，强调边界的概念，分为技术Context与业务Context。</p>
<p>部署视图（Deployment View）描述了系统的部署架构和环境，包括物理设备、服务器、网络拓扑以及协议等信息。</p>
<p>构件视图（Building Block View）描述了系统内部的组件、模块、子系统、包等，并展示它们之间的关系和依赖。构件视图是源码结构的概览。</p>
<p>运行时视图（Runtime View）描述了系统在运行时的行为和交互以及具体场景下对其他构件的运行时依赖。使用序列图、状态图等方式可展示系统的运行时行为。</p>
<h3>2.7 Diagrams As Code</h3>
<p>架构设计不是一成不变的，需要不断演进，因此架构视图也需要“与时俱进”的更新。但直接更新图片格式似乎很不方便，也无法在形式上很好的达成一致，于是一些基于<a href="https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go">DSL</a>语法生成架构设计图(Diagram)的工具便涌现了出来，比如：<a href="http://plantuml.com/">PlantUML</a>、<a href="https://structurizr.com/dsl">Structurizr</a>、<a href="https://mermaid.live/">Mermaid</a>等。有了这些工具，架构师便可以使用文本编辑器来“画图”，支持“所见即所得”。并且由于Diagrams As Code(代码即图)，我们可以将架构设计图与版本控制系统很好地集成。</p>
<p>到这里，我们知道了基于多视角+“Diagrams As Code”是目前的主流的架构设计表达和实践方法，那么我们在软件架构表达实践中，究竟选择哪几个视角来表达呢？这个目前没有统一标准。调研了4+1 Views、C4 model以及Arc42后，我这里说说自己日常做架构表达时使用的最小视图集。</p>
<h2>3. 最小图集</h2>
<p>很多读者可能听说或学习过或实践过<a href="https://book.douban.com/subject/33391219/">金字塔写作</a>，金字塔写作原理是一种用于新闻报道和科技写作的写作方法，它的核心思想是将最重要的信息放在文章的开头，然后逐渐向下展开，提供更多的细节和背景信息。</p>
<p>金字塔写作的优势在于：</p>
<ul>
<li>它可以迅速吸引读者的注意力，让读者在最短时间内了解文章的核心内容；</li>
<li>它还可确保信息传递：将最重要的信息放在开头，可以避免读者在阅读过程中错过关键信息或迷失在细枝末节中，确保信息有效地传达给读者；</li>
<li>它还具备灵活性和可定制性，不要求严格按照一个固定的结构来组织文章，而是提供了一种基本的思路和原则，可以根据具体情况进行调整和定制，以适应不同的写作需求和读者群体。</li>
</ul>
<p>我理解，金字塔写作方法之所以能够成功，其本质是站在了读者的角度去思考问题，想读者之所想，做读者之所需。</p>
<p>软件架构表达的目的也是让开发人员快速深入的理解架构，与设计人员达成共识，指导后续软件系统的实现。所以要想形成有效表达，我们就需要像金字塔写作那样<strong>站在开发人员的角度</strong>来考虑架构表达，借鉴金字塔原理，自上而下，先表达最重要的信息，然后逐渐向下展开，避免开发人员在理解过程中错过关键信息或迷失在细枝末节当中。</p>
<p>综合前面介绍的多种Views的方法，我们觉得软件架构表达的起点，即第一个图必须是语境图(Context Diagram)。</p>
<h3>3.1 语境图(Context Diagram)</h3>
<p>语境图表达的是系统最高的抽象层次，是最高视角，全局视角。通过语境图，可以解决开发人员在内心中提出的下面问题：</p>
<ul>
<li>我们构建的（或已经构建的）软件系统是什么(What)？</li>
<li>谁会用它？</li>
<li>如何融入已有的IT环境？</li>
<li>系统的边界是什么？（业务的，技术的)</li>
</ul>
<p>语境图不会也不应该展示太多细节，它是软件系统设计图的起点。后续的图都是用“放大镜”将我们的系统放大后的细节的表达。当牵涉到理解系统间接口的问题时，语境图还可以为你识别可能需要沟通的人提供了一个起点。</p>
<p>语境图向开发者展现的重点在于软件系统的范围以及与外部的交互行为（用户&lt; &#8211; >系统、系统&lt; &#8211; >系统等等）。下面是使用<a href="https://structurizr.com/dsl">structurizr</a>绘制的一个语境图的实例：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-11.png" alt="" /></p>
<p>语境图中心蓝色的矩形框代表的是我们的软件系统，上方的user、role、actor是我们的软件系统的用户；client是与我们的软件系统交互的系统，是系统到系统交互的一个代表；在我们的软件系统、Inner System1和Inner System2之外有一个虚线框，代表了企业范围；而Inner System1和Inner System2是我们的软件系统在企业内部依赖的系统；同时，我们的软件系统还依赖企业外部的Outer System1和Outer System2。</p>
<p>上述语境图对应的structurizr dsl代码如下：</p>
<pre><code>// system context diagrams

workspace {

    model {
        u = person "User"
        r = person "Role"
        a = person "Actor"
        c = softwareSystem "Client Software System" {
            tags "client"
        }

        enterprise = group "Enterprise A" {
            s = softwareSystem "Our Software System" {
                tags "server"
            }

            d1 = softwareSystem "Inner System1" {
                tags "dep"
            }

            d2 = softwareSystem "Inner System2" {
                tags "dep"
            }
        }
        d3 = softwareSystem "Outer System1" {
            tags "dep"
        }

        d4 = softwareSystem "Outer System2" {
            tags "dep"
        }

        u -&gt; s "Uses"
        r -&gt; s "Uses"
        a -&gt; s "Uses"
        c -&gt; s "Call"
        s -&gt; d1 "Uses"
        s -&gt; d2 "Uses"
        s -&gt; d3 "Uses"
        s -&gt; d4 "Uses"
    }

    views {
        systemContext s {
            include *
            autoLayout
        }

        styles {
            element "server" {
                background #1168bd
                color #ffffff
            }

            element "dep" {
                background #e5e4e2
                color #000000
            }

            element "client" {
                background #e5e4e2
                color #000000
            }

            element "Person" {
                shape person
                background #08427b
                color #ffffff
            }
        }

    }

}
</code></pre>
<p>基于语境图，就好比我们站在万米高空一览Our Software System。不过对于架构设计表达来说，这还不够，现在是时候下降高度让视野进入到系统内部去挖掘一些细节了。</p>
<h3>3.2 容器图(Container Diagram)</h3>
<p>在从万米高空的系统全局视角了解了我们的软件系统是什么后，我们将第一次进入到系统内部。我们现在所处的高度是100米，在这个高度上，可以清晰地看到软件系统的整体形态、内部脉络、技术选择、职责分布以及各个部分之间是如何交流的。我们将每个部分称为一个容器(container)。一个容器通常可以表示一个应用/服务或数据存储，如果你的软件系统采用了微服务架构，那么将每个服务作为一个容器通常是可行的。</p>
<p>针对每个容器，我们可以设置它的属性：名字(如Web App、API网关、关系数据库存储、订阅服务等）、实现技术(如mvc等)以及功能性的描述。在容器间的联系上我们可以附加上通信方式(json over http、gRPC、websocket等)。</p>
<p>下面是上面语境图中的My Software System的容器图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-12.png" alt="" /></p>
<p>在这个容器图中，我们看到了系统支持通过Web app和mobile app访问和使用；系统的入口使用了API网关；系统内部分为业务服务和基础服务，基础服务封装了到关系数据库、对象存储(oss)的接口(关系数据库和oss都是技术选择)；业务服务可以调用企业内部服务，亦可调用企业外部服务，并且明确了调用方式。</p>
<p>下面是生成上述容器图的structurizr的代码：</p>
<pre><code>// container diagrams

workspace {

    model {
        u = person "User"

        enterprise = group "Enterprise A" {
            s = softwareSystem "Our Software System" {
                tags "server"

                mobileApp = container "Mobile App" {
                    tags "container"
                }

                webApp = container "Web App" {
                     tags "container"
                }

                apiGw = container "API Gateway" {
                     tags "container"
                }

                biz1 = container "Business Service 1" {
                     tags "container"
                }

                biz2 = container "Business Service 2" {
                     tags "container"
                }

                biz3 = container "Business Service 3" {
                     tags "container"
                }

                base1 = container "Base Service 1" {
                     tags "container"
                }

                base2 = container "Base Service 2" {
                     tags "container"
                }

                base3 = container "Base Service 3" {
                     tags "container"
                }

                rds = container "Relational Database system" {
                     tags "container"
                }

                oss = container "Object Storage Service" {
                     tags "container"
                }
            }

            d1 = softwareSystem "Inner System1" {
                tags "dep"
            }

            d2 = softwareSystem "Inner System2" {
                tags "dep"
            }
        }

        d3 = softwareSystem "Outer System1" {
            tags "dep"
        }

        d4 = softwareSystem "Outer System2" {
            tags "dep"
        }

        u -&gt; mobileApp "Uses"
        u -&gt; webApp "Uses"
        mobileApp -&gt; apiGw "Makes API calls to" "JSON/HTTPS"
        WEBApp -&gt; apiGw "Makes API calls to" "JSON/HTTPS"
        apiGw -&gt; biz1 "Route API calls to" "gRPC"
        apiGw -&gt; biz2 "Route API calls to" "gRPC"
        apiGw -&gt; biz3 "Route API calls to" "gRPC"
        biz1 -&gt; base1 "Inner API calls to" "gRPC"
        biz1 -&gt; base2 "Inner API calls to" "gRPC"
        biz2 -&gt; base2 "Inner API calls to" "gRPC"
        biz2 -&gt; base3 "Inner API calls to" "gRPC"
        biz3 -&gt; base3 "Inner API calls to" "gRPC"
        base1 -&gt; rds "Reads from and writes to" "Raw SQL"
        base1 -&gt; oss "Reads from and writes to" "HTTPS"
        base2 -&gt; rds "Reads from and writes to" "Raw SQL"
        base3 -&gt; oss "Reads from and writes to" "HTTPS"
        biz1 -&gt; d1 "Make API calls to" "HTTP"
        biz2 -&gt; d3 "Make API calls to" "HTTP"
        biz3 -&gt; d2 "Make API calls to" "HTTP"
        biz3 -&gt; d4 "Make API calls to" "HTTP"
    }

    views {
        container s {
            include *
            autoLayout
        }

        styles {
            element "server" {
                background #1168bd
                color #ffffff
            }

            element "container" {
                background #1168bd
                color #ffffff
            }

            element "dep" {
                background #e5e4e2
                color #000000
            }

            element "Person" {
                shape person
                background #08427b
                color #ffffff
            }
        }

    }

}
</code></pre>
<blockquote>
<p>注：在容器图这个层次上，group关键字没有起作用，导致企业内部服务与外部服务放在一起了。</p>
</blockquote>
<p>按照C4 model的思路，接下来我们会再下降高度，来到10米的高空，进入到某个容器的内部。但容器内部的设计在我看来属于详细设计范畴，如果采用的是微服务架构，那么容器内部的设计就相当于某个服务的设计。所以这里，我并未将这部分作为架构表达的必需之图。</p>
<h3>3.3 序列图(Sequence Diagram)</h3>
<p>无论是语境图，还是容器图，从大类来看，都属于静态的结构图。但做过软件系统设计和研发的童鞋都知道，仅有静态的表达还是不够的，不足以传达软件系统的所有信息，我们还需要对动态行为的表达。这就是为什么我将序列图作为软件表达最小图集一份子的原因。</p>
<p>可能有些人将序列图作为需求分析阶段的产物，其实，序列图既可以在需求阶段产生，也可以在架构设计阶段产生。它在不同阶段有不同的应用和目的。</p>
<p>在需求阶段，序列图被用于描述系统的功能需求和行为。它可以帮助分析和定义系统的用例或用户故事，以及系统与外部实体（如用户、其他系统、服务等）之间的交互过程。通过序列图，需求分析人员和开发团队可以更清晰地理解系统的功能需求，并就用户与系统之间的交互进行沟通和确认。</p>
<p>在架构设计阶段，序列图被用于描述系统的结构和组件之间的交互。在这个阶段，序列图通常用于展示系统的运行时行为、组件之间的消息传递和调用关系。架构师使用序列图来验证系统的设计方案，确保系统的各个组件按预期互相协作，并满足功能和性能要求。</p>
<p>这里的序列图，可以对应前面的Arc42的Runtime View，以及C4 model的<a href="https://c4model.com/#DynamicDiagram">Dynamic Diagram</a>。</p>
<p>序列图也是UML语言中最常被使用的一种Diagram，即便是在UML不那么被提及的今天，我个人也推荐使用<a href="https://www.uml-diagrams.org/sequence-diagrams.html">UML的序列图</a>来表达，而不推荐用structurizr来画了，structurizr在序列图方面的表达能力还是弱了许多。</p>
<p>你可以用你最喜欢的画图工具来绘制UML序列图（比如我经常用的<a href="https://www.drawio.com">drawio</a>），也可以选择<a href="https://plantuml.com/zh/sequence-diagram">plantuml</a>这种基于DSL语法生成序列图的方式来绘制。plantuml对序列图的支持还是非常好的，支持了序列图的大多数元素，可以绘制出非常复杂的图来(下图来自plantuml官网)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-13.png" alt="" /></p>
<p>针对一个复杂的软件系统，我们可能需要针对不同的Container(或更进一步的组件)绘制较多的序列图，至少要覆盖到软件系统各个Container的核心交互流程。</p>
<h3>3.4 部署图(Deployment Diagram)</h3>
<p>无论是C4模型，还是arc42，亦或是UML语言，都包含部署图。在软件架构表达时，准确表达部署设计，对开发人员后续的实现具有很好的指导作用。通过部署图，架构设计人员可以说明静态图中的软件系统和/或容器实例是如何部署到给定部署环境（如生产、暂存、开发等）中的基础设施上的，比如下面这个部署示意图(来自c4model.com)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-14.png" alt="" /></p>
<p>我们看到部署图中的核心角色是部署节点(Node)，它代表了软件系统/容器实例运行的位置；可能是物理基础设施（如物理服务器或设备）、虚拟化基础设施（如IaaS、PaaS、虚拟机）、容器化基础设施（如Docker容器）、执行环境（如数据库服务器、Java EE Web/应用服务器、Microsoft IIS）等，并且部署节点还可以嵌套。此外，右下角的”x N”表示需要多少个部署节点。</p>
<p>通过部署图还可以表达云基础架构的情况(下图来自c4model.com)，可以包含DNS、负载均衡器以及防火墙等部署的基础设施的节点：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-15.png" alt="" /></p>
<p>structurizr对于部署图支持的还不错，还可以像上图那样使用不同公有云提供商特色的Theme来绘制部署图。</p>
<p>到这里，我们已经“凑齐”了表达软件系统架构的最小图集：语境图、容器图、序列图和部署图。我们要学会灵活使用这些图。在软件系统十分复杂的情况下，我们可以将语境图分为<a href="https://c4model.com/#SystemLandscapeDiagram">System Landscape diagram</a>和多个sub system的语境图，之后以此类推，对于每个sub system做容器图等。</p>
<h2>4. 最小图集之外的图(可选)</h2>
<p>有些公司或组织会将架构设计阶段延伸到container内部，这样对软件系统架构的表达就要延伸到详细设计，甚至是编码阶段时，我们就要考虑下面两个类型的Diagram了：组件图和代码图。</p>
<h3>4.1 组件图(Component Diagram)</h3>
<p>如果容器图阶段，你所在的高度是100米，那么组件图阶段，你将位于高度为10米的空中，这足以让你看清容器中每个组件(Component)的细节。</p>
<p>组件图就是容器内部的设计，它涉及到容器内部各个逻辑组件的结构与组件间的交互。在这个层次，你可以使用你擅长的面向对象设计方法，或者面向契约/接口的设计模式，你也可以使用一些成熟的企业应用设计模式，比如MVC等。</p>
<p>下面是一张组件图示例(来自c4model.com)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-16.png" alt="" /></p>
<p>我们看到中间的部分就是API Application这个容器内部的逻辑组件结构与交互情况。有些时候在组件图这一层面，我们甚至可以对照初对应项目中的代码布局结构。</p>
<p>对于组件图中关键组件间的复杂交互流程，可辅以序列图的方式来表达。</p>
<p>此外，组件图可以使用structurizr绘制，语法和语境图、容器图十分相似。</p>
<h3>4.2 代码图(Code Diagram)</h3>
<p>再下降，我们来到离地面1米的高度，我们几乎要躬身入局，参与编码了。通常架构设计不会到达这个阶段，架构师们在100米或10米高度完成任务后，就可以去休息了。</p>
<p>但如果包含这个阶段，我们要给出的便是代码图(Code Diagram)，再直白些，就是UML类图、E-R关系图等，下面是一个示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-17.png" alt="" /></p>
<p>这是一个直面开发人员的图，你可以看到编程语言中的那些机制：接口、继承、实现等等，开发人员甚至可以通过工具将这样的uml class图直接转换为项目的骨架代码。</p>
<h2>4. 小结</h2>
<p>本文首先介绍了为什么软件架构需要有效表达，以便开发者更好地理解架构设计。然后回顾了软件架构表达方式的演进历史，从自然语言描述到图形化表达，再到结构化图形表达、UML、形式化表达，最终发展到现在的多视角表达方式。</p>
<p>文章结合笔者实践经验，借鉴多个多视角软件架构模型，提出了最小图集的概念，笔者认为有效表达软件架构最关键的视角有四个，分别是:</p>
<ol>
<li>语境图：描述系统的整体位置和边界 </li>
<li>容器图：展示系统内部的容器及其关系</li>
<li>序列图：呈现容器内组件以及组件之间的交互行为</li>
<li>部署图：阐明系统在实际环境中的部署情况</li>
</ol>
<p>此外，我认为还可根据需要补充组件图和代码图等更细节的视图。这套最小图集能较全面地表达软件系统的静态结构和动态行为，帮助开发者理解架构设计。</p>
<p>总的来说，该文章从工程实践的视角出发，提出了一套行之有效的软件架构表达方法，对于架构设计的团队沟通及实现具有很好的指导意义。</p>
<p>btw，在容器图或组件图设计阶段，如果要完善工程设计，还可以结合具体的接口文档予以表达，比如基于Swagger的API设计文档等。</p>
<h2>5. 参考资料</h2>
<ul>
<li>《<a href="https://book.douban.com/subject/26248182/">Software Architecture for Developers</a>》- https://book.douban.com/subject/26248182/</li>
<li><a href="https://c4model.com">The C4 model for visualising software architecture</a> &#8211; https://c4model.com</li>
<li><a href="https://www.omg.org/spec/UML">Unified Modeling Language Specification</a> &#8211; https://www.omg.org/spec/UML</li>
<li><a href="https://www.uml-diagrams.org">The Unified Modeling Language</a> &#8211; https://www.uml-diagrams.org</li>
<li><a href="https://arquisoft.github.io/slides/course2021/EN.ASW.TE03_Documentation.pdf">Communicating Software Architecture</a> &#8211; https://arquisoft.github.io/slides/course2021/EN.ASW.TE03_Documentation.pdf</li>
<li><a href="https://arc42.org">arc42</a> &#8211; https://arc42.org</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/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用go test框架驱动的自动化测试</title>
		<link>https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/</link>
		<comments>https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/#comments</comments>
		<pubDate>Thu, 30 Mar 2023 13:42:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[acceptance-test]]></category>
		<category><![CDATA[awk]]></category>
		<category><![CDATA[broker]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-test-report]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[mosquitto]]></category>
		<category><![CDATA[mqtt]]></category>
		<category><![CDATA[Parallel]]></category>
		<category><![CDATA[publish]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Setup]]></category>
		<category><![CDATA[smoke-test]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[subscribe]]></category>
		<category><![CDATA[subtest]]></category>
		<category><![CDATA[teardown]]></category>
		<category><![CDATA[testcase]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TestMain]]></category>
		<category><![CDATA[TOML]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[yaml]]></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=3841</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test 一. 背景 团队的测试人员稀缺，无奈只能“自己动手，丰衣足食”，针对我们开发的系统进行自动化测试，这样既节省的人力，又提高了效率，还增强了对系统质量保证的信心。 我们的目标是让自动化测试覆盖三个环境，如下图所示： 我们看到这三个环境分别是： CI/CD流水线上的自动化测试 发版后在各个stage环境中的自动化冒烟/验收测试 发版后在生产环境的自动化冒烟/验收测试 我们会建立统一的用例库或针对不同环境建立不同用例库，但这些都不重要，重要的是我们用什么语言来编写这些用例、用什么工具来驱动这些用例。 下面看看方案的诞生过程。 二. 方案 最初组内童鞋使用了YAML文件来描述测试用例，并用Go编写了一个独立的工具读取这些用例并执行。这个工具运作起来也很正常。但这样的方案存在一些问题： 编写复杂 编写一个最简单的connect连接成功的用例，我们要配置近80行yaml。一个稍微复杂的测试场景，则要150行左右的配置。 难于扩展 由于最初的YAML结构设计不足，缺少了扩展性，使得扩展用例时，只能重新建立一个用例文件。 表达能力不足 我们的系统是消息网关，有些用例会依赖一定的时序，但基于YAML编写的用例无法清晰地表达出这种用例。 可维护性差 如果换一个人来编写新用例或维护用例，这个人不仅要看明白一个个百十来行的用例描述，还要翻看一下驱动执行用例的工具，看看其执行逻辑。很难快速cover这个工具。 为此我们想重新设计一个工具，测试开发人员可以利用该工具支持的外部DSL文法来编写用例，然后该工具读取这些用例并执行。 注：根据Martin Fowler的《领域特定语言》一书对DSL的分类，DSL有三种选型：通用配置文件(xml, json, yaml, toml)、自定义领域语言，这两个合起来称为外部DSL。如：正则表达式、awk, sql、xml等。利用通用编程语言片段/子集作为DSL则称为内部dsl，像ruby等。 后来基于待测试的场景数量和用例复杂度粗略评估了一下DSL文法(甚至借助ChatGPT生成过几版DSL文法)，发现这个“小语言”那也是“麻雀虽小五脏俱全”。如果用这样的DSL编写用例，和利用通用语言(比如Python)编写的用例在代码量级上估计也不相上下了。 既然如此，自己设计外部DSL意义也就不大了。还不如用Python来整。但转念一想，既然用通用语言的子集了，团队成员对Python又不甚熟悉，那为啥不回到Go呢^_^。 让我们进行一个大胆的设定：将Go testing框架作为“内部DSL”来编写用例，用go test命令作为执行这些用例的测试驱动工具。此外，有了GPT-4加持，生成TestXxx、补充用例啥的应该也不是大问题。 下面我们来看看如何组织和编写用例并使用go test驱动进行自动化测试。 三. 实现 1. 测试用例组织 我的《Go语言精进之路vol2》书中的第41条“有层次地组织测试代码”中对基于go test的测试用例组织做过系统的论述。结合Go test提供的TestMain、TestXxx与sub test，我们完全可以基于go test建立起一个层次清晰的测试用例结构。这里就以一个对开源mqtt broker的自动化测试为例来说明一下。 注：你可以在本地搭建一个单机版的开源mqtt broker服务作为被测对象，比如使用Eclipse的mosquitto。 在组织用例之前，我先问了一下ChatGPT对一个mqtt broker测试都应该包含哪些方面的用例，ChatGPT给了我一个简单的表： 如果你对MQTT协议有所了解，那么你应该觉得ChatGPT给出的答案还是很不错的。 这里我们就以connection、subscribe和publish三个场景(scenario)来组织用例： $tree [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test">本文永久链接</a> &#8211; https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test</p>
<h2>一. 背景</h2>
<p>团队的测试人员稀缺，无奈只能“自己动手，丰衣足食”，针对我们开发的系统进行自动化测试，这样<strong>既节省的人力，又提高了效率，还增强了对系统质量保证的信心</strong>。</p>
<p>我们的目标是让自动化测试覆盖三个环境，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-2.png" alt="" /></p>
<p>我们看到这三个环境分别是：</p>
<ul>
<li>CI/CD流水线上的自动化测试</li>
<li>发版后在各个stage环境中的自动化冒烟/<a href="http://en.wikipedia.org/wiki/Acceptance_testing">验收测试</a></li>
<li>发版后在生产环境的自动化冒烟/验收测试</li>
</ul>
<p>我们会建立统一的用例库或针对不同环境建立不同用例库，但这些都不重要，重要的是我们<strong>用什么语言来编写这些用例、用什么工具来驱动这些用例</strong>。</p>
<p>下面看看方案的诞生过程。</p>
<h2>二. 方案</h2>
<p>最初组内童鞋使用了<a href="https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/">YAML文件</a>来描述测试用例，并用Go编写了一个独立的工具读取这些用例并执行。这个工具运作起来也很正常。但这样的方案存在一些问题：</p>
<ul>
<li>编写复杂</li>
</ul>
<p>编写一个最简单的connect连接成功的用例，我们要配置近80行yaml。一个稍微复杂的测试场景，则要150行左右的配置。</p>
<ul>
<li>难于扩展</li>
</ul>
<p>由于最初的YAML结构设计不足，缺少了扩展性，使得扩展用例时，只能重新建立一个用例文件。</p>
<ul>
<li>表达能力不足</li>
</ul>
<p>我们的系统是消息网关，有些用例会依赖一定的时序，但基于YAML编写的用例无法清晰地表达出这种用例。</p>
<ul>
<li>可维护性差</li>
</ul>
<p>如果换一个人来编写新用例或维护用例，这个人不仅要看明白一个个百十来行的用例描述，还要翻看一下驱动执行用例的工具，看看其执行逻辑。很难快速cover这个工具。</p>
<p>为此我们想重新设计一个工具，测试开发人员可以利用该工具支持的<a href="https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go">外部DSL文法</a>来编写用例，然后该工具读取这些用例并执行。</p>
<blockquote>
<p>注：根据Martin Fowler的<a href="https://book.douban.com/subject/21964984/">《领域特定语言》</a>一书对DSL的分类，DSL有三种选型：通用配置文件(xml, json, yaml, toml)、自定义领域语言，这两个合起来称为外部DSL。如：正则表达式、awk, sql、xml等。利用通用编程语言片段/子集作为DSL则称为内部dsl，像ruby等。</p>
</blockquote>
<p>后来基于待测试的场景数量和用例复杂度粗略评估了一下DSL文法(甚至借助ChatGPT生成过几版DSL文法)，发现这个“小语言”那也是“麻雀虽小五脏俱全”。如果用这样的DSL编写用例，和利用通用语言(比如Python)编写的用例在代码量级上估计也不相上下了。</p>
<p>既然如此，自己设计外部DSL意义也就不大了。还不如用Python来整。但转念一想，既然用通用语言的子集了，团队成员对Python又不甚熟悉，那为啥不回到Go呢^_^。</p>
<p>让我们进行一个大胆的设定：将Go testing框架作为“内部DSL”来编写用例，用go test命令作为执行这些用例的测试驱动工具。此外，有了GPT-4加持，生成TestXxx、补充用例啥的应该也不是大问题。</p>
<p>下面我们来看看如何组织和编写用例并使用go test驱动进行自动化测试。</p>
<h2>三. 实现</h2>
<h3>1. 测试用例组织</h3>
<p>我的<a href="https://item.jd.com/13694000.html">《Go语言精进之路vol2》</a>书中的<a href="https://book.douban.com/subject/35720729/">第41条“有层次地组织测试代码”</a>中对基于go test的测试用例组织做过系统的论述。结合Go test提供的<a href="https://pkg.go.dev/testing#Main">TestMain</a>、TestXxx与<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest/">sub test</a>，我们完全可以基于go test建立起一个层次清晰的测试用例结构。这里就以一个对开源mqtt broker的自动化测试为例来说明一下。</p>
<blockquote>
<p>注：你可以在本地搭建一个单机版的开源mqtt broker服务作为被测对象，比如使用<a href="https://github.com/eclipse/mosquitto">Eclipse的mosquitto</a>。</p>
</blockquote>
<p>在组织用例之前，我先问了一下ChatGPT对一个mqtt broker测试都应该包含哪些方面的用例，ChatGPT给了我一个简单的表：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-3.png" alt="" /></p>
<p>如果你对<a href="https://mqtt.org/mqtt-specification/">MQTT协议</a>有所了解，那么你应该觉得ChatGPT给出的答案还是很不错的。</p>
<p>这里我们就以connection、subscribe和publish三个场景(scenario)来组织用例：</p>
<pre><code>$tree -F .
.
├── Makefile
├── go.mod
├── go.sum
├── scenarios/
│   ├── connection/              // 场景：connection
│   │   ├── connect_test.go      // test suites
│   │   └── scenario_test.go
│   ├── publish/                 // 场景：publish
│   │   ├── publish_test.go      // test suites
│   │   └── scenario_test.go
│   ├── scenarios.go             // 场景中测试所需的一些公共函数
│   └── subscribe/               // 场景：subscribe
│       ├── scenario_test.go
│       └── subscribe_test.go    // test suites
└── test_report.html             // 生成的默认测试报告
</code></pre>
<p>简单说明一下这个测试用例组织布局：</p>
<ul>
<li>我们将测试用例分为多个场景(scenario)，这里包括connection、subscribe和publish；</li>
<li>由于是由go test驱动，所以每个存放test源文件的目录中都要遵循Go对Test的要求，比如：源文件以&#95;test.go结尾等。</li>
<li>每个场景目录下存放着测试用例文件，一个场景可以有多个&#95;test.go文件。这里设定&#95;test.go文件中的每个TestXxx为一个test suite，而TestXxx中再基于subtest编写用例，这里每个subtest case为一个最小的test case；</li>
<li>每个场景目录下的scenario_test.go，都是这个目录下包的TestMain入口，主要是考虑为所有包传入统一的命令行标志与参数值，同时你也针对该场景设置在TestMain中设置setup和teardown。该文件的典型代码如下：</li>
</ul>
<pre><code>// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/scenario_test.go

package subscribe

import (
    "flag"
    "log"
    "os"
    "testing"

    mqtt "github.com/eclipse/paho.mqtt.golang"
)

var addr string

func init() {
    flag.StringVar(&amp;addr, "addr", "", "the broker address(ip:port)")
}

func TestMain(m *testing.M) {
    flag.Parse()

    // setup for this scenario
    mqtt.ERROR = log.New(os.Stdout, "[ERROR] ", 0)

    // run this scenario test
    r := m.Run()

    // teardown for this scenario
    // tbd if teardown is needed

    os.Exit(r)
}
</code></pre>
<p>接下来我们再来看看具体测试case的实现。</p>
<h3>2. 测试用例实现</h3>
<p>我们以稍复杂一些的subscribe场景的测试为例，我们看一下subscribe目录下的subscribe_test.go中的测试suite和cases：</p>
<pre><code>// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/subscribe_test.go

package subscribe

import (
    scenarios "bigwhite/autotester/scenarios"
    "testing"
)

func Test_Subscribe_S0001_SubscribeOK(t *testing.T) {
    t.Parallel() // indicate the case can be ran in parallel mode

    tests := []struct {
        name  string
        topic string
        qos   byte
    }{
        {
            name:  "Case_001: Subscribe with QoS 0",
            topic: "a/b/c",
            qos:   0,
        },
        {
            name:  "Case_002: Subscribe with QoS 1",
            topic: "a/b/c",
            qos:   1,
        },
        {
            name:  "Case_003: Subscribe with QoS 2",
            topic: "a/b/c",
            qos:   2,
        },
    }

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // indicate the case can be ran in parallel mode
            client, testCaseTeardown, err := scenarios.TestCaseSetup(addr, nil)
            if err != nil {
                t.Errorf("want ok, got %v", err)
                return
            }
            defer testCaseTeardown()

            token := client.Subscribe(tt.topic, tt.qos, nil)
            token.Wait()

            // Check if subscription was successful
            if token.Error() != nil {
                t.Errorf("want ok, got %v", token.Error())
            }

            token = client.Unsubscribe(tt.topic)
            token.Wait()
            if token.Error() != nil {
                t.Errorf("want ok, got %v", token.Error())
            }
        })
    }
}

func Test_Subscribe_S0002_SubscribeFail(t *testing.T) {
}
</code></pre>
<p>这个测试文件中的测试用例与我们日常编写单测并没有什么区别！有一些需要注意的地方是：</p>
<ul>
<li>Test函数命名</li>
</ul>
<p>这里使用了Test_Subscribe_S0001_SubscribeOK、Test_Subscribe_S0002_SubscribeFail命名两个Test suite。命名格式为：</p>
<pre><code>Test_场景_suite编号_测试内容缩略
</code></pre>
<p>之所以这么命名，一来是测试用例组织的需要，二来也是为了后续在生成的Test report中区分不同用例使用。</p>
<ul>
<li>testcase通过subtest呈现</li>
</ul>
<p>每个TestXxx是一个test suite，而基于表驱动的每个sub test则对应一个test case。</p>
<ul>
<li>test suite和test case都可单独标识为是否可并行执行</li>
</ul>
<p>通过testing.T的Parallel方法可以标识某个TestXxx或test case(subtest)是否是可以并行执行的。</p>
<ul>
<li>针对每个test case，我们都调用setup和teardown</li>
</ul>
<p>这样可以保证test case间都相互独立，互不影响。</p>
<h3>3. 测试执行与报告生成</h3>
<p>设计完布局，编写完用例后，接下来就是执行这些用例。那么怎么执行这些用例呢？</p>
<p>前面说过，我们的方案是基于go test驱动的，我们的执行也要使用go test。</p>
<p>在顶层目录automated-testing下，执行如下命令：</p>
<pre><code>$go test ./... -addr localhost:30083
</code></pre>
<p>go test会遍历执行automated-testing下面每个包的测试，在执行每个包的测试时会将-addr这个flag传入。如果localhost:30083端口并没有mqtt broker服务监听，那么上面的命令将输出如下信息：</p>
<pre><code>$go test ./... -addr localhost:30083
?       bigwhite/autotester/scenarios   [no test files]
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
--- FAIL: Test_Connection_S0001_ConnectOKWithoutAuth (0.00s)
    connect_test.go:20: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL    bigwhite/autotester/scenarios/connection    0.015s
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
--- FAIL: Test_Publish_S0001_PublishOK (0.00s)
    publish_test.go:11: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL    bigwhite/autotester/scenarios/publish   0.016s
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
[ERROR] [client]   Failed to connect to a broker
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
--- FAIL: Test_Subscribe_S0001_SubscribeOK (0.00s)
    --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_002:_Subscribe_with_QoS_1 (0.00s)
        subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
    --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_003:_Subscribe_with_QoS_2 (0.00s)
        subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
    --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_001:_Subscribe_with_QoS_0 (0.00s)
        subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL    bigwhite/autotester/scenarios/subscribe 0.016s
FAIL
</code></pre>
<p>这也是一种测试失败的情况。</p>
<p>在自动化测试时，我们一般会把错误或成功的信息保存到一个测试报告文件(多是html)中，那么我们如何基于上面的测试结果内容生成我们的测试报告文件呢？</p>
<p>首先go test支持将输出结果以结构化的形式展现，即传入-json这个flag。这样我们仅需基于这些json输出将各个字段读出并写入html中即可。好在有现成的开源工具可以做到这点，那就是<a href="https://github.com/vakenbolt/go-test-report">go-test-report</a>。下面是通过命令行管道让go test与go-test-report配合工作生成测试报告的命令行：</p>
<blockquote>
<p>注：go-test-report工具的安装方法：go install github.com/vakenbolt/go-test-report@latest</p>
</blockquote>
<pre><code>$go test ./... -addr localhost:30083 -json|go-test-report
[go-test-report] finished in 1.375540542s
</code></pre>
<p>执行结束后，就会在当前目录下生成一个test_report.html文件，使用浏览器打开该文件就能看到测试执行结果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-4.png" alt="" /></p>
<p>通过测试报告的输出，我们可以很清楚看到哪些用例通过，哪些用例失败了。并且通过Test suite的名字或Test case的名字可以快速定位是哪个scenario下的哪个suite的哪个case报的错误！我们也可以点击某个test suite的名字，比如：Test_Connection_S0001_ConnectOKWithoutAuth，打开错误详情查看错误对应的源文件与具体的行号：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-5.png" alt="" /></p>
<p>为了方便快速敲入上述命令，我们可以将其放入Makefile中方便输入执行，即在顶层目录下，执行make即可执行测试：</p>
<pre><code>$make
go test ./... -addr localhost:30083 -json|go-test-report
[go-test-report] finished in 2.011443636s
</code></pre>
<p>如果要传入自定义的mqtt broker的服务地址，可以用：</p>
<pre><code>$make broker_addr=192.168.10.10:10083
</code></pre>
<h2>四. 小结</h2>
<p>在这篇文章中，我们介绍了如何实现基于go test驱动的自动化测试，介绍了这样的测试的结构布局、用例编写方法、执行与报告生成等。</p>
<p>这个方案的不足是<strong>要求测试用例所在环境需要部署go与go-test-report</strong>。</p>
<p>go test支持将test编译为一个可执行文件，不过不支持将多个包的测试编译为一个可执行文件：</p>
<pre><code>$go test -c ./...
cannot use -c flag with multiple packages
</code></pre>
<p>此外由于go test编译出的可执行文件<a href="https://github.com/golang/go/issues/22996">不支持将输出内容转换为JSON格式</a>，因此也无法对接go-test-report将测试结果保存在文件中供后续查看。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/automated-testing">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/automated-testing</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>2022年博客回顾与总结</title>
		<link>https://tonybai.com/2023/01/11/2022-blog-summary/</link>
		<comments>https://tonybai.com/2023/01/11/2022-blog-summary/#comments</comments>
		<pubDate>Tue, 10 Jan 2023 23:04:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go1.19]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopherchina]]></category>
		<category><![CDATA[Gopher部落]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[operator]]></category>
		<category><![CDATA[时序数据库]]></category>
		<category><![CDATA[智能网联]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[知识星球]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3779</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/01/11/2022-blog-summary 2022年对我来说，也是十分忙碌和充实的一年。尽管和2021年相比，成果物没那么多^_^。 伴随着二宝的长大，我发现自己的闲暇时间被进一步“压缩”，还好大宝上初中后领悟到了自驱学习的重要性和相关方法后，她的学习现在基本不需要我过问了。 2022年初，《Go语言精进之路：从新手到高手的编程思想、方法和技巧》系列1、2册上市后取得了不错的口碑，纸版书售卖量也还不错，在年中的时候都进入二印了，这也让我有机会修复一些勘误表中的问题，让读者拿到内容更准确的的版本。年中，我还借助谢大组织的“GoCN社区的Go读书会”分享了《Go语言精进之路》这本书的写作历程、内容导读以及个人的一些读书方法和经验。 我在极客时间的《Go语言第一课》专栏由于口碑相传，得到了很多Gopher的关注和学习，我也很积极的回答学员们的各种问题。目前该专栏大约排在极客时间周学习排行榜15名左右，不过还进不了首页推荐，和那些常驻首页的大V课程还没法比^_^。 2022年应谢大之邀，原本计划在GopherChina 2022之前的培训环节做一期《Go高级工程师训练营》培训的，但GopherChina因为疫情原因两次推迟，最终线下大会被取消，没能成行。期望在2023年能把这个培训补上。 2022年是我进入智能网联汽车这个赛道的第二个年头，精力主要花在研发几个重要的核心产品上，包括：车云通信产品、车端的基于DSL的数据处理产品、云端的流数据处理和时序数据存储产品等，跨度很大，难度很大。目前车云通信产品已经部署在我们主要客户的生产环境，为2023车型提供车云通道能力，另外我们也在与国内一些大主机厂做POC测试。时序数据存储产品进入关键的设计阶段，这块经验的确不足，需要投入较大精力学习、思考和尝试，2023年我将投入较大精力在这个产品上。 在博客写作方面，我仍然保持高昂的热情，粗略统计了一下，今年写了有80余篇。和2021年一样，这里我也根据阅读数量选出了2022年本博客最受欢迎的若干篇文章(排名不分先后)： 《2022年Go语言盘点：泛型落地，无趣很好，稳定为王》 《Go编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策》 《评点2021-2022年上市的那些Go语言新书》 《这可能是最权威、最全面的Go语言编码风格规范了！》 《Go 1.20新特性前瞻》 《Go为什么能成功》 《通过实例理解Go标准库context包》 《slog：Go官方版结构化日志包》 《Go语言之道[译]》 《Go 1.19中值得关注的几个变化》 《使用Go开发Kubernetes Operator：基本结构》 《使用Go语言开发eBPF程序》 《使用C语言从头开发一个Hello World级别的eBPF程序》 《GoCN社区Go读书会第二期：《Go语言精进之路》》 《使用Go基于国密算法实现双向认证》 《小厂内部私有Go module拉取方案（续）》 《Go程序员拥抱C语言简明指南》 《使用ANTLR和Go实现DSL入门》 《Go 1.18中值得关注的几个变化》 《Go社区主流Kafka客户端简要对比》 《为什么有了Go module后“依赖地狱”问题依然存在》 《聊聊Go应用输出日志的工程实践》 复盘了一下2022年的博客文章，感觉目前的博客选题更多是“工作和学习中遇到的问题”来驱动的，还缺少系统化的规划和组织，2023年在这方面需要加强。 最后展望一下2023年！ 在纸版书写作方面，2023年是否开启《Go语言精进之路》第三册、 是否扩写《Go语言第一课》专栏，然后整理成纸版书出版等都是可尝试的但未确定的事情。 专栏方面也有一些不成熟的选题想法： 如何写出地道的Go代码 深入理解Go核心技术 Go高级工程师必知必会(在培训的基础上扩充) Gopher部落知识星球也将进入运营的第三个年头，2022年尝试做出了一些改变，效果还不错。2023年争取再聚焦一下：围绕少而精的几个主题做高质量分享。 2022年最大的“损失”是微博账号被冻结了，目前只能暂时用小号。之前的账号啥时候恢复也不清楚，也可能会永远失去那个账号。失去了微博这个重要的私域流量，损失还是蛮大的:( 2023年，由于时序数据存储这个产品的原因，可能会真正开始学习Rust语言，虽然之前了解过多次，但都是从入门到放弃了。这回有了产品和项目驱动，希望不会“重蹈覆辙”^_^。 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！ 著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2021-blog-summary-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/01/11/2022-blog-summary">本文永久链接</a> &#8211; https://tonybai.com/2023/01/11/2022-blog-summary</p>
<p>2022年对我来说，也是<strong>十分忙碌和充实的一年</strong>。尽管和<a href="https://tonybai.com/2021/12/31/2021-blog-summary">2021年</a>相比，成果物没那么多^_^。</p>
<p>伴随着<a href="https://daughter2.tonybai.com">二宝</a>的长大，我发现自己的闲暇时间被进一步“压缩”，还好<a href="https://daughter.tonybai.com">大宝</a>上初中后领悟到了自驱学习的重要性和相关方法后，她的学习现在基本不需要我过问了。</p>
<p>2022年初，<a href="https://book.douban.com/subject/35720728/">《Go语言精进之路：从新手到高手的编程思想、方法和技巧》</a>系列1、<a href="https://book.douban.com/subject/35720729/">2册</a>上市后取得了不错的口碑，纸版书售卖量也还不错，在年中的时候都进入二印了，这也让我有机会修复一些<a href="https://github.com/bigwhite/GoProgrammingFromBeginnerToMaster/blob/main/errata.md">勘误表</a>中的问题，让读者拿到内容更准确的的版本。年中，我还借助谢大组织的“GoCN社区的Go读书会”分享了<a href="https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master">《Go语言精进之路》这本书的写作历程</a>、内容导读以及个人的一些读书方法和经验。</p>
<p>我在极客时间的<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏由于口碑相传，得到了很多Gopher的关注和学习，我也<a href="https://tonybai.com/go-course-faq">很积极的回答学员们的各种问题</a>。目前该专栏大约排在极客时间周学习排行榜15名左右，不过还进不了首页推荐，和那些常驻首页的大V课程还没法比^_^。</p>
<p>2022年应谢大之邀，原本计划在GopherChina 2022之前的培训环节做一期《Go高级工程师训练营》培训的，但GopherChina因为疫情原因两次推迟，最终线下大会被取消，没能成行。期望在2023年能把这个培训补上。</p>
<p>2022年是我进入智能网联汽车这个赛道的第二个年头，精力主要花在研发几个重要的核心产品上，包括：车云通信产品、车端的基于DSL的数据处理产品、云端的流数据处理和时序数据存储产品等，跨度很大，难度很大。目前车云通信产品已经部署在我们主要客户的生产环境，为2023车型提供车云通道能力，另外我们也在与国内一些大主机厂做POC测试。时序数据存储产品进入关键的设计阶段，这块经验的确不足，需要投入较大精力学习、思考和尝试，2023年我将投入较大精力在这个产品上。</p>
<p>在博客写作方面，我仍然保持高昂的热情，粗略统计了一下，今年写了有80余篇。和2021年一样，这里我也根据阅读数量选出了2022年本博客最受欢迎的若干篇文章(排名不分先后)：</p>
<ul>
<li>《<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">2022年Go语言盘点：泛型落地，无趣很好，稳定为王</a>》</li>
<li>《<a href="https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment">Go编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策</a>》</li>
<li>《<a href="https://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022">评点2021-2022年上市的那些Go语言新书</a>》</li>
<li>《<a href="https://tonybai.com/2022/11/26/intro-of-google-go-style">这可能是最权威、最全面的Go语言编码风格规范了！</a>》</li>
<li>《<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">Go 1.20新特性前瞻</a>》</li>
<li>《<a href="https://tonybai.com/2022/12/07/why-go-succeed/">Go为什么能成功</a>》</li>
<li>《<a href="https://tonybai.com/2022/11/08/understand-go-context-by-example">通过实例理解Go标准库context包</a>》</li>
<li>《<a href="https://tonybai.com/2022/10/30/first-exploration-of-slog">slog：Go官方版结构化日志包</a>》</li>
<li>《<a href="https://tonybai.com/2022/09/25/the-tao-of-go">Go语言之道[译]</a>》</li>
<li>《<a href="https://tonybai.com/2022/08/22/some-changes-in-go-1-19">Go 1.19中值得关注的几个变化</a>》</li>
<li>《<a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">使用Go开发Kubernetes Operator：基本结构</a>》</li>
<li>《<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/">使用Go语言开发eBPF程序</a>》</li>
<li>《<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">使用C语言从头开发一个Hello World级别的eBPF程序</a>》</li>
<li>《<a href="https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master">GoCN社区Go读书会第二期：《Go语言精进之路》</a>》</li>
<li>《<a href="https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm">使用Go基于国密算法实现双向认证</a>》</li>
<li>《<a href="https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2">小厂内部私有Go module拉取方案（续）</a>》</li>
<li>《<a href="https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher">Go程序员拥抱C语言简明指南</a>》</li>
<li>《<a href="https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go">使用ANTLR和Go实现DSL入门</a>》</li>
<li>《<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18中值得关注的几个变化</a>》</li>
<li>《<a href="https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients">Go社区主流Kafka客户端简要对比</a>》</li>
<li>《<a href="https://tonybai.com/2022/03/12/dependency-hell-in-go/">为什么有了Go module后“依赖地狱”问题依然存在</a>》</li>
<li>《<a href="https://tonybai.com/2022/03/05/go-logging-practice">聊聊Go应用输出日志的工程实践</a>》</li>
</ul>
<p>复盘了一下2022年的博客文章，感觉目前的博客选题更多是“工作和学习中遇到的问题”来驱动的，还缺少系统化的规划和组织，2023年在这方面需要加强。</p>
<p>最后展望一下2023年！</p>
<p>在纸版书写作方面，2023年是否开启《Go语言精进之路》第三册、 是否扩写《Go语言第一课》专栏，然后整理成纸版书出版等都是可尝试的但未确定的事情。</p>
<p>专栏方面也有一些不成熟的选题想法：</p>
<ul>
<li>如何写出地道的Go代码</li>
<li>深入理解Go核心技术</li>
<li>Go高级工程师必知必会(在培训的基础上扩充)</li>
</ul>
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">Gopher部落知识星球</a>也将进入运营的第三个年头，<a href="https://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe">2022年尝试做出了一些改变</a>，效果还不错。2023年争取再聚焦一下：围绕少而精的几个主题做高质量分享。</p>
<p>2022年最大的“损失”是<a href="https://weibo.com/bigwhite20xx">微博账号</a>被冻结了，目前只能暂时用<a href="https://weibo.com/u/6484441286">小号</a>。之前的账号啥时候恢复也不清楚，也可能会永远失去那个账号。失去了微博这个重要的私域流量，损失还是蛮大的:(</p>
<p>2023年，由于时序数据存储这个产品的原因，可能会真正开始学习Rust语言，虽然之前了解过多次，但都是从入门到放弃了。这回有了产品和项目驱动，希望不会“重蹈覆辙”^_^。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/01/11/2022-blog-summary/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>手把手教你使用ANTLR和Go实现一门DSL语言（第五部分）：错误处理</title>
		<link>https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5/</link>
		<comments>https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5/#comments</comments>
		<pubDate>Sun, 29 May 2022 22:31:51 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANTLR]]></category>
		<category><![CDATA[ast]]></category>
		<category><![CDATA[binary-tree]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[Evaluate]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Listener]]></category>
		<category><![CDATA[literal]]></category>
		<category><![CDATA[MartinFowler]]></category>
		<category><![CDATA[node]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[parser]]></category>
		<category><![CDATA[pop]]></category>
		<category><![CDATA[push]]></category>
		<category><![CDATA[ReversePolishExpr]]></category>
		<category><![CDATA[semantic-model]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[top]]></category>
		<category><![CDATA[variable]]></category>
		<category><![CDATA[中序遍历]]></category>
		<category><![CDATA[二叉树]]></category>
		<category><![CDATA[产生式规则]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[前序遍历]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[压栈]]></category>
		<category><![CDATA[叶子节点]]></category>
		<category><![CDATA[后天]]></category>
		<category><![CDATA[后序遍历]]></category>
		<category><![CDATA[弹栈]]></category>
		<category><![CDATA[操作符]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[栈]]></category>
		<category><![CDATA[求值]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[表达式树]]></category>
		<category><![CDATA[表驱动]]></category>
		<category><![CDATA[解析器]]></category>
		<category><![CDATA[语义模型]]></category>
		<category><![CDATA[语法树]]></category>
		<category><![CDATA[运算符]]></category>
		<category><![CDATA[逆波兰表达式]]></category>
		<category><![CDATA[递归]]></category>
		<category><![CDATA[遍历]]></category>
		<category><![CDATA[错误处理]]></category>
		<category><![CDATA[领域特定语言]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3570</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5 无论是端应用还是云应用，要上生产环境，有一件事必须要做好，那就是错误处理。在本系列前面的文章中，我们设计了文法与语法、建立并验证了语义模型，但我们没有特别关注错误处理。在这一篇中，我们就来补上这个环节。 DSL设计与实现过程有以下几个主要环节，在不同环节，我们关注的错误处理的主要对象是不同的。如下图所示： 在文法设计与验证环节，我们更多关注文法设计的正确性。错误的文法会导致解析法示例时失败，但这个环节是在生产Parser代码之前，我们更多是通过ANTLR提供的调试工具对文法的正确性进行调试，无需自己写代码做错误处理。 在语法解析与建立语法树环节，由于文法问题已经解决，生成的Parser可以解析正确的语法示例了。此时，错误处理主要聚焦在如何处理语法错误上面。 而在组装语义模型并语义模型执行环节，我们关注的则是用于组装语义模型的元素值的合理性。以windowsRange为例，在语义模型中，它有两个元素low和max，代表的windowsRange为[low, max]。但如果你的源码中low的值大于了max的值，从语法的角度是合法的，是可以通过语法解析的。但在语义层面，这就是不合理的。在组装语义模型与执行环节，我们需要将这类问题找出来，报告错误并进行处理。 在本文中我们将对后面两个环节的错误处理的思路与方法做简要说明。 一. 语法解析的错误处理 语法解析这个环节就好比静态语言的编译或动态语言的解析，如果发现语法错误，则提供源码中语法错误的位置和相关辅助信息。ANTLR的Go runtime中提供了ErrorListener接口以及一个DefaultErrorListener的空实现： // github.com/antlr/antlr4/runtime/Go/antlr/error_listener.go type ErrorListener interface { SyntaxError(recognizer Recognizer, offendingSymbol interface{}, line, column int, msg string, e RecognitionException) ReportAmbiguity(recognizer Parser, dfa *DFA, startIndex, stopIndex int, exact bool, ambigAlts *BitSet, configs ATNConfigSet) ReportAttemptingFullContext(recognizer Parser, dfa *DFA, startIndex, stopIndex int, conflictingAlts *BitSet, configs ATNConfigSet) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part5-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5">本文永久链接</a> &#8211; https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5</p>
<p>无论是端应用还是云应用，要上生产环境，有一件事必须要做好，那就是<strong>错误处理</strong>。在本系列前面的文章中，我们<a href="https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1">设计了文法与语法</a>、<a href="https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3">建立并验证了语义模型</a>，但我们没有特别关注错误处理。在这一篇中，我们就来补上这个环节。</p>
<p>DSL设计与实现过程有以下几个主要环节，在不同环节，我们关注的错误处理的主要对象是不同的。如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part5-2.png" alt="" /></p>
<p>在<strong>文法设计与验证环节</strong>，我们更多关注文法设计的正确性。错误的文法会导致解析法示例时失败，但这个环节是在生产Parser代码之前，我们更多是通过ANTLR提供的调试工具对文法的正确性进行调试，无需自己写代码做错误处理。</p>
<p>在<strong>语法解析与建立语法树环节</strong>，由于文法问题已经解决，生成的Parser可以解析正确的语法示例了。此时，错误处理主要聚焦在如何处理语法错误上面。</p>
<p>而在<strong>组装语义模型并语义模型执行环节</strong>，我们关注的则是用于组装语义模型的元素值的合理性。以windowsRange为例，在语义模型中，它有两个元素low和max，代表的windowsRange为[low, max]。但如果你的源码中low的值大于了max的值，从语法的角度是合法的，是可以通过语法解析的。但在语义层面，这就是不合理的。在组装语义模型与执行环节，我们需要将这类问题找出来，报告错误并进行处理。</p>
<p>在本文中我们将对后面两个环节的错误处理的思路与方法做简要说明。</p>
<h3>一. 语法解析的错误处理</h3>
<p>语法解析这个环节就好比静态语言的编译或动态语言的解析，如果发现语法错误，则提供源码中语法错误的位置和相关辅助信息。ANTLR的Go runtime中提供了ErrorListener接口以及一个DefaultErrorListener的空实现：</p>
<pre><code>// github.com/antlr/antlr4/runtime/Go/antlr/error_listener.go
type ErrorListener interface {
    SyntaxError(recognizer Recognizer, offendingSymbol interface{}, line, column int, msg string, e RecognitionException)
    ReportAmbiguity(recognizer Parser, dfa *DFA, startIndex, stopIndex int, exact bool, ambigAlts *BitSet, configs ATNConfigSet)
    ReportAttemptingFullContext(recognizer Parser, dfa *DFA, startIndex, stopIndex int, conflictingAlts *BitSet, configs ATNConfigSet)
    ReportContextSensitivity(recognizer Parser, dfa *DFA, startIndex, stopIndex, prediction int, configs ATNConfigSet)
}
</code></pre>
<p>ErrorListener这个接口中的SyntaxError方法正是我们在这个环节需要的，它可以帮助我们捕捉到语法示例解析时的语法错误。</p>
<p>Parser内置了ErrorListener的实现，比如antlr.ConsoleErrorListener。但这个Listener在源码示例的解析过程中啥也不会输出，毫无存在感，我们需要自定义一个可以提示错误语法信息的ErrorListener实现。</p>
<p>下面是我参考《ANTLR4权威指南》中的Java例子实现的一个Go版本的VerboseErrorListener：</p>
<pre><code>// tdat/error_listener.go
type VerboseErrorListener struct {
    *antlr.DefaultErrorListener
    hasError bool
}

func NewVerboseErrorListener() *VerboseErrorListener {
    return new(VerboseErrorListener)
}

func (d *VerboseErrorListener) HasError() bool {
    return d.hasError
}

func (d *VerboseErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
    p := recognizer.(antlr.Parser)
    stack := p.GetRuleInvocationStack(p.GetParserRuleContext())

    fmt.Printf("rule stack: %v ", stack[0])
    fmt.Printf("line %d: %d at %v : %s\n", line, column, offendingSymbol, msg)

    d.hasError = true
}
</code></pre>
<p>Parser在解析源码过程中，在发现语法错误时会回调VerboseErrorListener的SyntaxError方法，SyntaxError传入的各个参数中包含语法错误的详细信息，我们只需向上面这样将这些信息按一定格式组装起来输出即可。</p>
<p>另外这里给VerboseErrorListener增加了一个hasError布尔字段，用于标识源文件解析过程中是否出现了语法错误，程序可以根据这个错误标识选择后续的执行路径。</p>
<p>下面是main函数中VerboseErrorListener的用法：</p>
<pre><code>func main() {
    ... ...
    lexer := parser.NewTdatLexer(input)
    stream := antlr.NewCommonTokenStream(lexer, 0)
    p := parser.NewTdatParser(stream)

    el := NewVerboseErrorListener()
    p.RemoveErrorListeners()
    p.AddErrorListener(el)

    tree := p.Prog()

    if el.HasError() {
        return
    }
    ... ...
}
</code></pre>
<p>从上面代码可以看到，我们在创建TdatParser实例后面，在解析源码(p.Prog())之前，需要先将其默认内置的ErrorListener删除掉，然后加入我们自己的VerboseErrorListener实例。之后main函数根据VerboseErrorListener是否包含监听到语法错误的状态决定是否继续向下执行，如果发现有语法错误，则终止程序运行。</p>
<p>我们添加一个带有语法错误的语法示例sample5-invalid.t：</p>
<pre><code>// tdat/samples/sample5-invalid.t

r0006: Aach { |1,3| ($speed &lt; 50e) and (($temperature + 1) &lt; 4) or ((roundDown($salinity) &lt;= 600.0) or (roundUp($ph) &gt; 8.0)) } =&gt; ();
</code></pre>
<p>让tdat程序解析一下sample5-invalid.t，我们得到下面结果：</p>
<pre><code>$./tdat samples/sample5-invalid.t
input file: samples/sample5-invalid.t
rule: enumerableFunc line 2: 7 at [@2,8:11='Aach',&lt;29&gt;,2:7] : mismatched input 'Aach' expecting {'Each', 'None', 'Any'}
rule: conditionExpr line 2: 32 at [@13,33:33='e',&lt;29&gt;,2:32] : extraneous input 'e' expecting ')'
</code></pre>
<p>我们看到，程序输出了语法问题的详细信息，并停止了继续执行。</p>
<h3>二. 语义模型组装与执行环节的错误处理</h3>
<p>和语法解析时相对形式固定的错误处理不同，语义层面的错误形式更加多种多样，分布的位置也比较光，每个解析规则(parse rule)处都可能存在语义问题，就像前面提到的windowsRange的low > high的问题。再比如在传入的数据中找不到result中指明的字段等。</p>
<p>无论是组装语义模型，还是语义模型的执行，都是树的遍历，遍历函数存在递归，且层次可能很深，这样传统的error作为返回值不太适合。最好的方式是结合panic+recover的方式，当某个环节的语义出现问题时，直接panic，然后在上层通过recover捕捉panic，再以error方式将panic携带的error信息返回。我们就以windowRange的语义问题作为一个例子来看看语义模型组装和执行过程中如何处理错误。</p>
<p>首先，我们改造一下ReversePolishExprListener的ExitWindowsWithLowAndHighIndex方法，当解析后发现low > high时，抛出panic：</p>
<pre><code>// tdat/reverse_polish_expr_listener.go

func (l *ReversePolishExprListener) ExitWindowsWithLowAndHighIndex(c *parser.WindowsWithLowAndHighIndexContext) {
    s := c.GetText()
    s = s[1 : len(s)-1] // remove two '|'

    t := strings.Split(s, ",")

    if t[0] == "" {
        l.low = 1
    } else {
        l.low, _ = strconv.Atoi(t[0])
    }

    if t[1] == "" {
        l.high = windowsRangeMax
    } else {
        l.high, _ = strconv.Atoi(t[1])
    }

    if l.high &lt; l.low {
        panic(fmt.Sprintf("windowsRange: low(%d) &gt; high(%d)", l.low, l.high))
    }
}
</code></pre>
<p>为了不在main中直接捕获panic，我们将原先的遍历tree的语句：</p>
<pre><code>antlr.ParseTreeWalkerDefault.Walk(l, tree)
</code></pre>
<p>挪到一个新函数extractReversePolishExpr中，我们在extractReversePolishExpr中捕获panic，并以普通error的形式将错误返回给main函数：</p>
<pre><code>// tdat/main.go

func extractReversePolishExpr(listener antlr.ParseTreeListener, t antlr.Tree) (err error) {
    defer func() {
        if x := recover(); x != nil {
            err = fmt.Errorf("semantic tree assembly error: %v", x)
        }
    }()

    antlr.ParseTreeWalkerDefault.Walk(listener, t)

    return nil
}
</code></pre>
<p>在main函数中，我们像下面这样使用extractReversePolishExpr：</p>
<pre><code>// tdat/main.go

func main() {
    ... ...
    l := NewReversePolishExprListener()
    err = extractReversePolishExpr(l, tree)
    if err != nil {
        fmt.Printf("%s\n", err)
        return
    }
    ... ...
}
</code></pre>
<p>当extractReversePolishExpr返回错误时，意味着提取逆波兰式的过程出现了问题，我们将终止程序运行。</p>
<p>接下来我们就构造一个语义错误的例子samples/sample6-windowrange-invalid.t来看看上述程序捕捉语义错误的过程：</p>
<pre><code>// samples/sample6-windowrange-invalid.t
r0006: Each { |3,1| ($speed &lt; 50) and (($temperature + 1) &lt; 4) or ((roundDown($salinity) &lt;= 600.0) or (roundUp($ph) &gt; 8.0)) } =&gt; ();
</code></pre>
<p>运行一下我们的新程序：</p>
<pre><code>$./tdat samples/sample6-windowrange-invalid.t
input file: samples/sample6-windowrange-invalid.t
semantic tree assembly error: windowsRange: low(3) &gt; high(1)
</code></pre>
<p>我们看到：程序成功捕捉到了预期的语义错误。</p>
<p>在后续的语义模型执行过程中，semantic包的Evaluate函数也使用了defer + recover捕捉了可能在表达式树求值过程中可能出现的panic，并通过error形式返回给其调用者。甚至在组装过程中没有被捕捉到的语义问题，一旦引发语义执行错误，同样也会被捕捉到。</p>
<p>由于原理相同，针对语义模型执行过程的错误处理，这里就不赘述了。</p>
<h3>三. 小结</h3>
<p>在本篇文章中，我们补充了设计与实现DSL过程中错误处理，针对语法解析和语义模型组装与执行两个环节给出相应的错误处理方案。</p>
<p>在《领域特定语言》一书中，Martin Fowler写道：“解析和生成输出是编写编译器中容易的部分，真正的难点在于给出更好的错误信息”。错误处理在基于DSL的处理引擎中占有十分重要的地位，良好的错误处理设计对后续引擎的问题诊断、演进与维护大有裨益。</p>
<p>本文中涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/antlr/tdat">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，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><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>手把手教你使用ANTLR和Go实现一门DSL语言（第四部分）：组装语义模型并测试DSL</title>
		<link>https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4/</link>
		<comments>https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4/#comments</comments>
		<pubDate>Sat, 28 May 2022 06:48:44 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANTLR]]></category>
		<category><![CDATA[ast]]></category>
		<category><![CDATA[binary-tree]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[Evaluate]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Listener]]></category>
		<category><![CDATA[literal]]></category>
		<category><![CDATA[MartinFowler]]></category>
		<category><![CDATA[node]]></category>
		<category><![CDATA[parser]]></category>
		<category><![CDATA[pop]]></category>
		<category><![CDATA[push]]></category>
		<category><![CDATA[ReversePolishExpr]]></category>
		<category><![CDATA[semantic-model]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[top]]></category>
		<category><![CDATA[variable]]></category>
		<category><![CDATA[中序遍历]]></category>
		<category><![CDATA[二叉树]]></category>
		<category><![CDATA[产生式规则]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[前序遍历]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[压栈]]></category>
		<category><![CDATA[叶子节点]]></category>
		<category><![CDATA[后天]]></category>
		<category><![CDATA[后序遍历]]></category>
		<category><![CDATA[弹栈]]></category>
		<category><![CDATA[操作符]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[栈]]></category>
		<category><![CDATA[求值]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[表达式树]]></category>
		<category><![CDATA[表驱动]]></category>
		<category><![CDATA[解析器]]></category>
		<category><![CDATA[语义模型]]></category>
		<category><![CDATA[语法树]]></category>
		<category><![CDATA[运算符]]></category>
		<category><![CDATA[逆波兰表达式]]></category>
		<category><![CDATA[递归]]></category>
		<category><![CDATA[遍历]]></category>
		<category><![CDATA[领域特定语言]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3565</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4 在上一篇文章中，我们为DSL建立了完整的语义模型，我们距离DSL的语法示例真正run起来还差最后一步，那就是基于语法树提取信息(逆波兰式)、组装语义模型，在加载语义模型并实例化各个规则处理器(processor)后，我们就可以处理数据了！下面是我们部署在海洋浮标上的指标采集程序的全景图： 在这一篇中，我们就来按照上图，通过语法树提取逆波兰式并组装语义模型，让我们的语法示例能真正按预期run起来！ 一. 从语法树提取逆波兰式并组装语义模型 通过上面语义模型的讲解，我们知道了语法树与语义模型之间的联系包括逆波兰式、windowsRange、result和enumableFunc。其主要联系是那个逆波兰式，而像windowsRange、result和enumableFunc这些信息都相对容易提取。 接下来，我们先来看看如何从DSL的语法树构提取到逆波兰式，完成逆波兰式的提取，我们的语义模型组装工作就算完成大半了。好，下面我们就将目光聚焦在DSL语法树上。 为了聚焦原理的讲解，我们在本篇仅实现支持语法示例文件中包含单rule的语法树的逆波兰式等信息的提取。而语法示例文件中有多个rule的情况就当做思考题留给大家了。 在本系列第二部分验证文法中，我们知道了ANTLR Listener对DSL语法树的遍历默认都是前序遍历。在这样的遍历过程中，我们要提取variable、literal、一元操作符以及二元操作符，并将它们的运算次序以逆波兰式的形式组织起来。我们采用的提取转换算法如下： 我们借由两个Stack来完成此次转换，s1用于存储已有序的逆波兰式；s2是一个临时栈，用于临时存放一元和二元操作符； 我们在所有节点的ExitXXX回调中执行提取操作； 当节点为variable或literal时，直接将节点text转换为对应的类型值(比如int、float64或string)后，打包为Value，压入s1栈； 当节点为一元操作符节点时，计算节点深度(level)，与其代表的一个semantic.UnaryOperator一同压入s2栈； 当节点为二元操作符节点时，包括arithmeticOp、comparisionOp以及logicalOp，则用当前节点的深度(level)与s2栈顶元素进行比较，如果比s2栈顶内的节点的深度(level)小，就将s2栈顶的节点弹出，并压入s1栈；循环此步骤，直到s2栈空或当前节点深度大于s2栈顶元素深度，则将该节点打包为semantic.BinaryOperator并压入s2栈； 在顶层conditionExpr节点(parent node为ruleLine)的exit回调中，将s2栈中元素全部弹出并依次压入s1栈；此时s1栈中从栈底到栈顶就是一个逆波兰式。 下面是具体的代码实现，我们建立一个ReversePolishExprListener结构用于从语法树中提取用于构建语义模型的信息： // tdat/reverse_polish_expr_listener.go type ReversePolishExprListener struct { *parser.BaseTdatListener ruleID string // for constructing Reverse Polish expression // // infixExpr:($speed&#60;5)and($temperature&#60;2)or(roundDown($sanility)&#60;600) =&#62; // // reversePolishExpr: // $speed,5,&#60;,$temperature,2,&#60;,and,$sanility,roundDown,600,&#60;,or // reversePolishExpr []semantic.Value s1 semantic.Stack[*Item] // temp stack for constructing reversePolishExpr, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part4-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4">本文永久链接</a> &#8211; https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4</p>
<p>在<a href="https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3">上一篇文章</a>中，我们为DSL建立了完整的语义模型，我们距离DSL的语法示例真正run起来还差最后一步，那就是基于语法树提取信息(逆波兰式)、组装语义模型，在加载语义模型并实例化各个规则处理器(processor)后，我们就可以处理数据了！下面是我们部署在海洋浮标上的指标采集程序的全景图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part4-2.png" alt="" /></p>
<p>在这一篇中，我们就来按照上图，通过语法树提取逆波兰式并组装语义模型，让我们的语法示例能真正按预期run起来！</p>
<h3>一. 从语法树提取逆波兰式并组装语义模型</h3>
<p>通过上面语义模型的讲解，我们知道了语法树与语义模型之间的联系包括逆波兰式、windowsRange、result和enumableFunc。其主要联系是那个逆波兰式，而像windowsRange、result和enumableFunc这些信息都相对容易提取。</p>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part4-3.png" alt="" /></p>
<p>接下来，我们先来看看如何从DSL的语法树构提取到逆波兰式，完成逆波兰式的提取，我们的语义模型组装工作就算完成大半了。好，下面我们就将目光聚焦在DSL语法树上。</p>
<p>为了聚焦原理的讲解，我们在本篇<strong>仅实现支持语法示例文件中包含单rule的语法树的逆波兰式等信息的提取</strong>。而语法示例文件中有多个rule的情况就当做思考题留给大家了。</p>
<p>在<a href="https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2">本系列第二部分验证文法</a>中，我们知道了ANTLR Listener对DSL语法树的遍历默认都是前序遍历。在这样的遍历过程中，我们要提取variable、literal、一元操作符以及二元操作符，并将它们的运算次序以逆波兰式的形式组织起来。我们采用的提取转换算法如下：</p>
<ul>
<li>我们借由两个Stack来完成此次转换，s1用于存储已有序的逆波兰式；s2是一个临时栈，用于临时存放一元和二元操作符；</li>
<li>我们在所有节点的ExitXXX回调中执行提取操作；</li>
<li>当节点为variable或literal时，直接将节点text转换为对应的类型值(比如int、float64或string)后，打包为Value，压入s1栈；</li>
<li>当节点为一元操作符节点时，计算节点深度(level)，与其代表的一个semantic.UnaryOperator一同压入s2栈；</li>
<li>当节点为二元操作符节点时，包括arithmeticOp、comparisionOp以及logicalOp，则用当前节点的深度(level)与s2栈顶元素进行比较，如果比s2栈顶内的节点的深度(level)小，就将s2栈顶的节点弹出，并压入s1栈；循环此步骤，直到s2栈空或当前节点深度大于s2栈顶元素深度，则将该节点打包为semantic.BinaryOperator并压入s2栈；</li>
<li>在顶层conditionExpr节点(parent node为ruleLine)的exit回调中，将s2栈中元素全部弹出并依次压入s1栈；此时s1栈中从栈底到栈顶就是一个逆波兰式。</li>
</ul>
<p>下面是具体的代码实现，我们建立一个ReversePolishExprListener结构用于从语法树中提取用于构建语义模型的信息：</p>
<pre><code>// tdat/reverse_polish_expr_listener.go

type ReversePolishExprListener struct {
    *parser.BaseTdatListener

    ruleID string

    // for constructing Reverse Polish expression
    //
    // infixExpr:($speed&lt;5)and($temperature&lt;2)or(roundDown($sanility)&lt;600) =&gt;
    //
    // reversePolishExpr:
    // $speed,5,&lt;,$temperature,2,&lt;,and,$sanility,roundDown,600,&lt;,or
    //
    reversePolishExpr []semantic.Value
    s1                semantic.Stack[*Item] // temp stack for constructing reversePolishExpr, for final result
    s2                semantic.Stack[*Item] // temp stack for constructing reversePolishExpr, for operator temporarily

    // for windowsRange
    low  int
    high int

    // for enumerableFunc
    ef string

    // for result
    result []string
}
</code></pre>
<p>对于variable、literal都是直接压到s1栈中，对于一元操作符，直接压入s2栈中；对于二元操作符，我们以比较操作符(comparisonOp)为例，看看其处理逻辑：</p>
<pre><code>func (l *ReversePolishExprListener) ExitComparisonOp(c *parser.ComparisonOpContext) {
    l.handleBinOperator(c.BaseParserRuleContext)
}

func (l *ReversePolishExprListener) handleBinOperator(c *antlr.BaseParserRuleContext) {
    v := c.GetText()
    lvl := getLevel(c)

    for {
        lastOp := l.s2.Top()
        if lastOp == nil {
            l.s2.Push(&amp;Item{
                level: lvl,
                val: &amp;semantic.BinaryOperator{
                    Val: v,
                },
            })
            return
        }

        if lvl &gt; lastOp.level {
            l.s2.Push(&amp;Item{
                level: lvl,
                val: &amp;semantic.BinaryOperator{
                    Val: v,
                },
            })
            return
        }
        l.s1.Push(l.s2.Pop())
    }
}
</code></pre>
<p>算术操作符、逻辑操作符等二元操作符都像比较操作符一样，直接调用handleBinOperator。handleBinOperator的逻辑就像我们前面描述的算法步骤那样，先比较s2栈顶的节点的level，如果该节点的深度比s2栈顶内的节点的深度(level)小，就将s2栈顶的节点弹出，并压入s1栈；循环此步骤，直到s2栈空或当前节点深度大于s2栈顶节点深度，则将该节点打包为semantic.BinaryOperator并压入s2栈。</p>
<p>我们在最顶层的conditionExpr中基于s1栈得到我们期望的逆波兰表达式：</p>
<pre><code>func (l *ReversePolishExprListener) ExitConditionExpr(c *parser.ConditionExprContext) {
    // get the rule index of parent context
    if i, ok := c.GetParent().(antlr.RuleContext); ok {
        if i.GetRuleIndex() != parser.TdatParserRULE_ruleLine {
            // 非最顶层的conditionExpr节点
            return
        }
    }

    // pop all left in the stack
    for l.s2.Len() != 0 {
        l.s1.Push(l.s2.Pop())
    }

    // fill in the reversePolishExpr
    var vs []semantic.Value
    for l.s1.Len() != 0 {
        vs = append(vs, l.s1.Pop().val)
    }

    for i := len(vs) - 1; i &gt;= 0; i-- {
        l.reversePolishExpr = append(l.reversePolishExpr, vs[i])
    }
}
</code></pre>
<p>其他诸如result、windowsRange等构建语义模型所需的信息的提取比较简单，大家可以直接参考ReversePolishExprListener相应的方法的源码。</p>
<h3>二. 实例化Processor并运行语法示例</h3>
<p>是时候将这门语言的前端(语法树)和后端(语义模型)串起来了！为此，我们定义了一个类型Processor用于组装前端与后端：</p>
<pre><code>type Processor struct {
    name  string // for ruleid
    model *semantic.Model
}
</code></pre>
<p>同时每个Processor实例对应一个语法rule，如果有多个rule，可以实例化不同的Processor，之后我们就可以使用Processor实例的Exec方法来处理数据了：</p>
<pre><code>func (p *Processor) Exec(in []map[string]interface{}) (map[string]interface{}, error) {
    return p.model.Exec(in)
}
</code></pre>
<p>我们看一下main函数：</p>
<pre><code>// tdat/main.go

func main() {
    println("input file:", os.Args[1])
    input, err := antlr.NewFileStream(os.Args[1])
    if err != nil {
        panic(err)
    }

    lexer := parser.NewTdatLexer(input)
    stream := antlr.NewCommonTokenStream(lexer, 0)
    p := parser.NewTdatParser(stream)
    tree := p.Prog()

    l := NewReversePolishExprListener()
    antlr.ParseTreeWalkerDefault.Walk(l, tree)

    processor := &amp;Processor{
        name:  l.ruleID,
        model: semantic.NewModel(l.reversePolishExpr, semantic.NewWindowsRange(l.low, l.high), l.ef, l.result),
    }

    // r0006: Each { |1,3| ($speed &lt; 50) and (($temperature + 1) &lt; 4) or ((roundDown($salinity) &lt;= 600.0) or (roundUp($ph) &gt; 8.0)) } =&gt; ();

    in := []map[string]interface{}{
        {
            "speed":       30,
            "temperature": 6,
            "salinity":    500.0,
            "ph":          7.0,
        },
        {
            "speed":       31,
            "temperature": 7,
            "salinity":    501.0,
            "ph":          7.1,
        },
        {
            "speed":       30,
            "temperature": 6,
            "salinity":    498.0,
            "ph":          6.9,
        },
    }

    out, err := processor.Exec(in)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%v\n", out)

}
</code></pre>
<p>main函数的步骤大致是：构建语法树(p.Prog)，提取语义模型所需信息(ParseTreeWalkerDefault.Walk)，然后实例化Processor，连接前后端，最后通过processor.Exec处理输入数据in。</p>
<p>接下来，我们定义samples/sample4.t作为语法示例来测试main：</p>
<pre><code>// samples/sample4.t

r0006: Each { |1,3| ($speed &lt; 50) and (($temperature + 1) &lt; 4) or ((roundDown($salinity) &lt;= 600.0) or (roundUp($ph) &gt; 8.0)) } =&gt; ();
</code></pre>
<p>构建并执行main：</p>
<pre><code>$make
$./tdat samples/sample4.t
map[ph:7 salinity:500 speed:30 temperature:6]
</code></pre>
<p>我们看到，程序输出了我们期望的结果！</p>
<h3>三. 小结</h3>
<p>到这里，我们为《后天》里的气象学家构建的DSL语言以及其处理引擎的核心都已经介绍完了。上述代码<strong>目前仅能处理一个源文件中仅有一个rule</strong>。将处理引擎扩展为可以支持在一个源文件中放置多个rule的任务就留给大家作为“作业”了^_^。</p>
<p>经过这个系列四篇文章后，相信你已经基本了解了基于ANTLR和Go设计和实现一门DSL语言的方法。现在你可以为你的领域设计你自用或团队自用的DSL了，欢迎大家在文章后面留言交流，我们一起提升设计和实现DSL的水平。</p>
<p>本文中涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/antlr/tdat">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，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><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
