<?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; Ruby</title>
	<atom:link href="http://tonybai.com/tag/ruby/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 12 Apr 2026 22:30:28 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>硬核测评：哪门语言最受 AI 宠爱？13 种语言横向对比，Go 表现如何？</title>
		<link>https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance/</link>
		<comments>https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance/#comments</comments>
		<pubDate>Mon, 09 Mar 2026 00:02:44 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AIAgent]]></category>
		<category><![CDATA[AIProgramming]]></category>
		<category><![CDATA[AI智能体]]></category>
		<category><![CDATA[AI编程]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[BorrowChecker]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[CodeGeneration]]></category>
		<category><![CDATA[CompilationSpeed]]></category>
		<category><![CDATA[DeveloperExperience]]></category>
		<category><![CDATA[DynamicLanguages]]></category>
		<category><![CDATA[FunctionalLanguages]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Haskell]]></category>
		<category><![CDATA[InferenceCost]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[mini-git]]></category>
		<category><![CDATA[Ocaml]]></category>
		<category><![CDATA[PerformanceReview]]></category>
		<category><![CDATA[ProgrammingLanguages]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[StaticLanguages]]></category>
		<category><![CDATA[TypeChecking]]></category>
		<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=6010</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance 大家好，我是Tony Bai。 随着 Claude Code、Gemini Cli、Codex 等 AI 编程工具的全面普及，“让 AI 写代码”已经从极客的玩具变成了日常的生产力。随之而来的是一个触及灵魂的问题：哪种编程语言最适合交给 AI 去写？ 作为 Gopher，我们一直为 Go 语言的“极简语法”、“极速编译”和“强类型安全”感到自豪。我们理所当然地认为，这种没有任何隐式魔法、像白开水一样的语言，绝对是 LLM 的最爱。 然而，现实总是比直觉更骨感。近日，Ruby 核心提交者 Yusuke Endoh（@mame）发布了一份名为 ai-coding-lang-bench 的硬核定量测评报告。他使用 Claude Code（Opus 4.6 模型）对 13 种主流编程语言进行了系统性横向对比。 在这场涵盖了动态语言、静态语言和函数式语言的混战中，Go 语言的表现究竟如何？ 是力压群雄，还是黯然失色？那些备受人类推崇的静态类型系统，在 AI 面前是否成了累赘？ 本文和大家一起阅读和拆解这份报告，为你揭晓 AI 时代的语言偏好图谱。 实验设计：让 AI 写一个 Mini-Git 在评价这份报告之前，我们先来看看它的实验设计，这是目前业内少见的、针对 AI Agent 的工程化能力的量化评估。 任务目标：让 Claude Code (Opus 4.6) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/hardcore-review-13-languages-ai-favorite-go-performance-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance">本文永久链接</a> &#8211; https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance</p>
<p>大家好，我是Tony Bai。</p>
<p>随着 <a href="http://gk.link/a/12EPd">Claude Code</a>、<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4067128336651386882#wechat_redirect">Gemini Cli</a>、Codex 等 AI 编程工具的全面普及，“让 AI 写代码”已经从极客的玩具变成了日常的生产力。随之而来的是一个触及灵魂的问题：<strong>哪种编程语言最适合交给 AI 去写？</strong></p>
<p>作为 Gopher，我们一直为 Go 语言的“极简语法”、“极速编译”和“强类型安全”感到自豪。我们理所当然地认为，这种没有任何隐式魔法、像白开水一样的语言，绝对是 LLM 的最爱。</p>
<p>然而，现实总是比直觉更骨感。近日，Ruby 核心提交者 Yusuke Endoh（@mame）发布了<a href="https://github.com/mame/ai-coding-lang-bench">一份名为 ai-coding-lang-bench 的硬核定量测评报告</a>。他使用 Claude Code（Opus 4.6 模型）对 13 种主流编程语言进行了系统性横向对比。</p>
<p>在这场涵盖了动态语言、静态语言和函数式语言的混战中，<strong>Go 语言的表现究竟如何？</strong> 是力压群雄，还是黯然失色？那些备受人类推崇的静态类型系统，在 AI 面前是否成了累赘？</p>
<p>本文和大家一起阅读和拆解这份报告，为你揭晓 AI 时代的语言偏好图谱。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/agentic-software-engineering-qr.png" alt="" /></p>
<h2>实验设计：让 AI 写一个 Mini-Git</h2>
<p>在评价这份报告之前，我们先来看看它的实验设计，这是目前业内少见的、针对 AI Agent 的工程化能力的量化评估。</p>
<p><strong>任务目标</strong>：让 Claude Code (Opus 4.6) 从零开始实现一个 mini-git（简化版的 Git）。这是一个极具代表性的任务，它包含了文件 I/O、哈希计算、数据结构操作以及命令行接口，足以考验模型对语言生态的综合运用能力。</p>
<p>测试被巧妙地分为两个阶段，模拟了真实的软件生命周期：</p>
<ul>
<li>v1 (新项目构建)：实现基础的 init, add, commit 和 log。</li>
<li>v2 (特性扩展)：在 v1 的基础上，增加 status, diff, checkout, reset, rm, show 等复杂指令。</li>
</ul>
<p><strong>提示词（Prompt）极其极简</strong>：“阅读 SPEC-v1.txt，实现它，并确保 test-v1.sh 测试通过。”这种设计最大程度地减少了人类指令的干预，纯粹考验 AI 代理在闭环环境下的自主编码、调试和测试能力。</p>
<p><strong>参赛选手（13种语言/15种配置）</strong>：</p>
<ul>
<li>动态语言：Python, Ruby, JavaScript, Perl, Lua</li>
<li>动态+类型检查器：Python/mypy, Ruby/Steep</li>
<li>静态语言：TypeScript, Go, Rust, C, Java</li>
<li>函数式语言：Scheme (动态), OCaml (静态), Haskell (静态)</li>
</ul>
<p>每种语言配置运行 <strong>20 次</strong>，以消除 LLM 生成的随机性带来的误差，并统计其耗时（Time）、成本（Cost，即 Token 消耗）和代码行数（LOC）。</p>
<h2>核心发现：动态语言逆袭，Go 位居第二梯队</h2>
<p>如果仅看总耗时和总成本（v1 + v2），测试结果呈现出了令人瞩目的阶梯式分布。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/hardcore-review-13-languages-ai-favorite-go-performance-2.png" alt="" /></p>
<h3>第一梯队：Ruby, Python, JavaScript 的绝对统治</h3>
<p>在这场 AI 编程竞速中，Ruby（73.1s）、Python（74.6s）和 JavaScript（81.1s）组成了无可争议的第一阵营。</p>
<p>它们不仅生成速度最快、消耗 API 成本最低（均在 $0.40 以下），而且在 20 次测试中表现出了极高的稳定性（标准差极小）。</p>
<p>对于 AI 来说，生成这三种语言的代码就像呼吸一样自然。它们无需繁琐的项目初始化配置（如 Cargo.toml 或 package.json），可以做到“建个文件直接跑”，这种极简的工作流在 v1 阶段（新项目构建）优势极大。</p>
<h3>第二梯队：被“强类型”拖慢脚步的 Go 与 Java</h3>
<p>现在，来回答大家最关心的问题：Go 表现如何？</p>
<p>答案是：位居第二梯队。Go 的总耗时为 101.6s，平均成本 $0.50。中规中矩。Go 虽然在语法上非常克制，但依然落后于 Python 和 JS 等动态语言。与之类似，Java（115.4s）也因为繁琐的语法结构和强类型约束，留在了这一梯队。</p>
<p>尽管如此，Go 在整个 20 次测试中没有出现一次失败（0 次 fail），这证明了 <a href="https://tonybai.com/2026/01/04/stop-lying-to-the-compiler">Go 的编译器在防止 AI 产生“幻觉 Bug”方面，发挥了极其可靠的安全网作用</a>。</p>
<h3>“后进生”阵营：Rust 与 C 的挣扎</h3>
<p>备受人类极客推崇的 Rust（113.7s，且有 2 次失败）和底层的 C（155.8s）在测试中显得步履维艰。</p>
<p>尤为值得注意的是，在总共 600 次的独立运行中，只有 Rust (2次) 和 Haskell (1次) 出现了测试失败（未能最终跑通 Shell 脚本）的情况。这两门语言都以其极高的心智负担和“编译器教你做人”的严格程度而闻名。</p>
<p>这也是将Rust列入“后进生”阵营的主要原因。如果用《飞驰人生》的拉力赛来比喻，Rust 相当于在40站的赛季中，有两站没能完赛！</p>
<h2>深度剖析：为什么 AI 更偏爱动态语言？</h2>
<p>在传统的工程视角中，“静态类型防止低级 Bug”、“动态语言难以维护”是金科玉律。但在 LLM 驱动的 Agent 开发流中，这个逻辑为何失效了？作者 Yusuke Endoh 提出了几个关键的解释维度。</p>
<h3>训练数据的“虹吸效应”</h3>
<p>LLM 的能力直接取决于训练语料的规模和质量。Python、JavaScript 和 Ruby 是过去十几年 Web 开发的绝对主流。GitHub 上海量的这三种语言的开源代码、StackOverflow 上的问答，为 Claude Code 提供了极其丰富的“预训练肌肉记忆”。</p>
<p>当 AI 需要实现一个功能时，它在 Python 的隐空间（Latent Space）中寻找最优解的路径，远比在 Haskell 甚至 Rust 中要清晰、笔直得多。</p>
<h3>静态类型的“双刃剑”与重构阻力</h3>
<p>静态类型系统的初衷是约束人类，防止我们在重构时犯错。但在 AI 的“ Prompt -> 生成 -> 测试报错 -> 思考 -> 再生成”的迭代循环中，严格的类型检查反而成了巨大的“摩擦力”。</p>
<ul>
<li>编译成本与调试死锁：在 Rust 或 C 中，当 AI 生成的代码出现类型不匹配时，它需要花费大量的 Token 去阅读复杂的编译器报错信息。有时，为了解决一个简单的借用检查器（Borrow Checker）报错，AI 可能会陷入漫长的、无休止的“试错-编译失败”死循环。</li>
<li>重构牵一发而动全身：在 v2 特性扩展阶段，往往需要修改现有的数据结构。对于 Python，AI 只需要在字典里加个 key；而对于 Rust 或 Java，这可能意味着需要重构一系列的 Struct、更新类型签名、甚至修改与之相关的无数个函数的参数声明。这种“爆炸半径”极大地增加了耗时。</li>
</ul>
<h3>“附加类型检查”的巨大损耗</h3>
<p>报告中一个非常有意思的对照组是：原生动态语言 vs 附加类型检查器的动态语言。</p>
<ul>
<li>Python (74.6s) vs Python/mypy (125.3s) —— 变慢了 1.6~1.7 倍。</li>
<li>Ruby (73.1s) vs Ruby/Steep (186.6s) —— 变慢了 2.0~3.2 倍！</li>
</ul>
<p>这证明了，迫使 AI 在动态语言中编写严谨的类型注解（Type Annotations），是一项极其昂贵的任务。模型需要耗费额外的算力去推导类型、生成类型声明文件，并且在类型检查器报错时，还要去修复那些在纯动态模式下可能根本不影响运行的“伪 Bug”。</p>
<h3>代码量（LOC）的迷思：越短越好吗？</h3>
<p>我们通常认为，写得越少，跑得越快。但数据打破了这个迷思。</p>
<p>Haskell 和 OCaml 生成的最终代码行数（224行和 216行）是所有语言中最少的，甚至少于 Python 和 Ruby。然而，它们在生成时间上的表现却排在倒数（Haskell 耗时最长，达 174s）。</p>
<p>这表明，对于 AI 来说，函数式语言那种高度抽象、信息密度极大的代码，生成和推理的成本远高于像 Python、Go 那种稍微啰嗦但逻辑平铺直叙的“大白话”代码。浓缩的未必是精华，对于 LLM 来说，高度浓缩往往意味着更高的生成熵和更高的试错概率。</p>
<h2>行业启示：我们需要重新思考 AI 时代的技术栈选型</h2>
<p>面对这份详实的基准测试报告，无论你是 CTO 还是普通开发者，都必须开始重新审视未来的技术选型逻辑。</p>
<h3>动态语言是快速原型的“绝对王者”</h3>
<p>如果你正在启动一个新项目，或者需要用 AI Agent 快速验证一个业务流程，Python 和 TypeScript 是首选（报告中 JavaScript 表现优于 TS，但在实际工程中 TS 的综合权衡更佳）。</p>
<p>不要迷信“大型项目必须一开始就上强类型编译语言”。在需求快速变化的初期，让 AI 用动态语言狂飙突进，是获取业务反馈最高效的手段。</p>
<h3>性能王者们的困境：Go 与 Rust 在 AI 时代掉队了吗？</h3>
<p>看到测评数据，很多 Gopher 可能会感到失落：难道注重工程严谨性和系统级性能的静态语言，真的在 AI 时代掉队了吗？</p>
<p>结论并非如此悲观。我们需要明确一点：Agent 测评的速度，不等于软件最终运行的速度。</p>
<ul>
<li>业务试错 vs 基础设施：AI Agent 目前最擅长、也最快速能完成的，是写“胶水逻辑”和“业务 CRUD”。在这些领域，Python 确实快。但当你的系统涉及到高并发、内存精细控制、或者需要打包为轻量级容器部署时，人类依然需要 Go。</li>
<li>容错的底线：在这场 600 次的庞大测试中，只有 Rust 和 Haskell 出现了最终测试失败，而 Go 保持了完美的 100% 成功率。这恰恰说明，Go 在“极度灵活（易幻觉）”与“极度严格（难生成）”之间，找到了一个非常微妙的平衡点。它可能不是 AI 写得最快的，但它一定是 AI 写出来最让人放心的系统级语言。</li>
</ul>
<p>我们不应期待 AI Agent 能够像写 Python 脚本一样，如德芙般丝滑地生成出一个复杂的 Go 并发系统。但在 AI 给出的初稿之上，Go 语言极佳的可读性和统一的规范，将为人类工程师的最终审查（Code Review）节省巨大的精力。</p>
<h2>小结：下一个十年的编程语言，长什么样？</h2>
<p>ai-coding-lang-bench 给我们上了生动的一课。它揭示了当前 LLM 的偏好：<strong>它们喜欢有海量训练数据的、灵活的、不需要应对死板编译器的语言。</strong></p>
<p>但我们必须认识到，这只是一份基于 2026 年初模型（Claude Opus 4.6）的快照。未来的 AI 编程语言形态，可能会朝着两个方向演进：</p>
<ol>
<li>AI Native 语言的诞生：抛弃目前设计给人类阅读的语法，出现一种专门为了降低 LLM 生成 Token 成本、且天然抗幻觉的机器中间语言。</li>
<li>现有静态语言的“Agent 友好化”编译模式：Go 和 Rust 可能会进化出一种特殊的编译模式。在这个模式下，编译器不仅是冷冰冰地报错，还能以结构化的、对 LLM 更友好的方式提供“修复建议”，从而大幅缩短 Agent 修复编译错误的反馈回路。</li>
</ol>
<p>无论如何，浪潮已经来临。在 AI 主导代码生成的新时代，我们评价一门编程语言的标准，正在从“它对人类大脑是否友好”，悄然转变为<strong>“它对大模型推理是否友好”</strong>。</p>
<p>而在这场新赛道上，动态语言们，已经抢跑了。</p>
<p>本文核心数据与图表均来源于 GitHub 项目 <a href="https://github.com/mame/ai-coding-lang-bench">mame/ai-coding-lang-bench</a>。</p>
<hr />
<p><strong>你的 AI 编程初体验</strong></p>
<p>看完这个排名，你是感到意外，还是早已感同身受？在你日常使用 AI 编程时，你觉得它写哪种语言最让你省心？你是否也曾为了修一个 AI 写的编译错误而陷入“死循环”？</p>
<p>欢迎在评论区分享你的“AI 协作”红黑榜！</p>
<hr />
<p>“语言的严格性正在变成 AI 的摩擦力？在 AI 时代，掌握一套能驱动 Agent 自动化、自修复的‘工作流’比死磕语法更重要。我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将教你如何利用 Claude Code 结合 Spec 驱动开发，构建真正高产出的‘软件工厂’。”</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p><strong>原「Gopher部落」已重装升级为「Go &amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！</strong></p>
<p>我们致力于打造一个高品质的 <strong>Go 语言深度学习</strong> 与 <strong>AI 应用探索</strong> 平台。在这里，你将获得：</p>
<ul>
<li><strong>体系化 Go 核心进阶内容:</strong> 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。</li>
<li><strong>前沿 Go+AI 实战赋能:</strong> 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 </li>
<li><strong>星主 Tony Bai 亲自答疑:</strong> 遇到难题？星主第一时间为你深度解析，扫清学习障碍。</li>
<li><strong>高活跃 Gopher 交流圈:</strong> 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。</li>
<li><strong>独家资源与内容首发:</strong> 技术文章、课程更新、精选资源，第一时间触达。</li>
</ul>
<p>衷心希望「Go &amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>告别懵圈：实战派 Gopher 的类型理论入门</title>
		<link>https://tonybai.com/2025/10/30/type-theory-intro-for-gopher/</link>
		<comments>https://tonybai.com/2025/10/30/type-theory-intro-for-gopher/#comments</comments>
		<pubDate>Thu, 30 Oct 2025 00:04:05 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Add]]></category>
		<category><![CDATA[addr接口]]></category>
		<category><![CDATA[AdhocPolymorphism]]></category>
		<category><![CDATA[Animal]]></category>
		<category><![CDATA[any]]></category>
		<category><![CDATA[Area]]></category>
		<category><![CDATA[assignment]]></category>
		<category><![CDATA[bug温床]]></category>
		<category><![CDATA[bytes.Buffer]]></category>
		<category><![CDATA[C/C++]]></category>
		<category><![CDATA[calculate]]></category>
		<category><![CDATA[Cat]]></category>
		<category><![CDATA[ChanMap]]></category>
		<category><![CDATA[chanT]]></category>
		<category><![CDATA[Circle]]></category>
		<category><![CDATA[Closed]]></category>
		<category><![CDATA[DependentTypes]]></category>
		<category><![CDATA[DiscriminatedUnion]]></category>
		<category><![CDATA[Dog]]></category>
		<category><![CDATA[Duck]]></category>
		<category><![CDATA[DuckTyping]]></category>
		<category><![CDATA[Dynamic]]></category>
		<category><![CDATA[DynamicallyTyped]]></category>
		<category><![CDATA[dynamicType]]></category>
		<category><![CDATA[dynamicValue]]></category>
		<category><![CDATA[enum]]></category>
		<category><![CDATA[expression]]></category>
		<category><![CDATA[FunctionTypes]]></category>
		<category><![CDATA[Functor]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[GitHubissue]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[Go实现]]></category>
		<category><![CDATA[Go接口]]></category>
		<category><![CDATA[Go接口系统]]></category>
		<category><![CDATA[Go泛型]]></category>
		<category><![CDATA[Go类型系统]]></category>
		<category><![CDATA[Go设计哲学]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[Go语言特性]]></category>
		<category><![CDATA[Haskell]]></category>
		<category><![CDATA[HigherKindedTypes]]></category>
		<category><![CDATA[HKTs]]></category>
		<category><![CDATA[HTTP中间件]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Interfaces]]></category>
		<category><![CDATA[intSlice]]></category>
		<category><![CDATA[invalidoperation]]></category>
		<category><![CDATA[io.Reader]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Kind]]></category>
		<category><![CDATA[len]]></category>
		<category><![CDATA[main.go]]></category>
		<category><![CDATA[MakeItQuack]]></category>
		<category><![CDATA[map[K]V]]></category>
		<category><![CDATA[mismatchedtypes]]></category>
		<category><![CDATA[monad]]></category>
		<category><![CDATA[multiply]]></category>
		<category><![CDATA[net/url.go]]></category>
		<category><![CDATA[Nominal]]></category>
		<category><![CDATA[NominalTyping]]></category>
		<category><![CDATA[operator]]></category>
		<category><![CDATA[os.File]]></category>
		<category><![CDATA[overloading]]></category>
		<category><![CDATA[ParametricPolymorphism]]></category>
		<category><![CDATA[Person]]></category>
		<category><![CDATA[Point]]></category>
		<category><![CDATA[Polymorphism]]></category>
		<category><![CDATA[PrintArea]]></category>
		<category><![CDATA[ProductID]]></category>
		<category><![CDATA[ProductType]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Quacker]]></category>
		<category><![CDATA[ReadAndPrint]]></category>
		<category><![CDATA[Rectangle]]></category>
		<category><![CDATA[Reverse]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Scala]]></category>
		<category><![CDATA[sealed]]></category>
		<category><![CDATA[shape]]></category>
		<category><![CDATA[SimonThompson]]></category>
		<category><![CDATA[SliceMap]]></category>
		<category><![CDATA[static]]></category>
		<category><![CDATA[StaticallyTyped]]></category>
		<category><![CDATA[strconv]]></category>
		<category><![CDATA[stringSlice]]></category>
		<category><![CDATA[strong]]></category>
		<category><![CDATA[Structural]]></category>
		<category><![CDATA[StructuralSubtyping]]></category>
		<category><![CDATA[StructuralTyping]]></category>
		<category><![CDATA[SubtypePolymorphism]]></category>
		<category><![CDATA[SumType]]></category>
		<category><![CDATA[traits]]></category>
		<category><![CDATA[Tuple]]></category>
		<category><![CDATA[type-switch]]></category>
		<category><![CDATA[typeassertion]]></category>
		<category><![CDATA[TypeConstructor]]></category>
		<category><![CDATA[typeswitch]]></category>
		<category><![CDATA[TypeSystem]]></category>
		<category><![CDATA[TypeTheory&FunctionalProgramming]]></category>
		<category><![CDATA[UniversalMap]]></category>
		<category><![CDATA[untypedstring]]></category>
		<category><![CDATA[UserID]]></category>
		<category><![CDATA[value]]></category>
		<category><![CDATA[variable]]></category>
		<category><![CDATA[Variant]]></category>
		<category><![CDATA[vector]]></category>
		<category><![CDATA[Vector(n)]]></category>
		<category><![CDATA[weak]]></category>
		<category><![CDATA[[N]T]]></category>
		<category><![CDATA[[]T]]></category>
		<category><![CDATA[一等公民函数]]></category>
		<category><![CDATA[一阶类型]]></category>
		<category><![CDATA[不同类型]]></category>
		<category><![CDATA[严谨性]]></category>
		<category><![CDATA[乘法]]></category>
		<category><![CDATA[代码示例]]></category>
		<category><![CDATA[依赖类型]]></category>
		<category><![CDATA[值]]></category>
		<category><![CDATA[元组]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[函数式编程]]></category>
		<category><![CDATA[函数签名]]></category>
		<category><![CDATA[函数类型]]></category>
		<category><![CDATA[函数重载]]></category>
		<category><![CDATA[加法]]></category>
		<category><![CDATA[动态值]]></category>
		<category><![CDATA[动态类型]]></category>
		<category><![CDATA[参数多态]]></category>
		<category><![CDATA[反射]]></category>
		<category><![CDATA[变体]]></category>
		<category><![CDATA[变量]]></category>
		<category><![CDATA[可维护性]]></category>
		<category><![CDATA[可读性]]></category>
		<category><![CDATA[可辨识联合]]></category>
		<category><![CDATA[可预测]]></category>
		<category><![CDATA[名义类型]]></category>
		<category><![CDATA[告别懵圈]]></category>
		<category><![CDATA[和类型]]></category>
		<category><![CDATA[复杂性]]></category>
		<category><![CDATA[复杂类型]]></category>
		<category><![CDATA[多态]]></category>
		<category><![CDATA[多种形态]]></category>
		<category><![CDATA[多返回值]]></category>
		<category><![CDATA[子类型]]></category>
		<category><![CDATA[子类型多态]]></category>
		<category><![CDATA[字段]]></category>
		<category><![CDATA[学术殿堂]]></category>
		<category><![CDATA[实战派Gopher]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[封闭性]]></category>
		<category><![CDATA[局限性]]></category>
		<category><![CDATA[工具支持]]></category>
		<category><![CDATA[工程实用性]]></category>
		<category><![CDATA[工程实践]]></category>
		<category><![CDATA[工程师]]></category>
		<category><![CDATA[工程权衡]]></category>
		<category><![CDATA[底层结构]]></category>
		<category><![CDATA[开发者]]></category>
		<category><![CDATA[开放性]]></category>
		<category><![CDATA[弱类型]]></category>
		<category><![CDATA[强类型]]></category>
		<category><![CDATA[形状]]></category>
		<category><![CDATA[心智负担]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[意图保证]]></category>
		<category><![CDATA[意图明确性]]></category>
		<category><![CDATA[技术提案]]></category>
		<category><![CDATA[技术沟通]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[抽象术语]]></category>
		<category><![CDATA[接口设计]]></category>
		<category><![CDATA[数组类型]]></category>
		<category><![CDATA[方法]]></category>
		<category><![CDATA[早期错误发现]]></category>
		<category><![CDATA[易于维护]]></category>
		<category><![CDATA[显式优于隐式]]></category>
		<category><![CDATA[显式转换]]></category>
		<category><![CDATA[更好]]></category>
		<category><![CDATA[模式匹配]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[泛型系统]]></category>
		<category><![CDATA[灵活性]]></category>
		<category><![CDATA[特质]]></category>
		<category><![CDATA[理论价值]]></category>
		<category><![CDATA[理论边界]]></category>
		<category><![CDATA[私有方法]]></category>
		<category><![CDATA[积类型]]></category>
		<category><![CDATA[稳健]]></category>
		<category><![CDATA[策略模式]]></category>
		<category><![CDATA[简洁性]]></category>
		<category><![CDATA[类型依赖于值]]></category>
		<category><![CDATA[类型兼容]]></category>
		<category><![CDATA[类型参数]]></category>
		<category><![CDATA[类型名称]]></category>
		<category><![CDATA[类型安全]]></category>
		<category><![CDATA[类型属性]]></category>
		<category><![CDATA[类型断言]]></category>
		<category><![CDATA[类型构造器]]></category>
		<category><![CDATA[类型检查]]></category>
		<category><![CDATA[类型理论]]></category>
		<category><![CDATA[类型理论入门]]></category>
		<category><![CDATA[类型类]]></category>
		<category><![CDATA[类型系统]]></category>
		<category><![CDATA[类型组合]]></category>
		<category><![CDATA[类型选择]]></category>
		<category><![CDATA[结构]]></category>
		<category><![CDATA[结构化子类型]]></category>
		<category><![CDATA[结构类型]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[编译时]]></category>
		<category><![CDATA[编译时常量]]></category>
		<category><![CDATA[编译时类型安全]]></category>
		<category><![CDATA[编译时类型检查]]></category>
		<category><![CDATA[编译错误]]></category>
		<category><![CDATA[翻译成果]]></category>
		<category><![CDATA[自动类型转换]]></category>
		<category><![CDATA[表达力]]></category>
		<category><![CDATA[表达式]]></category>
		<category><![CDATA[解释器]]></category>
		<category><![CDATA[计算机科学]]></category>
		<category><![CDATA[设计决策]]></category>
		<category><![CDATA[设计权衡]]></category>
		<category><![CDATA[软件工程]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[运行时信息]]></category>
		<category><![CDATA[逻辑错误]]></category>
		<category><![CDATA[重载]]></category>
		<category><![CDATA[隐式转换]]></category>
		<category><![CDATA[静态]]></category>
		<category><![CDATA[静态类型]]></category>
		<category><![CDATA[非侵入式]]></category>
		<category><![CDATA[非侵入式多态]]></category>
		<category><![CDATA[非法状态]]></category>
		<category><![CDATA[非结构化数据]]></category>
		<category><![CDATA[预防错误]]></category>
		<category><![CDATA[高度解耦]]></category>
		<category><![CDATA[高阶类型]]></category>
		<category><![CDATA[鸭子类型]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5329</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/30/type-theory-intro-for-gopher 大家好，我是Tony Bai。 你是否曾有过这样的经历：在浏览一个关于 Go 泛型或接口设计的 GitHub issue 或技术提案时，评论区里的大佬们突然开始讨论 “Sum Type”、“Product Type”、“Parametric Polymorphism” 或是 “Higher-Kinded Types”。一瞬间，你感觉自己仿佛闯入了一个学术研讨会，这些看似熟悉又陌生的词汇让你一头雾水，只想默默关掉页面。 作为一名务实的 Gopher，我们习惯于用具体的代码和设计模式来思考问题。我们关心的是接口的解耦能力、struct 的组合性、goroutine 的并发效率。这些学院派的类型理论术语，似乎离我们的日常工作很遥远。 然而，事实并非如此。这些术语并非象牙塔里的空谈，它们是计算机科学家们经过几十年沉淀，用来精确描述和分类编程语言核心特性的“通用语言”。理解它们，就像给一位经验丰富的工匠配上了一套精准的图纸和测量工具。它能让你： 更深刻地理解 Go 的设计哲学：为什么 Go 的接口如此强大？为什么 Go 1.18之前 长期以来没有泛型？为什么 int 和 int32 不能直接相加？这些背后都有类型理论的影子。 更清晰地沟通技术方案：当你能用“Product Type”来描述 struct，用“Sum Type”的思想来解释接口的用途时，你的技术沟通会变得更加精确和高效。 看懂高阶的技术讨论：无论是 Go 语言的未来演进，还是与其他语言（如 Rust, Haskell, Scala）的对比，这些术语都是绕不开的基石。 本文的灵感来源于阅读Simon Thompson教授所著《Type Theory &#38; Functional Programming》一书时的感悟，但我们的目标并非成为类型理论的研究者。恰恰相反，我们的目标是做一个“翻译者”，将这些核心的理论概念，用我们最熟悉的 Go 语言特性和代码示例进行“转码”，彻底拉通学术殿堂与工程实践之间的鸿沟。 准备好了吗？让我们一起告别懵圈，开启这段实战派 Gopher [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/type-theory-intro-for-gopher-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/30/type-theory-intro-for-gopher">本文永久链接</a> &#8211; https://tonybai.com/2025/10/30/type-theory-intro-for-gopher</p>
<p>大家好，我是Tony Bai。</p>
<p>你是否曾有过这样的经历：在浏览一个关于 Go 泛型或接口设计的 GitHub issue 或技术提案时，评论区里的大佬们突然开始讨论 “Sum Type”、“Product Type”、“Parametric Polymorphism” 或是 “Higher-Kinded Types”。一瞬间，你感觉自己仿佛闯入了一个学术研讨会，这些看似熟悉又陌生的词汇让你一头雾水，只想默默关掉页面。</p>
<p>作为一名务实的 Gopher，我们习惯于用具体的代码和设计模式来思考问题。我们关心的是接口的解耦能力、struct 的组合性、goroutine 的并发效率。这些学院派的类型理论术语，似乎离我们的日常工作很遥远。</p>
<p>然而，事实并非如此。这些术语并非象牙塔里的空谈，它们是计算机科学家们经过几十年沉淀，用来精确描述和分类编程语言核心特性的“通用语言”。理解它们，就像给一位经验丰富的工匠配上了一套精准的图纸和测量工具。它能让你：</p>
<ol>
<li><strong>更深刻地理解 Go 的设计哲学</strong>：为什么 Go 的接口如此强大？为什么 Go 1.18之前 长期以来没有泛型？为什么 int 和 int32 不能直接相加？这些背后都有类型理论的影子。</li>
<li><strong>更清晰地沟通技术方案</strong>：当你能用“Product Type”来描述 struct，用“Sum Type”的思想来解释接口的用途时，你的技术沟通会变得更加精确和高效。</li>
<li><strong>看懂高阶的技术讨论</strong>：无论是 Go 语言的未来演进，还是与其他语言（如 Rust, Haskell, Scala）的对比，这些术语都是绕不开的基石。</li>
</ol>
<p>本文的灵感来源于阅读<a href="https://www.kent.ac.uk/school-of-computing/people/3164/thompson-simon">Simon Thompson教授</a>所著《<a href="https://www.cs.kent.ac.uk/people/staff/sjt/TTFP/">Type Theory &amp; Functional Programming</a>》一书时的感悟，但我们的目标并非成为类型理论的研究者。恰恰相反，我们的目标是做一个“翻译者”，将这些核心的理论概念，用我们最熟悉的 Go 语言特性和代码示例进行“转码”，彻底拉通学术殿堂与工程实践之间的鸿沟。</p>
<p>准备好了吗？让我们一起告别懵圈，开启这段实战派 Gopher 的类型理论入门之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/the-ultimate-guide-to-go-module-qr.png" alt="" /></p>
<h2>地基与框架 —— 到底什么是“类型系统”？</h2>
<p>在深入具体的类型之前，我们首先需要建立一个宏观的框架。一个编程语言的<strong>类型系统 (Type System)</strong>，从学术角度来说，是一套规则集合，它为程序中的每个值（value）、变量（variable）和表达式（expression）都关联一个“类型”属性。</p>
<p>它的核心目的非常单纯且强大：<strong>在程序造成危害（比如运行时崩溃）之前，通过检查类型的合法性来预防错误</strong>。正如 Go 的领军人物 Rob Pike 所言：<strong>类型系统旨在“让非法的状态无法表示”</strong>。</p>
<p>为了系统性地理解它，我们可以从以下几个关键维度来对其进行分类和审视。</p>
<h3>类型检查的时机：编译时 vs. 运行时 (Static vs. Dynamic)</h3>
<p>这是对类型系统最基本、最重要的划分。</p>
<h4>静态类型 (Statically Typed)</h4>
<p><strong>定义</strong>：类型检查在<strong>编译时</strong>完成。编译器会像一位严谨的图书管理员，在程序运行前，通读你的全部代码，检查每一个变量的赋值、每一次函数调用，确保类型在所有地方都严格匹配。如果发现问题，程序将无法通过编译。</p>
<p><strong>优点</strong>：<br />
*   <strong>早期错误发现</strong>：绝大多数类型相关的 bug 在开发阶段就被扼杀在摇篮里。<br />
*   <strong>更高的性能</strong>：编译器确切地知道每个变量的类型和内存布局，可以生成高度优化的机器码。运行时无需再花费时间去检查类型。<br />
*   <strong>更好的工具支持和可维护性</strong>：类型本身就是最可靠的文档。IDE 能提供精准的自动补全、代码导航和安全的重构。</p>
<p><strong>Go 是一门不折不扣的静态类型语言。</strong> 它的编译器是你的第一道防线。</p>
<pre><code class="go">package main

func main() {
    var i int
    // 下面这行代码会导致编译失败，而不是运行时错误
    i = "hello"
}

// go build -&gt; ./main.go:6:4: cannot use "hello" (type untyped string) as type int in assignment
</code></pre>
<h4>动态类型 (Dynamically Typed)</h4>
<p><strong>定义</strong>：类型检查发生在<strong>运行时</strong>。变量本身没有固定的类型，它可以随时指向任何类型的值。只有当代码执行到某一行，需要对一个值进行特定操作时，解释器才会检查这个值的类型是否支持该操作。</p>
<p><strong>代表语言</strong>：Python, JavaScript, Ruby。</p>
<p><strong>Go 中的“动态”一面</strong>：虽然 Go 语言本身是静态的，但它通过 interface{} (自 Go 1.18 起的别名 any) 提供了一种强大的机制来处理不确定的类型，这在行为上<strong>模拟了动态类型</strong>的灵活性。</p>
<p>一个接口值可以看作一个“箱子”，它包含了两部分信息：值的动态类型（dynamic type）和动态值（dynamic value）。</p>
<pre><code class="go">package main
import "fmt"

func main() {
    // data 的静态类型是 any，它可以持有任何类型的值
    var data any

    data = "hello, world" // 编译通过，data 的动态类型是 string
    printValue(data)

    data = 42 // 编译通过，data 的动态类型是 int
    printValue(data)

    data = true // 编译通过，data 的动态类型是 bool
    printValue(data)
}

func printValue(v any) {
    // 使用类型断言(type assertion)或类型选择(type switch)在运行时检查动态类型
    switch val := v.(type) {
    case string:
        fmt.Printf("It's a string: %s\n", val)
    case int:
        fmt.Printf("It's an integer: %d\n", val)
    default:
        fmt.Printf("It's some other type: %T\n", val)
    }
}
</code></pre>
<p>这种机制是 Go 实现通用数据结构和处理 JSON 等非结构化数据的基石，但代价是放弃了部分编译时的类型安全，并将检查推迟到了运行时。</p>
<h3>类型的严格程度：强类型 vs. 弱类型 (Strong vs. Weak)</h3>
<p>这个维度的划分标准在学术界略有争议，但通常用来<strong>描述一门语言对于不同类型间隐式转换的容忍度</strong>。</p>
<h4>强类型 (Strongly Typed)</h4>
<p><strong>定义</strong>：语言严格限制不同类型之间的隐式转换。当一个操作需要特定类型时，你必须提供该类型的值。如果类型不匹配，要么编译失败，要么运行时报错，语言本身不会“自作主张”地进行不安全的转换。</p>
<p><strong>Go 的类型系统是出了名的“强硬”</strong>。</p>
<pre><code class="go">package main

import "strconv"

func main() {
    var a int = 10
    var b float64 = 5.5

    // 编译错误：不同数值类型之间不能直接运算
    // c := a + b // invalid operation: a + b (mismatched types int and float64)

    // 必须进行显式类型转换
    c := float64(a) + b // 正确

    var i int32 = 100
    var j int64 = 200

    // 即使是不同位数的整型，也必须显式转换
    // k := i + j // invalid operation: i + j (mismatched types int32 and int64)
}
</code></pre>
<p>这种严格性杜绝了许多在 C/C++ 或 JavaScript 中常见的、因隐式转换导致的难以察觉的 bug，让代码行为更加可预测。</p>
<h4>弱类型 (Weakly Typed)</h4>
<p><strong>定义</strong>：语言倾向于在操作中自动进行类型转换，以“尽力”让程序继续运行。</p>
<p><strong>代表语言</strong>：JavaScript 是典型代表，&#8217;5&#8242; + 1 会得到字符串 &#8217;51&#8242;，而 &#8217;5&#8242; &#8211; 1 会得到数字 4。这种灵活性有时很方便，但也是 bug 的温床。</p>
<h3>类型的等价性判断：名义类型 vs. 结构类型 (Nominal vs. Structural)</h3>
<p>这是判断“类型 A 和类型 B 是否相同（或兼容）”的规则，也是理解 Go 接口的关键。</p>
<h4>名义类型 (Nominal Typing)</h4>
<p><strong>定义</strong>：类型是否等价，取决于它们的<strong>名称</strong>。即使两个类型拥有完全相同的底层结构和字段，只要它们的类型名称不同，它们就是两个完全不同的、不兼容的类型。</p>
<p><strong>Go 的核心类型（structs, named basic types）遵循名义类型系统。</strong></p>
<pre><code class="go">package main
import "fmt"

type UserID int
type ProductID int

type Point struct {
    X, Y int
}

type Vector struct {
    X, Y int
}

func main() {
    var uid UserID = 123
    var pid ProductID = 123

    // 编译错误：尽管底层都是 int，但类型名称不同
    // if uid == pid { ... } // invalid operation: uid == pid (mismatched types UserID and ProductID)

    p := Point{1, 2}
    v := Vector{1, 2}

    // 编译错误：尽管结构完全相同，但类型名称不同
    // if p == v { ... } // invalid operation: p == v (mismatched types Point and Vector)
}
</code></pre>
<p>名义类型提供了非常强的意图保证。UserID 就是 UserID，它承载的业务含义与 ProductID 完全不同，编译器强制你区分它们，从而避免了将用户 ID 误用为产品 ID 的逻辑错误。</p>
<h4>结构类型 (Structural Typing)</h4>
<p><strong>定义</strong>：类型是否兼容，取决于它们的<strong>结构</strong>或“形状”（它们有哪些字段、哪些方法）。只要结构满足要求，类型就是兼容的，这与它们的名称无关。这通常被称为“<strong>鸭子类型</strong>”（Duck Typing）——“如果它走起来像鸭子，叫起来也像鸭子，那么它就是一只鸭子。”</p>
<p><strong>Go 的体现</strong>：<strong>Go 的 interface 系统是纯粹的结构类型系统。</strong></p>
<pre><code class="go">package main
import "fmt"

// 定义一个“会叫的”接口
type Quacker interface {
    Quack() string
}

// Duck 类型，它有一个 Quack 方法
type Duck struct{}
func (d Duck) Quack() string {
    return "Quack!"
}

// Person 类型，它也有一个 Quack 方法
type Person struct{}
func (p Person) Quack() string {
    return "I'm quacking like a duck!"
}

// 这个函数只关心传入的值是否满足 Quacker 接口的“结构”
func MakeItQuack(q Quacker) {
    fmt.Println(q.Quack())
}

func main() {
    var d Duck
    var p Person

    // Duck 和 Person 都没有显式声明 "implements Quacker"
    // 但因为它们都有 Quack() string 方法，所以它们都满足 Quacker 接口
    MakeItQuack(d) // 输出: Quack!
    MakeItQuack(p) // 输出: I'm quacking like a duck!
}
</code></pre>
<p>Go 的这一设计堪称神来之笔：<strong>在一个整体为名义类型的静态语言中，通过接口开辟了一块结构类型的区域，从而在不牺牲类型安全的前提下，获得了动态语言般的灵活性和强大的解耦能力。</strong> 你可以在不修改第三方库代码的情况下，让自己的类型去实现它的接口。</p>
<h3>Go 类型系统的定位</h3>
<p>综合以上维度，我们可以给 Go 的类型系统下一个精准的定义：</p>
<p>Go 是一门<strong>静态、强类型</strong>的语言。它主要采用<strong>名义类型系统</strong>来保证代码的严谨性和意图明确性，同时通过<strong>接口</strong>这一特性，创造性地引入了<strong>结构类型系统</strong>，以实现灵活、非侵入式的多态。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/type-theory-intro-for-gopher-2.png" alt="" /></p>
<p>现在，我们已经搭建好了理解类型系统的宏观框架。接下来，让我们深入到类型的“原子世界”，看看那些让 Gopher 们“懵圈”的术语，在 Go 中究竟是什么模样。</p>
<h2>类型的“和”与“积” —— Go 世界的 Sum &amp; Product Type</h2>
<p>在类型理论中，最基本的两种类型组合方式是“积”与“和”。它们就像算术中的乘法和加法，是构建更复杂类型的基础。</p>
<h3>Product Type (积类型)：A and B</h3>
<p><strong>学术定义</strong>：一个<strong>积类型</strong>（Product Type）的值由多个其他类型的值<strong>同时</strong>组成。如果一个类型 P 是类型 A 和类型 B 的积类型，那么 P 的一个值会同时包含一个 A 类型的值<strong>和</strong>一个 B 类型的值。</p>
<p>这听起来很熟悉，对吗？</p>
<p><strong>Go 的实现：struct</strong></p>
<p>struct 是 Go 对积类型的直接且完美的实现。</p>
<pre><code class="go">// Person 类型是 string 和 int 的积类型
type Person struct {
    Name string // 包含一个 string
    Age  int    // 和一个 int
}

// p1 这个值同时持有一个 string "Alice" 和一个 int 30
var p1 Person = Person{Name: "Alice", Age: 30}
</code></pre>
<p>学术上，积类型最简单的形式是<strong>元组 (Tuple)</strong>，例如 (string, int)。Go 不支持原生的元组语法，但 struct 在功能上是更强大的、带命名字段的元组。你甚至可以通过多返回值来模拟元组的使用：</p>
<pre><code class="go">func getPerson() (string, int) {
    return "Bob", 42
}

// name 和 age 在这里就像一个临时的元组
name, age := getPerson()
</code></pre>
<p>所以，下次当你在讨论中听到 <strong>Product Type</strong>，你就可以自信地在脑海里将它替换为：<strong>“哦，就是 struct 这种东西。”</strong></p>
<h3>Sum Type (和类型)：A or B</h3>
<p><strong>学术定义</strong>：一个<strong>和类型</strong>（Sum Type），也叫<strong>可辨识联合 (Discriminated Union)</strong> 或<strong>变体 (Variant)</strong>，它的值在任意时刻只能是几种可能性中的<strong>一种</strong>。如果一个类型 S 是类型 A 和类型 B 的和类型，那么 S 的一个值要么是一个 A 类型的值，<strong>要么</strong>是一个 B 类型的值，绝不可能同时是两者。</p>
<p>很多现代语言，如 Rust、Swift、Haskell，都有原生语法来支持和类型：</p>
<pre><code class="rust">// Rust 中的 enum 就是一个和类型
enum Result&lt;T, E&gt; {
    Ok(T),    // 要么是成功，里面包含一个 T 类型的值
    Err(E),   // 要么是失败，里面包含一个 E 类型的值
}
</code></pre>
<p>Go 语言没有提供上述那样的原生和类型语法。这是 Go 设计者在语言复杂性上做出的一个明确权衡。但是，Go 开发者每天都在使用和类型的思想，只是我们用的是另一种工具——<strong>接口</strong>。</p>
<p>一个接口类型定义了一个方法的集合。任何实现了这些方法的类型，都可以被看作是这个接口类型集合中的一员。因此，一个接口类型的变量，可以持有任何一个满足其要求的具体类型的值。这正是“A <strong>或</strong> B <strong>或</strong> C&#8230;”的核心思想。</p>
<p>让我们用一个经典的例子来具象化这个概念：一个图形应用需要处理不同的形状。</p>
<pre><code class="go">package main
import "math"

// Shape 接口定义了一个“和类型”，它可以是任何能计算面积的东西。
// 它可以是 Circle，或者是 Rectangle，或者是未来我们定义的任何其他形状。
type Shape interface {
    Area() float64
}

// --- 可能性 1: Circle ---
type Circle struct {
    Radius float64
}
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// --- 可能性 2: Rectangle ---
type Rectangle struct {
    Width, Height float64
}
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 这个函数接受一个 Shape 类型的值。
// 它不关心这个值到底是 Circle 还是 Rectangle，只关心它能调用 Area() 方法。
func PrintArea(s Shape) {
    // 这时，变量 s 的值可能是 Circle 或 Rectangle 之一
    fmt.Printf("Area of %T is %0.2f\n", s, s.Area())
}

func main() {
    c := Circle{Radius: 5}
    r := Rectangle{Width: 4, Height: 3}

    PrintArea(c) // 输出: Area of main.Circle is 78.54
    PrintArea(r) // 输出: Area of main.Rectangle is 12.00
}
</code></pre>
<p>在这个例子里，Shape 接口扮演了和类型的角色。一个 Shape 变量的值，在任何时刻，要么是一个 Circle，要么是一个 Rectangle。</p>
<p><strong>如何“辨识”具体的类型？—— type switch</strong></p>
<p>和类型的一个关键特性是“可辨识”（Discriminated）。这意味着我们必须有办法知道当前的值到底是哪个具体的类型。在 Go 中，我们使用 type switch 来实现这一点。</p>
<pre><code class="go">func PrintShapeDetails(s Shape) {
    fmt.Printf("Shape details for %T:\n", s)
    switch shape := s.(type) {
    case Circle:
        // 在这个 case 分支里，编译器知道 shape 的类型是 Circle
        fmt.Printf("  It's a circle with radius %.2f\n", shape.Radius)
    case Rectangle:
        // 在这个 case 分支里，编译器知道 shape 的类型是 Rectangle
        fmt.Printf("  It's a rectangle with width %.2f and height %.2f\n", shape.Width, shape.Height)
    default:
        fmt.Println("  It's an unknown shape.")
    }
}
</code></pre>
<p>type switch 是处理和类型值时的“模式匹配”，它安全地拆开接口这个“箱子”，并根据里面的动态类型执行相应的逻辑。</p>
<p><strong>模拟的代价：开放性与编译时检查的缺失</strong></p>
<p>Go 的接口模拟与原生和类型有一个本质区别：<strong>接口是开放的，而原生和类型通常是封闭的</strong>。</p>
<ul>
<li><strong>封闭性 (Sealed/Closed)</strong>：在 Rust 的例子中，Result只能是 Ok(T)中的T 或 Err(E)中的E，编译器知道所有可能性。如果你在 match（类似 switch）时漏掉了一种情况，编译器会报错。</li>
<li><strong>开放性 (Open)</strong>：在 Go 的例子中，任何包、任何地方都可以定义一个新的类型（比如 Triangle），只要它实现了 Area() 方法，它就可以被赋值给 Shape 变量。这意味着编译器永远无法保证你的 type switch 处理了所有情况，因此 default 分支变得至关重要。</li>
</ul>
<p>为了在 Go 中模拟一个更“封闭”的和类型，有时会使用一种技巧：在接口中定义一个私有方法。</p>
<pre><code class="go">type Shape interface {
    Area() float64
    isShape() // 私有方法
}
</code></pre>
<p>由于私有方法 isShape 只能在同一个包内被实现，这实际上就将 Shape 接口的实现者限制在了当前包内，从而模拟了一个封闭的和类型。这在 Go 标准库中（例如 net/url.go 中的 addr 接口）时有应用。</p>
<p>所以，下次当你看到 <strong>Sum Type</strong> 这个术语，你的脑海中应该浮现出这样的映射：</p>
<blockquote>
<p><strong>“哦，这是指一个值在多个类型中‘非此即彼’的概念。Go 没有原生支持它，但我们通过 interface 和 type switch 的组合，在工程实践中出色地模拟了它的核心思想。”</strong></p>
</blockquote>
<h2>抽象的力量 —— Go 中的函数与多态</h2>
<p>类型系统不仅用于组合数据，更强大的能力在于抽象行为。这主要涉及到函数类型和多态。</p>
<h3>函数类型 (Function Types)</h3>
<p><strong>学术定义</strong>：从类型 A 到类型 B 的一个映射，记作 A -> B。在函数式编程和类型理论中，函数本身就是一种可以被传递、存储和返回的值，即“一等公民”。</p>
<p><strong>Go 的实现</strong>：Go 完全支持一等公民函数。我们可以定义函数类型，这在 Go 代码中非常常见。</p>
<pre><code class="go">package main
import "fmt"

// 定义一个函数类型 Operator，它接受两个 int，返回一个 int
type Operator func(int, int) int

func add(a, b int) int {
    return a + b
}

func multiply(a, b int) int {
    return a * b
}

// calculate 函数接受一个 Operator 类型的函数作为参数
func calculate(a, b int, op Operator) {
    result := op(a, b)
    fmt.Printf("Result is: %d\n", result)
}

func main() {
    calculate(10, 5, add)      // 输出: Result is: 15
    calculate(10, 5, multiply) // 输出: Result is: 50
}
</code></pre>
<p>HTTP 中间件、策略模式等诸多设计模式在 Go 中都大量利用了函数类型。</p>
<h3>多态 (Polymorphism)</h3>
<p>“Polymorphism”源于希腊语，意为“多种形态”。在编程中，它指代<strong>一段代码可以处理不同类型的值</strong>的能力。类型理论通常将其分为几种。</p>
<h4>参数多态 (Parametric Polymorphism)</h4>
<p><strong>学术定义</strong>：编写的代码其逻辑对于操作的值的<strong>具体类型</strong>是通用的、不相关的。函数或数据结构可以被一个或多个类型<strong>参数化</strong>。例如，一个反转列表的函数，其逻辑（交换头尾元素）与列表里存的是整数、字符串还是用户自定义结构完全无关。</p>
<p><strong>Go 的实现：泛型 (Generics, Go 1.18+)</strong></p>
<p>在 Go 1.18 之前，Gopher 们只能通过 interface{} 和反射来模拟参数多态，但这牺牲了类型安全和性能。泛型的引入，为 Go 提供了实现参数多态的“正统”方式。</p>
<pre><code class="go">package main
import "fmt"

// 这个函数的逻辑对任何类型 T 都是一样的
// T 是一个类型参数
func Reverse[T any](s []T) {
    for i, j := 0, len(s)-1; i &lt; j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

func main() {
    intSlice := []int{1, 2, 3, 4}
    Reverse(intSlice)
    fmt.Println(intSlice) // 输出: [4 3 2 1]

    stringSlice := []string{"a", "b", "c"}
    Reverse(stringSlice)
    fmt.Println(stringSlice) // 输出: [c b a]
}
</code></pre>
<p>当你听到 <strong>Parametric Polymorphism</strong>，你就可以直接联想到 <strong>Go 的泛型</strong>。</p>
<h4>子类型多态 (Subtype Polymorphism)</h4>
<p><strong>学术定义</strong>：一个函数或操作可以作用于某个类型 T，同时也能作用于 T 的所有<strong>子类型</strong>。例如，一个处理 Animal 的函数，应该也能处理 Dog 和 Cat，因为 Dog 和 Cat 都是 Animal 的子类型。</p>
<p><strong>Go 的实现：接口 (Interfaces)</strong></p>
<p>我们又回到了接口！在 Go 的世界里，子类型的概念正是通过接口来实现的。如果类型 T 实现了接口 I，那么 T 就可以被看作是 I 的一个“子类型”。</p>
<p>更准确地说，Go 实现的是<strong>结构化子类型 (Structural Subtyping)</strong>。</p>
<pre><code class="go">package main
import (
    "bytes"
    "fmt"
    "io"
    "os"
)

// 这个函数接受任何满足 io.Reader 接口的类型
// os.File 是 io.Reader 的一个“子类型”
// bytes.Buffer 也是 io.Reader 的一个“子类型”
func ReadAndPrint(r io.Reader) {
    data, err := io.ReadAll(r)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))
}

func main() {
    // 从文件读取
    file, _ := os.Open("test.txt")
    defer file.Close()
    ReadAndPrint(file)

    // 从内存中的 buffer 读取
    buffer := bytes.NewBufferString("Hello from buffer!")
    ReadAndPrint(buffer)
}
</code></pre>
<p>ReadAndPrint 函数体现了子类型多态：它被编写用来处理 io.Reader 这一通用类型，但实际上它可以无缝处理 <em>os.File、</em>bytes.Buffer 以及任何其他未来可能出现的、满足 io.Reader 结构的类型。</p>
<h4>Ad-hoc 多态 (Ad-hoc Polymorphism)</h4>
<p><strong>学术定义</strong>：也称为<strong>重载 (Overloading)</strong>。同一个函数名可以有多个不同的实现，具体调用哪个实现取决于参数的类型。例如，add(int, int) 和 add(string, string) 是两个不同的函数。</p>
<p>Go <strong>不支持</strong>函数重载。Go 的哲学是“显式优于隐式”，函数签名（包括函数名、参数类型和返回值类型）是唯一的。</p>
<h2>理论的边界 —— Go 类型系统“做不到”的事</h2>
<p>理解一门语言，不仅要知道它能做什么，也要知道它的边界在哪里，以及为什么会有这些边界。这通常是设计者在“表达力”与“简洁性”之间做出权衡的结果。</p>
<h3>依赖类型 (Dependent Types)</h3>
<p><strong>学术定义</strong>：一种高级的类型系统特性，允许<strong>类型依赖于值</strong>。这意味着类型可以由程序中的常规变量来参数化。</p>
<p><strong>经典例子</strong>：定义一个“长度为 n 的向量”类型 Vector(n)。这样，Vector(3) 和 Vector(4) 就是两个完全不同的类型。编译器可以静态地保证你不会把一个长度为 3 的向量赋值给一个长度为 4 的向量变量，或者保证矩阵乘法的维度匹配。</p>
<pre><code>// 伪代码，Go 并不支持
func dotProduct(n: int, v1: Vector(n), v2: Vector(n)) -&gt; float64 {
    // ...
}

var vec3 Vector(3)
var vec4 Vector(4)
dotProduct(3, vec3, vec4) // 编译错误！vec4 的长度不是 3
</code></pre>
<p>Go完全不支持依赖类型。Go 的类型系统在编译时工作，而像 n 这样的值通常在运行时才知道。将运行时信息混入编译时类型检查会极大地增加语言和编译器的复杂性。Go 选择了简洁，将这类检查（如切片长度）的责任交给了程序员，通过 len() 函数和运行时 panic 来保障。</p>
<p>值得一提的是，Go 的数组类型 [N]T 具有依赖类型的“影子”。例如，[3]int 和 [4]int 是不同的类型，因为它们的类型定义依赖于值 3 和 4。但这并非真正的依赖类型，因为数组的长度 N 必须是一个编译时常量，而不能是一个运行时变量。这个限制正是 Go 的数组与依赖类型的本质区别，也是 Go 在追求更强类型安全与保持语言简洁性之间做出的一种工程权衡。</p>
<h3>高阶类型 (Higher-Kinded Types, HKTs)</h3>
<p>这是一个在函数式编程和高级类型系统讨论中频繁出现的术语，也是理解 Go 泛型设计边界的关键所在。乍一听可能有些吓人，但我们可以通过类比来轻松理解它。</p>
<p><strong>通俗解释：类型的“阶”</strong></p>
<p>想象一下我们熟悉的函数：</p>
<ul>
<li><strong>一阶函数</strong>：操作“值”。例如，func add(a, b int) int 接受 int 值，返回 int 值。</li>
<li><strong>高阶函数</strong>：操作“函数”。例如，func apply(f func(int) int, v int) int 接受一个函数 f 作为参数。</li>
</ul>
<p>现在，我们把这个概念“提升”到类型层面：</p>
<ul>
<li><strong>一阶类型 (或称普通类型)</strong>：就是一个具体的类型，比如 int, string, struct{}。在类型理论中，它们的“种类”(Kind) 被记为 *。</li>
<li>
<p><strong>高阶类型 (Higher-Kinded Types)</strong>：不是一个完整的类型，而是一个“类型的模板”或“类型构造器”(Type Constructor)。它接受一个或多个普通类型作为参数，然后“构造”出一个新的普通类型。</p>
<ul>
<li>[]T 就是一个类型构造器。[] 本身不是类型，你必须给它一个类型（如 int），才能得到一个完整的类型 []int。它的“种类”可以记为 * -> * (接受一个类型，返回一个类型)。</li>
<li>同理，map[K]V 也是一个类型构造器，它的“种类”是 * -> * -> * (接受两个类型，返回一个类型)。</li>
<li>chan T 也是 * -> *。</li>
</ul>
</li>
</ul>
<p><strong>高阶类型系统</strong>，就是指一门语言的泛型系统<strong>能够对类型构造器本身进行抽象</strong>的能力。换句话说，泛型参数不仅可以是 T（代表一个普通类型），还可以是 F（代表一个类型构造器，如 [] 或 chan）。</p>
<p><strong>Go 的现状：不支持高阶类型</strong></p>
<p>Go 的泛型系统被设计为只处理<strong>一阶类型</strong>。这意味着 Go 的类型参数 [T any] <strong>只能代表一个完整的类型</strong>。</p>
<ul>
<li>T 可以是 int。</li>
<li>T 也可以是 []int。</li>
<li>但 T <strong>不能</strong>是 [] 本身。</li>
</ul>
<p>让我们通过一个经典的 Map 函数的例子来具体说明这一点。我们的目标是写一个<strong>通用</strong>的 Map 函数，它能将一个容器里的所有元素通过一个函数进行转换，并返回一个包含新元素的<strong>同类容器</strong>。</p>
<p><strong>Go 能做到的：为每种容器编写独立的泛型函数</strong></p>
<p>由于 Go 不支持 HKTs，我们必须为 slice、channel 或其他任何我们想支持的容器类型，分别编写一个泛型 Map 函数。</p>
<pre><code class="go">// 为 slice 实现的 Map
func SliceMap[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

// 为 channel 实现的 Map (简化版)
func ChanMap[T, U any](ch &lt;-chan T, f func(T) U) &lt;-chan U {
    result := make(chan U)
    go func() {
        defer close(result)
        for v := range ch {
            result &lt;- f(v)
        }
    }()
    return result
}
</code></pre>
<p>注意，SliceMap 和 ChanMap 的核心逻辑思想是一致的，但因为容器的操作方式（创建、遍历、添加元素）不同，且 Go 无法抽象“容器”这个概念，我们不得不重复编写。</p>
<p><strong>Go 做不到的：一个统一所有容器的 Map 函数（伪代码）</strong></p>
<p>如果 Go 支持高阶类型，我们就可以梦想编写一个 UniversalMap 函数。下面的代码使用了 Go 的语法风格，但它在 Go 中是<strong>完全无法编译</strong>的，它仅仅是为了展示 HKTs 的思想。</p>
<pre><code class="go">// ----------------------------------------------------
// !! 警告：以下是 HKTs 思想的伪代码，无法在 Go 中编译 !!
// ----------------------------------------------------

// 这里的 type F[T] any 是一种虚构的语法，
// 意在声明“F 是一个接受单一类型参数的类型构造器”。
func UniversalMap[type F[T] any, T, U any](container F[T], f func(T) U) F[U] {
    // 这段函数体在 Go 中是无法实现的，因为：
    // 1. 如何创建一个 F[U] 类型的新容器？make(F[U]) 语法无效。
    // 2. 如何遍历一个抽象的 F[T] 容器？range 关键字只认识内置类型。
    // 3. 如何向 F[U] 中添加一个元素？是 append 还是 &lt;- 发送？

    panic("This is pseudo-code demonstrating what HKTs would enable.")
}

func main() {
    ints := []int{1, 2, 3}
    intChan := make(chan int)

    // 在一个支持 HKTs 的理想世界里，我们可以这样调用：
    // strings := UniversalMap(ints, func(i int) string { ... })      // 期望返回 []string
    // stringChan := UniversalMap(intChan, func(i int) string { ... }) // 期望返回 chan string
}
</code></pre>
<p>这段伪代码清晰地揭示了 Go 泛型的边界：</p>
<ol>
<li><strong>语法限制</strong>：Go 没有定义 [type F[T] any] 这样的语法来表示“一个类型构造器”作为类型参数。</li>
<li><strong>实现限制</strong>：即使语法允许，Go 缺乏一个通用的接口来描述“容器”的基本操作（如 map, flatMap 等）。支持 HKTs 的语言（如 Haskell, Scala）通常会提供一套名为 Functor, Monad 的“类型类”或“特质”(traits) 来定义这些通用操作，程序员可以为自己的容器类型（比如自定义的 Tree[T]）实现这些接口。</li>
</ol>
<p><strong>为什么 Go 选择不支持 HKTs？</strong></p>
<p>这是一个深思熟虑的设计决策。Go 语言的核心哲学之一是<strong>简洁性</strong>和<strong>可读性</strong>。高阶类型的概念虽然强大，但它引入了更高层次的抽象，极大地增加了语言的复杂性和程序员的心智负担。对于 Go 团队来说，为 slice 和 chan 等几种常见类型编写独立的泛型函数，这种适度的代码重复，相比于引入整个 HKTs 体系所带来的复杂性，是一个更值得接受的权衡。</p>
<p>所以，当你听到 <strong>Higher-Kinded Types</strong>，你可以这样理解：<strong>“它是一种更强大的泛型，可以对像 []T 中的 [] 这样的‘类型模板’本身进行参数化，但 Go 为了保持简洁而没有支持它。因此在 Go 中，我们需要为不同的容器类型（如 slice, channel）编写各自的泛型工具函数。”</strong></p>
<h2>小结：从“懵圈”到“通透”</h2>
<p>我们从令人困惑的 GitHub issue 讨论出发，踏上了一段连接类型理论与 Go 语言实践的旅程。现在，让我们回顾一下我们的“翻译”成果，将那些抽象的术语牢牢地锚定在 Go 的具体实现上：</p>
<ul>
<li>
<p><strong>类型系统框架</strong>：我们确立了 Go 的定位——一个<strong>静态、强类型</strong>的系统，它以<strong>名义类型</strong>为基础保证代码的严谨性，同时通过<strong>接口</strong>这一卓越设计，巧妙地融合了<strong>结构类型</strong>的灵活性。</p>
</li>
<li>
<p><strong>Product Type (积类型)</strong>：这个概念不再神秘，它就是我们日常工作中构建复合数据的基石——<strong>struct</strong>。</p>
</li>
<li>
<p><strong>Sum Type (和类型)</strong>：我们揭示了 Go 是如何通过<strong>接口</strong>和<strong>type switch</strong> 这一组合拳，优雅地模拟出和类型的核心思想（“A 或 B”）。我们最熟悉的 error 接口，便是这一思想在 Go 生态中最无处不在的体现。</p>
</li>
<li>
<p><strong>Parametric Polymorphism (参数多态)</strong>：我们看到，Go 1.18+ 的<strong>泛型</strong>为其提供了原生的、类型安全的支持，让我们得以编写出与具体类型无关的通用算法和数据结构。</p>
</li>
<li>
<p><strong>Subtype Polymorphism (子类型多态)</strong>：这再次指向了 <strong>Go 接口</strong>的强大之处。它基于<strong>结构化子类型</strong>，构建了一个非侵入式、高度解耦的多态模型，这是 Go 强大组合能力的核心源泉。</p>
</li>
<li>
<p><strong>理论的边界 (Dependent Types &amp; HKTs)</strong>：我们不仅理解了这些高级特性是什么，更重要的是，通过具体的伪代码示例，我们清晰地看到了 <strong>Go 泛型的局限性</strong>——它只能参数化完整的类型，而无法抽象<strong>类型构造器</strong>（如 [] 或 chan）。我们明白了，这些“做不到”并非语言的缺陷，而是 Go 团队在<strong>追求简洁性、可读性和工程实用性</strong>方面做出的深思熟虑的<strong>设计权衡</strong>。</p>
</li>
</ul>
<p>掌握这些术语，并不仅仅是为了在技术讨论中显得“专业”。更重要的是，它为我们提供了一个更深刻、更系统的视角来审视我们每天使用的工具。它解释了 Go 为什么是现在这个样子，它的优势在哪里，它的取舍又在哪里。</p>
<p>希望这篇文章能成为你工具箱里的一件利器。当你下一次再遇到那些“学院派”术语时，你将不再“懵圈”，而是能够会心一笑，轻松地将它们映射到你熟悉的 Go 世界中，从而更加自信地去创造、去构建、去解决实际的工程问题。</p>
<p>毕竟，对于实战派 Gopher 而言，任何理论的最终价值，都在于它能否帮助我们写出更好、更稳健、更易于维护的代码。</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><strong>想系统学习Go，构建扎实的知识体系？</strong></p>
<p>我的新书《<a href="https://book.douban.com/subject/37499496/">Go语言第一课</a>》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/10/30/type-theory-intro-for-gopher/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<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>成为那个拿锤子的人</title>
		<link>https://tonybai.com/2024/11/03/become-the-one-with-the-hammer/</link>
		<comments>https://tonybai.com/2024/11/03/become-the-one-with-the-hammer/#comments</comments>
		<pubDate>Sun, 03 Nov 2024 13:49:18 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[思考控]]></category>
		<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[Cybertruck]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GUI]]></category>
		<category><![CDATA[hammer]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[Mobile]]></category>
		<category><![CDATA[Model3]]></category>
		<category><![CDATA[ModelY]]></category>
		<category><![CDATA[nail]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Web]]></category>
		<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=4374</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/11/03/become-the-one-with-the-hammer “当你有一个锤子时，每件事看起来都像一个钉子”，这句来自心理学家亚伯拉罕·马斯洛(没错！就是提出五层需求理论的那个马斯洛)的名言揭示了人们在掌握一种技能或工具时，很容易将其视作通用解决方案的倾向，在技术领域，这种倾向尤为明显。 同时这句话也常被用来描述人们对工具的过度依赖和思维的局限性。 在程序员圈子中，“语言战争(programming language war)”是一个永不过时的话题，而马斯洛的“锤子”观点在每一种新语言兴起并掀起波澜时，总会被用作“讥讽”该语言拥趸的“思想武器”，细数当前的主流语言，莫不如此： 上世纪90年代初，随着图形用户界面(GUI)和大型软件开发而兴起的C++语言； 上世纪90年代末至2000年初期，随着互联网的普及和企业应用程序需求增加而大火儿的Java语言； 从2001年开始，特别是在微软.NET框架推出之后逐渐成为Windows应用开发新霸主的C#语言； 2004年以后，随着Ruby on Rails框架的推出，而在Web应用开发领域变成网红且语法优雅的Ruby语言； 2009年以后，刚发布就赢得TIOBE编程语言排行榜年度最佳语言，并在之后引领云原生时代的Go语言； 2010年诞生，从2016年开始连续8年霸榜Stackoverflow最受欢迎编程语言、打出“用Rust重写一切”的Rust语言。 这些语言都有自己的高光时刻，语言拥趸们举起大锤到处砸钉子，伴随而来的是来自其他语言阵营的讥讽。以我最熟悉的Go为例，在Go 1.5版本实现自举并实现GC延迟大幅度下降后，Go社区迎来了快速发展。Go也开始飘了！Gopher们乃至Go团队开始了在各个领域积(四)极(处)探(出)索(击)，除了云原生基础设施和服务、Cli和Web这几个主流领域之外，Go还进军了GUI、游戏、移动开发以及嵌入式系统等领域，这让Go语言一度也面临过与目前Rust相似的境遇和挑战，遭遇了一些质疑和嘲讽： 然而，这真的是一种糟糕的状态吗？手握大锤找钉子真的有错吗？让我们将视野从狭小的编程语言领域拓展到更广阔的其他领域。 我们先来看看汽车领域，如果说内燃机驱动技术和机械变速箱技术属于上一代成熟技术的话，那么基于锂电池和电动机的新能源驱动技术就是这个领域的“新锤子”，它也一直在被以丰田为代表的传统主机厂诟病。但以特斯拉为代表的的新能源车企是如何使用这柄锤子的呢？下面是特斯拉的产品发布历史： 2008年 Roadster：特斯拉的首款量产电动车 2012年 Model S：高档电动轿车，获得广泛好评，具有长续航和高性能，奠定了特斯拉在豪华车市场的地位。 2015年 Model X：一款豪华电动SUV，以独特的鹰翼门设计和高度的安全性著称。 2017年 Model 3：面向大众市场的紧凑型电动车，成为全球销量最高的电动车之一。 2020年 Model Y：一款电动跨界SUV，基于Model 3平台，迅速赢得市场。 2021年 Cybertruck：特斯拉的电动皮卡 2022年 Tesla Semi：电动重型卡车，专注于运输行业的可持续性。 哦，没错！就像编程界一样，一旦他们拿到这柄锤子，也会到处找钉子：从轿车、SUV、皮卡到电动重卡，甚至国内一些新能源主机厂已经发布了几款概念版电动飞行汽车： 我们再来看看四轴或多轴无人机领域，随着大疆等厂商拿到这把锤子后，无人机的应用范围得到了极大的拓展。从最初的航拍工具和玩具，逐渐演变为物流配送的利器，甚至展望未来，它们有可能成为飞行汽车的一部分。此外，一些军工企业也开始将无人机用于战场，成为一种武器。 如今，大语言模型正成为新时代的”锤子”，从自然语言处理到代码生成，从内容创作到自动驾驶决策辅助，从寻找新蛋白质到新药研发等，正在重塑各个领域的工作方式。 到这里，我们看到每一种新技术的诞生，都像一把新锤子，重塑着所在领域的版图。它们不是简单的工具替换，而是带来了全新的思维方式和解决方案。现在，你还担心拿着锤子找钉子会遭到他人的“讥讽”吗？ 在不断演变的科技世界，真正驱动变革的往往就是那些“拿锤子”的人。他们不只是拥有先进技术的工具，更重要的是，他们拥有通过这些工具改变世界的意愿。因此，找到并精通一项核心技术，就像获得了一把改变世界的锤子。这不是局限，而是机遇。重要的不是担心把所有问题都看成钉子，而是要深入理解你的”锤子”，保持开放的心态，发现新的应用场景，勇于尝试用锤子去创新性解决问题。 由此可见，本文开头处马斯洛的那句话在今天有了新的意义：成为那个拿锤子的人，意味着你有能力参与改变世界的进程。拥抱你手中的锤子吧，这是你的幸运！我们要做的就是善用这份幸运，创造更大的价值。 作为程序员，我们需要挑选一柄锤子并握在手中，而Go是一个很好的候选。如果你觉得拥抱Go这柄锤子，那我推荐大家关注极客时间的“Go语言第一课”，这是一个很好的起点，帮助你入门Go语言并深入理解Go语言的精髓。 同时，我的书籍《Go语言精进之路》也将为你提供更深入的知识和实用技巧。 让我们一起在这条道路上不断探索，提升自我，以更好地应对未来的挑战！ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！ 著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/become-the-one-with-the-hammer-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/11/03/become-the-one-with-the-hammer">本文永久链接</a> &#8211; https://tonybai.com/2024/11/03/become-the-one-with-the-hammer</p>
<p>“当你有一个锤子时，每件事看起来都像一个钉子”，这句来自心理学家亚伯拉罕·马斯洛(没错！就是提出五层需求理论的那个马斯洛)的名言揭示了人们在掌握一种技能或工具时，很容易将其视作通用解决方案的倾向，在技术领域，这种倾向尤为明显。</p>
<p><img src="https://tonybai.com/wp-content/uploads/become-the-one-with-the-hammer-2.png" alt="" /></p>
<p>同时这句话也常被用来描述人们对工具的过度依赖和思维的局限性。</p>
<p>在程序员圈子中，“<a href="https://www.microsoft.com/en-us/research/video/the-programming-language-wars/">语言战争(programming language war)</a>”是一个永不过时的话题，而马斯洛的“锤子”观点在每一种新语言兴起并掀起波澜时，总会被用作“讥讽”该语言拥趸的“思想武器”，细数当前的主流语言，莫不如此：</p>
<ul>
<li>上世纪90年代初，随着图形用户界面(GUI)和大型软件开发而兴起的C++语言；</li>
<li>上世纪90年代末至2000年初期，随着互联网的普及和企业应用程序需求增加而大火儿的Java语言；</li>
<li>从2001年开始，特别是在微软.NET框架推出之后逐渐成为Windows应用开发新霸主的C#语言；</li>
<li>2004年以后，随着Ruby on Rails框架的推出，而在Web应用开发领域变成网红且语法优雅的Ruby语言；</li>
<li>2009年以后，刚发布就赢得TIOBE编程语言排行榜年度最佳语言，并在之后引领<a href="https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/">云原生时代的Go语言</a>；</li>
<li>2010年诞生，从2016年开始连续8年霸榜Stackoverflow最受欢迎编程语言、打出“用Rust重写一切”的<a href="https://tonybai.com/tag/rust">Rust语言</a>。</li>
</ul>
<p>这些语言都有自己的高光时刻，语言拥趸们举起大锤到处砸钉子，伴随而来的是来自其他语言阵营的讥讽。以我最熟悉的Go为例，在<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5版本实现自举并实现GC延迟大幅度下降后</a>，Go社区迎来了快速发展。Go也开始飘了！Gopher们乃至Go团队开始了在各个领域积(四)极(处)探(出)索(击)，除了云原生基础设施和服务、Cli和Web这几个主流领域之外，Go还进军了GUI、游戏、<a href="https://github.com/golang/mobile">移动开发</a>以及嵌入式系统等领域，这让<strong>Go语言一度也面临过与目前Rust相似的境遇和挑战</strong>，遭遇了一些质疑和嘲讽：</p>
<p><img src="https://tonybai.com/wp-content/uploads/become-the-one-with-the-hammer-3.png" alt="" /></p>
<p>然而，这真的是一种糟糕的状态吗？<strong>手握大锤找钉子真的有错吗</strong>？让我们将视野从狭小的编程语言领域拓展到更广阔的其他领域。</p>
<p>我们先来看看汽车领域，如果说内燃机驱动技术和机械变速箱技术属于上一代成熟技术的话，那么基于锂电池和电动机的新能源驱动技术就是这个领域的“新锤子”，它也一直在被以丰田为代表的传统主机厂诟病。但以特斯拉为代表的的新能源车企是如何使用这柄锤子的呢？下面是特斯拉的产品发布历史：</p>
<ul>
<li>2008年 Roadster：特斯拉的首款量产电动车</li>
<li>2012年 Model S：高档电动轿车，获得广泛好评，具有长续航和高性能，奠定了特斯拉在豪华车市场的地位。</li>
<li>2015年 Model X：一款豪华电动SUV，以独特的鹰翼门设计和高度的安全性著称。</li>
<li>2017年 Model 3：面向大众市场的紧凑型电动车，成为全球销量最高的电动车之一。</li>
<li>2020年 Model Y：一款电动跨界SUV，基于Model 3平台，迅速赢得市场。</li>
<li>2021年 Cybertruck：特斯拉的电动皮卡</li>
<li>2022年 Tesla Semi：电动重型卡车，专注于运输行业的可持续性。</li>
</ul>
<p>哦，没错！就像编程界一样，一旦他们拿到这柄锤子，也会到处找钉子：从轿车、SUV、皮卡到电动重卡，甚至国内一些新能源主机厂已经发布了几款概念版电动飞行汽车：</p>
<p><img src="https://tonybai.com/wp-content/uploads/become-the-one-with-the-hammer-4.png" alt="" /></p>
<p>我们再来看看四轴或多轴无人机领域，随着大疆等厂商拿到这把锤子后，无人机的应用范围得到了极大的拓展。从最初的航拍工具和玩具，逐渐演变为物流配送的利器，甚至展望未来，它们有可能成为飞行汽车的一部分。此外，一些军工企业也开始将无人机用于战场，成为一种武器。</p>
<p>如今，大语言模型正成为新时代的”锤子”，从自然语言处理到代码生成，从内容创作到自动驾驶决策辅助，从寻找新蛋白质到新药研发等，正在重塑各个领域的工作方式。</p>
<p>到这里，我们看到<strong>每一种新技术的诞生，都像一把新锤子</strong>，重塑着所在领域的版图。它们不是简单的工具替换，而是带来了全新的思维方式和解决方案。现在，你还担心拿着锤子找钉子会遭到他人的“讥讽”吗？</p>
<p>在不断演变的科技世界，<strong>真正驱动变革的往往就是那些“拿锤子”的人</strong>。他们不只是拥有先进技术的工具，更重要的是，他们拥有通过这些工具改变世界的意愿。因此，找到并精通一项核心技术，就像获得了一把改变世界的锤子。这不是局限，而是机遇。重要的不是担心把所有问题都看成钉子，而是要深入理解你的”锤子”，保持开放的心态，发现新的应用场景，勇于尝试用锤子去创新性解决问题。</p>
<p>由此可见，本文开头处马斯洛的那句话在今天有了新的意义：<strong>成为那个拿锤子的人，意味着你有能力参与改变世界的进程。拥抱你手中的锤子吧，这是你的幸运！我们要做的就是善用这份幸运，创造更大的价值</strong>。</p>
<p>作为程序员，我们需要挑选一柄锤子并握在手中，而Go是一个很好的候选。如果你觉得拥抱Go这柄锤子，那我推荐大家关注极客时间的“<a href="http://gk.link/a/10AVZ">Go语言第一课</a>”，这是一个很好的起点，帮助你入门Go语言并深入理解Go语言的精髓。</p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /></p>
<p>同时，我的书籍《<a href="https://item.jd.com/13694000.html">Go语言精进之路</a>》也将为你提供更深入的知识和实用技巧。</p>
<p><img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p>让我们一起在这条道路上不断探索，提升自我，以更好地应对未来的挑战！</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/11/03/become-the-one-with-the-hammer/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>htmx：Gopher走向全栈的完美搭档？</title>
		<link>https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack/</link>
		<comments>https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack/#comments</comments>
		<pubDate>Fri, 20 Sep 2024 11:44:51 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Angular]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[DOM]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[HTMX]]></category>
		<category><![CDATA[httpserver]]></category>
		<category><![CDATA[hx-delete]]></category>
		<category><![CDATA[hx-get]]></category>
		<category><![CDATA[hx-post]]></category>
		<category><![CDATA[hx-put]]></category>
		<category><![CDATA[hx-swap]]></category>
		<category><![CDATA[hx-target]]></category>
		<category><![CDATA[hx-trigger]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JS]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[request]]></category>
		<category><![CDATA[response]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[SSE]]></category>
		<category><![CDATA[templ]]></category>
		<category><![CDATA[template]]></category>
		<category><![CDATA[TypeScript]]></category>
		<category><![CDATA[Vue]]></category>
		<category><![CDATA[websocket]]></category>
		<category><![CDATA[前端]]></category>
		<category><![CDATA[模板]]></category>
		<category><![CDATA[模板引擎]]></category>
		<category><![CDATA[浏览器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4288</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack 在传统的Web开发领域，前端和后端开发通常被明确划分。前端主要负责用户界面的交互和视觉呈现，运用HTML、CSS和JavaScript等技术；后端则专注于服务器逻辑、数据库管理和核心功能实现，常用Go、Java、PHP、Ruby等语言。 然而，随着技术的不断演进和开发流程的优化，全栈开发逐渐成为一种趋势。全栈开发者能够在项目的不同阶段灵活转换角色，有效降低沟通成本和缩短开发周期。他们对系统的整体架构和工作原理有更深入的理解，从而能更高效地解决问题。此外，全栈技能也使得开发者在就业市场上更具竞争力，能够承担更多样化的职责。 尽管如此，对于许多专注后端的工程师（包括众多Gopher）来说，前端开发仍然是一个不小的挑战。它不仅要求熟悉JavaScript等语言，还需要理解复杂的前端框架和工具链。这使得不少后端开发者在面对全栈开发时感到力不从心。 幸运的是，技术的进步为我们提供了更简单、高效的开发途径。Go语言以其简洁和高效著称，而htmx库则通过HTML属性实现丰富的前端交互。将两者结合，开发者可以在无需深入学习JavaScript的情况下，轻松实现全栈开发。这种组合不仅能够显著提升开发效率，还能充分利用服务器端渲染（SSR）的优势，在性能和用户体验方面取得显著提升。 那么，htmx是否真的是Gopher走向全栈的完美搭档呢？在本文中，我们就将探讨一下这个问题，介绍一下htmx的核心理念和工作原理，并结合代码示例和使用场景，详细分析Go和htmx如何协同工作。至于Go+htmx究竟有多能打，相信在本文最后，你会得出自己的评价！ 1. htmx：为简化前端开发而生 传统的前端开发通常依赖于JavaScript框架，例如React、Vue或Angular。这些框架虽然功能强大，但往往伴随着高昂的学习成本和复杂的开发流程。对于那些主要从事后端开发的程序员来说，学习和掌握这些框架不仅需要花费大量时间，还需要深入理解前端生态系统中的各种概念和工具链。这种学习曲线和开发复杂性成为了许多后端开发者的阻碍，同时也成为了阻碍Go开发者迈向全栈的绊脚石。 htmx的诞生正是为了简化前端开发，特别是对于那些不愿意或没有时间深入学习JavaScript的开发者。 htmx的核心理念是通过扩展HTML，使其具备更强大的功能，从而减少对JavaScript的依赖。它遵循了”HTML优先”的设计原则，允许开发者直接在HTML元素中添加特殊的属性来定义与服务器交互的行为，比如动态加载、表单处理、局部刷新等，从而实现动态交互，而无需编写任何JavaScript代码。可以说，htmx的出现为后端开发者(包括Gopher)提供了一种新的选择，使得Web应用的开发变得更加直观和简便。 不过，htmx自身却是一个轻量级的JavaScript库，这与Go的设计哲学有些“异曲同工”，即简单留给大家，复杂留给自己。作为js库，它提供了一组简洁而强大的API，通过设置HTML属性，开发者就可以实现多种交互功能。以下是htmx的一些核心特性： 请求类型（hx-get、hx-post、hx-put和hx-delete） 通过指定请求类型，htmx可以在用户触发事件时向服务器发送请求，并处理响应。 目标更新（hx-target） 支持指定服务器响应数据要插入的DOM元素，支持部分页面更新而无需刷新整个页面。 触发条件（hx-trigger） 支持定义请求触发的条件，例如点击、鼠标悬停、表单提交等事件。 交换方式（hx-swap） 支持定义响应内容插入DOM的方式，可以选择替换、插入、删除等操作。 这些API的设计目标是让开发者能够通过声明式的方式来实现前端逻辑，而不必依赖JavaScript代码，以简化开发过程。 由于几乎无需后端开发者写JavaScript，HTMX很容易被认为是SSR（服务器端渲染）的一种实现。它们看似很相似，但它们的思路并不完全一致。SSR的渲染过程是在服务器上完成的，服务器生成整个HTML页面的内容，并将其发送给客户端。客户端接收到完整的HTML直接展示给用户。这也使得SSR通常可以提供更快的初始加载体验，因为用户可以立即看到页面内容，而不必等待JavaScript加载和执行。此外，由于HTML内容在服务器上渲染，搜索引擎更容易抓取和索引内容。 而HTMX的大部分渲染也是在服务端完成的，但它支持在客户端通过AJAX请求动态更新页面的某些部分，而不需要重新加载整个页面，只是它是通过简单的HTML属性(外加自身js)实现这些功能的，而无需用户手工写JavaScript实现。HTMX还使得页面能够更具交互性，用户可以在不离开当前页面的情况下与应用程序进行交互。 因此，htmx可以视为一种结合SSR和局部CSR(客户端渲染)的技术，它让你通过服务器端渲染HTML，同时在客户端实现灵活的动态交互功能。这使得开发者能够在SSR提供的性能优势和SEO友好性基础上，提升用户体验而不必依赖完整的客户端框架。 虽然保留了CSR，但与传统的JavaScript框架（如 React、Vue、Angular）相比，htmx非常轻量，体积非常小，以撰写本文时的最新2.0.2版本htmx为例，它的js包大小如下，压缩版才10几k： 此外，传统框架虽然功能强大，但往往需要复杂的配置和较高的学习成本，尤其对于习惯后端开发的开发者来说，更是如此。而使用HTMX，只需掌握HTML和少量的htmx API即可开始开发，适合后端开发者快速上手。 说了这么多htmx的优点，那基于htmx的开发究竟是怎样的呢？下面我们就以htmx的几个核心特性为例，看看如何基于htmx开发简单web应用。 2. htmx的基本用法 在前面我们了解了htmx的几个核心特性，包括请求类型、目标更新等。下面我们就针对这些核心特性，举几个例子，大家初步了解一下基于htmx的开发web应用的流程。 我们先从请求类型开始，了解一下基于htmx如何向后端发起POST/GET/PUT/DELETE等请求。 2.1 示例1：请求类型 在这第一个示例中，我们使用Go语言创建一个简单的服务器，并使用htmx在前端实现不同类型的请求。下面是我们定义的html模板，其中包含了htmx的自定义属性： // go-htmx/demo1/index.html &#60;!DOCTYPE html&#62; &#60;html lang="en"&#62; &#60;head&#62; &#60;meta charset="UTF-8"&#62; &#60;meta name="viewport" content="width=device-width, initial-scale=1.0"&#62; &#60;title&#62;HTMX Go Example&#60;/title&#62; &#60;script [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack">本文永久链接</a> &#8211; https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack</p>
<p>在传统的Web开发领域，前端和后端开发通常被明确划分。前端主要负责用户界面的交互和视觉呈现，运用HTML、CSS和JavaScript等技术；后端则专注于服务器逻辑、数据库管理和核心功能实现，常用Go、Java、PHP、Ruby等语言。</p>
<p>然而，随着技术的不断演进和开发流程的优化，全栈开发逐渐成为一种趋势。全栈开发者能够在项目的不同阶段灵活转换角色，有效降低沟通成本和缩短开发周期。他们对系统的整体架构和工作原理有更深入的理解，从而能更高效地解决问题。此外，全栈技能也使得开发者在就业市场上更具竞争力，能够承担更多样化的职责。</p>
<p>尽管如此，对于许多专注后端的工程师（包括众多Gopher）来说，前端开发仍然是一个不小的挑战。它不仅要求熟悉JavaScript等语言，还需要理解复杂的前端框架和工具链。这使得不少后端开发者在面对全栈开发时感到力不从心。</p>
<p>幸运的是，技术的进步为我们提供了更简单、高效的开发途径。Go语言以其简洁和高效著称，而htmx库则通过HTML属性实现丰富的前端交互。将两者结合，开发者可以在无需深入学习JavaScript的情况下，轻松实现全栈开发。这种组合不仅能够显著提升开发效率，还能充分利用服务器端渲染（SSR）的优势，在性能和用户体验方面取得显著提升。</p>
<p>那么，htmx是否真的是Gopher走向全栈的完美搭档呢？在本文中，我们就将探讨一下这个问题，介绍一下htmx的核心理念和工作原理，并结合代码示例和使用场景，详细分析Go和htmx如何协同工作。至于Go+htmx究竟有多能打，相信在本文最后，你会得出自己的评价！</p>
<h2>1. htmx：为简化前端开发而生</h2>
<p>传统的前端开发通常依赖于JavaScript框架，例如React、Vue或Angular。这些框架虽然功能强大，但往往伴随着高昂的学习成本和复杂的开发流程。对于那些主要从事后端开发的程序员来说，学习和掌握这些框架不仅需要花费大量时间，还需要深入理解前端生态系统中的各种概念和工具链。这种学习曲线和开发复杂性成为了许多后端开发者的阻碍，同时也成为了阻碍Go开发者迈向全栈的绊脚石。</p>
<p>htmx的诞生正是为了简化前端开发，特别是对于那些不愿意或没有时间深入学习JavaScript的开发者。</p>
<p>htmx的核心理念是通过扩展HTML，使其具备更强大的功能，从而减少对JavaScript的依赖。它遵循了”HTML优先”的设计原则，允许开发者直接在HTML元素中添加特殊的属性来定义与服务器交互的行为，比如动态加载、表单处理、局部刷新等，从而实现动态交互，而无需编写任何JavaScript代码。可以说，htmx的出现为后端开发者(包括Gopher)提供了一种新的选择，使得Web应用的开发变得更加直观和简便。</p>
<p>不过，htmx自身却是一个轻量级的JavaScript库，这与Go的设计哲学有些“异曲同工”，即<strong>简单留给大家，复杂留给自己</strong>。作为js库，它提供了一组简洁而强大的API，通过设置HTML属性，开发者就可以实现多种交互功能。以下是htmx的一些核心特性：</p>
<ul>
<li>请求类型（hx-get、hx-post、hx-put和hx-delete）</li>
</ul>
<p>通过指定请求类型，htmx可以在用户触发事件时向服务器发送请求，并处理响应。</p>
<ul>
<li>目标更新（hx-target）</li>
</ul>
<p>支持指定服务器响应数据要插入的DOM元素，支持部分页面更新而无需刷新整个页面。</p>
<ul>
<li>触发条件（hx-trigger）</li>
</ul>
<p>支持定义请求触发的条件，例如点击、鼠标悬停、表单提交等事件。</p>
<ul>
<li>交换方式（hx-swap）</li>
</ul>
<p>支持定义响应内容插入DOM的方式，可以选择替换、插入、删除等操作。</p>
<p>这些API的设计目标是让开发者能够通过声明式的方式来实现前端逻辑，而不必依赖JavaScript代码，以简化开发过程。</p>
<p>由于几乎无需后端开发者写JavaScript，HTMX很容易被认为是<strong>SSR（服务器端渲染）</strong>的一种实现。它们看似很相似，但它们的思路并不完全一致。SSR的渲染过程是在服务器上完成的，服务器生成整个HTML页面的内容，并将其发送给客户端。客户端接收到完整的HTML直接展示给用户。这也使得SSR通常可以提供更快的初始加载体验，因为用户可以立即看到页面内容，而不必等待JavaScript加载和执行。此外，由于HTML内容在服务器上渲染，搜索引擎更容易抓取和索引内容。</p>
<p>而HTMX的大部分渲染也是在服务端完成的，但它支持在客户端通过AJAX请求动态更新页面的某些部分，而不需要重新加载整个页面，只是它是通过简单的HTML属性(外加自身js)实现这些功能的，而无需用户手工写JavaScript实现。HTMX还使得页面能够更具交互性，用户可以在不离开当前页面的情况下与应用程序进行交互。</p>
<p>因此，htmx可以视为一种结合SSR和<strong>局部CSR(客户端渲染)</strong>的技术，它让你通过服务器端渲染HTML，同时在客户端实现灵活的动态交互功能。这使得开发者能够在SSR提供的性能优势和SEO友好性基础上，提升用户体验而不必依赖完整的客户端框架。</p>
<p>虽然保留了CSR，但与传统的JavaScript框架（如 React、Vue、Angular）相比，htmx非常轻量，体积非常小，以撰写本文时的<a href="https://unpkg.com/browse/htmx.org@2.0.2/dist/">最新2.0.2版本htmx</a>为例，它的js包大小如下，压缩版才10几k：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-2.png" alt="" /></p>
<p>此外，传统框架虽然功能强大，但往往需要复杂的配置和较高的学习成本，尤其对于习惯后端开发的开发者来说，更是如此。而使用HTMX，只需掌握HTML和少量的htmx API即可开始开发，适合后端开发者快速上手。</p>
<p>说了这么多htmx的优点，那基于htmx的开发究竟是怎样的呢？下面我们就以htmx的几个核心特性为例，看看如何基于htmx开发简单web应用。</p>
<h2>2. htmx的基本用法</h2>
<p>在前面我们了解了htmx的几个核心特性，包括请求类型、目标更新等。下面我们就针对这些核心特性，举几个例子，大家初步了解一下基于htmx的开发web应用的流程。</p>
<p>我们先从请求类型开始，了解一下基于htmx如何向后端发起POST/GET/PUT/DELETE等请求。</p>
<h3>2.1 示例1：请求类型</h3>
<p>在这第一个示例中，我们使用Go语言创建一个简单的服务器，并使用htmx在前端实现不同类型的请求。下面是我们定义的html模板，其中包含了htmx的自定义属性：</p>
<pre><code>// go-htmx/demo1/index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX Go Example&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@2.0.2"&gt;&lt;/script&gt;
    &lt;style&gt;
        .row {
            margin-bottom: 10px;
        }
        button {
            width: 120px;
            margin-right: 10px;
        }
        .result {
            display: inline-block;
            width: 300px;
            border: 1px solid #ccc;
            padding: 5px;
            min-height: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;HTMX Request Types Demo&lt;/h1&gt;

    &lt;div class="row"&gt;
        &lt;button hx-get="/api/get" hx-target="#get-result"&gt;GET Request&lt;/button&gt;
        &lt;span id="get-result" class="result"&gt;&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class="row"&gt;
        &lt;button hx-post="/api/post" hx-target="#post-result"&gt;POST Request&lt;/button&gt;
        &lt;span id="post-result" class="result"&gt;&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class="row"&gt;
        &lt;button hx-put="/api/put" hx-target="#put-result"&gt;PUT Request&lt;/button&gt;
        &lt;span id="put-result" class="result"&gt;&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class="row"&gt;
        &lt;button hx-delete="/api/delete" hx-target="#delete-result"&gt;DELETE Request&lt;/button&gt;
        &lt;span id="delete-result" class="result"&gt;&lt;/span&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>在这个HTML模板文件中包含了四个按钮，每个按钮对应一种http请求类型（GET、POST、PUT、DELETE），具体的实现方式是每个按钮都使用了相应的htmx属性（hx-get、hx-post、hx-put、hx-delete）来指定请求类型和目标URL。此外，所有按钮都使用了hx-target来设置服务器的响应将被显示的元素id。以get请求button为例，响应的值将被放到id为get-result的span中。</p>
<p>对应的Go后端程序就非常简单了，下面是代码摘录：</p>
<pre><code>// go-htmx/demo1/main.go

package main

import (
    "fmt"
    "net/http"
    "os"
    "path/filepath"
)

func main() {
    http.HandleFunc("/", handleIndex)
    http.HandleFunc("/api/get", handleGet)
    http.HandleFunc("/api/post", handlePost)
    http.HandleFunc("/api/put", handlePut)
    http.HandleFunc("/api/delete", handleDelete)

    fmt.Println("Server is running on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    currentDir, _ := os.Getwd()
    filePath := filepath.Join(currentDir, "index.html")
    http.ServeFile(w, r, filePath)
}

func handleGet(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Received a GET request")
}

func handlePost(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Received a POST request")
}

func handlePut(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Received a PUT request")
}

func handleDelete(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Received a DELETE request")
}
</code></pre>
<p>运行该server后，用浏览器打开localhost:8080，我们将看到下面页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-3.png" alt="" /></p>
<p>逐一点击各个Button，htmx会将从服务器收到的响应内容放入对应的span中：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-4.png" alt="" /></p>
<h3>2.2 示例2：触发条件</h3>
<p>在这个示例2中，我们将基于htmx实现对各种触发条件的响应与处理，htmx提供了hx-trigger属性来应对这些不同的事件触发，包括点击、鼠标悬停和表单提交等。我们看下面html模板代码：</p>
<pre><code>// go-htmx/demo2/index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX Trigger Demo&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@2.0.2"&gt;&lt;/script&gt;
    &lt;style&gt;
        .demo-section {
            margin-bottom: 20px;
            padding: 10px;
            border: 1px solid #ccc;
        }
        .result {
            margin-top: 10px;
            padding: 5px;
            background-color: #f0f0f0;
            min-height: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;HTMX Trigger Demo&lt;/h1&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;Click Trigger&lt;/h2&gt;
        &lt;button hx-get="/api/click" hx-trigger="click" hx-target="#click-result"&gt;
            Click me
        &lt;/button&gt;
        &lt;div id="click-result" class="result"&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;Hover Trigger&lt;/h2&gt;
        &lt;div hx-get="/api/hover" hx-trigger="mouseenter" hx-target="#hover-result" style="display: inline-block; padding: 10px; background-color: #e0e0e0;"&gt;
            Hover over me
        &lt;/div&gt;
        &lt;div id="hover-result" class="result"&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;Form Submit Trigger&lt;/h2&gt;
        &lt;form hx-post="/api/submit" hx-trigger="submit" hx-target="#form-result"&gt;
            &lt;input type="text" name="message" placeholder="Enter a message"&gt;
            &lt;button type="submit"&gt;Submit&lt;/button&gt;
        &lt;/form&gt;
        &lt;div id="form-result" class="result"&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;Custom Delay Trigger&lt;/h2&gt;
        &lt;input type="text" name="search"
               hx-get="/api/search"
               hx-trigger="keyup changed delay:500ms"
               hx-target="#search-result"
               placeholder="Type to search..."&gt;
        &lt;div id="search-result" class="result"&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>通过模板代码，我们可以看到hx-trigger 的多种用法：</p>
<ul>
<li>点击触发（Click Trigger）：使用 hx-trigger=”click”，当按钮被点击时触发请求。</li>
<li>悬停触发（Hover Trigger）：使用 hx-trigger=”mouseenter”，当鼠标悬停在元素上时触发请求。</li>
<li>表单提交触发（Form Submit Trigger）：使用 hx-trigger=”submit”，当表单提交时触发请求。</li>
<li>自定义延迟触发（Custom Delay Trigger）：使用 hx-trigger=”keyup changed delay:500ms”，在输入框中输入时，等待500毫秒后触发请求。这对于实现搜索建议等功能很有用。</li>
</ul>
<p>下面是该示例的后端go代码，逻辑非常简单，针对每个事件调用，简单返回一个字符串：</p>
<pre><code>// go-htmx/demo2/main.go

... ...

func main() {
    http.HandleFunc("/", handleIndex)
    http.HandleFunc("/api/click", handleClick)
    http.HandleFunc("/api/hover", handleHover)
    http.HandleFunc("/api/submit", handleSubmit)
    http.HandleFunc("/api/search", handleSearch)

    fmt.Println("Server is running on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    currentDir, _ := os.Getwd()
    filePath := filepath.Join(currentDir, "index.html")
    http.ServeFile(w, r, filePath)
}

func handleClick(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Button was clicked!")
}

func handleHover(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "You hovered over the element!")
}

func handleSubmit(w http.ResponseWriter, r *http.Request) {
    message := r.FormValue("message")
    fmt.Fprintf(w, "Form submitted with message: %s", message)
}

func handleSearch(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("search")
    fmt.Fprintf(w, "Searching for: %s", query)
}
</code></pre>
<p>运行该server后，用浏览器打开localhost:8080，我们将看到下面页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-5.png" alt="" /></p>
<p>接下来，我们可以尝试点击按钮、悬停在元素上、提交表单和在搜索框中输入，看看每个操作如何触发HTMX 请求并更新页面的相应部分，下面是触发后的结果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-6.png" alt="" /></p>
<h3>2.3 示例3：交换方式</h3>
<p>在示例3中，我们将展示如何使用htmx的hx-swap属性实现不同的内容更新方式，包括替换、插入和删除操作，其中还包含多种替换方式。下面是html模板：</p>
<pre><code>// go-htmx/demo3/index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX Swap Demo - All Attributes&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@2.0.2"&gt;&lt;/script&gt;
    &lt;style&gt;
        .demo-section {
            margin-bottom: 20px;
            padding: 10px;
            border: 1px solid #ccc;
        }
        .content-box {
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #ddd;
            min-height: 50px;
        }
        .item {
            margin: 5px 0;
            padding: 5px;
            background-color: #f0f0f0;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;HTMX Swap Demo - All Attributes&lt;/h1&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;innerHTML (Default)&lt;/h2&gt;
        &lt;button hx-get="/api/swap/inner" hx-target="#inner-content"&gt;
            Swap innerHTML
        &lt;/button&gt;
        &lt;div id="inner-content" class="content-box"&gt;
            &lt;p&gt;This is the original content. The entire inner HTML will be replaced.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;outerHTML&lt;/h2&gt;
        &lt;button hx-get="/api/swap/outer" hx-target="#outer-content" hx-swap="outerHTML"&gt;
            Swap outerHTML
        &lt;/button&gt;
        &lt;div id="outer-content" class="content-box"&gt;
            &lt;p&gt;This entire div will be replaced, including its container.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;textContent&lt;/h2&gt;
        &lt;button hx-get="/api/swap/text" hx-target="#text-content" hx-swap="textContent"&gt;
            Swap textContent
        &lt;/button&gt;
        &lt;div id="text-content" class="content-box"&gt;
            &lt;p&gt;This &lt;strong&gt;text&lt;/strong&gt; will be replaced, but HTML tags will be treated as plain text.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;beforebegin&lt;/h2&gt;
        &lt;button hx-get="/api/swap/before" hx-target="#before-content" hx-swap="beforebegin"&gt;
            Insert before
        &lt;/button&gt;
        &lt;div id="before-content" class="content-box"&gt;
            &lt;p&gt;New content will be inserted before this div.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;afterbegin&lt;/h2&gt;
        &lt;button hx-get="/api/swap/afterbegin" hx-target="#afterbegin-content" hx-swap="afterbegin"&gt;
            Insert at beginning
        &lt;/button&gt;
        &lt;div id="afterbegin-content" class="content-box"&gt;
            &lt;p&gt;New content will be inserted at the beginning of this div, before this paragraph.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;beforeend&lt;/h2&gt;
        &lt;button hx-get="/api/swap/beforeend" hx-target="#beforeend-content" hx-swap="beforeend"&gt;
            Insert at end
        &lt;/button&gt;
        &lt;div id="beforeend-content" class="content-box"&gt;
            &lt;p&gt;New content will be inserted at the end of this div, after this paragraph.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;afterend&lt;/h2&gt;
        &lt;button hx-get="/api/swap/after" hx-target="#after-content" hx-swap="afterend"&gt;
            Insert after
        &lt;/button&gt;
        &lt;div id="after-content" class="content-box"&gt;
            &lt;p&gt;New content will be inserted after this div.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="demo-section"&gt;
        &lt;h2&gt;delete&lt;/h2&gt;
        &lt;button hx-get="/api/swap/delete" hx-target="#delete-content" hx-swap="delete"&gt;
            Delete content
        &lt;/button&gt;
        &lt;div id="delete-content" class="content-box"&gt;
            &lt;p&gt;This content will be deleted when the button is clicked.&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>这个示例略复杂，它涵盖了hx-swap的所有属性：</p>
<ul>
<li>innerHTML（默认）：替换目标元素的内部HTML。</li>
<li>outerHTML：用响应替换整个目标元素。</li>
<li>textContent：替换目标元素的文本内容，不解析HTML。</li>
<li>beforebegin：在目标元素之前插入响应。</li>
<li>afterbegin：在目标元素的第一个子元素之前插入响应。</li>
<li>beforeend：在目标元素的最后一个子元素之后插入响应。</li>
<li>afterend：在目标元素之后插入响应。</li>
<li>delete：删除目标元素，忽略响应内容。</li>
</ul>
<p>为了配合这个演示，我们编写了一个简单的go后端程序：</p>
<pre><code>// go-htmx/demo3/main.go
... ...

func main() {
    http.HandleFunc("/", handleIndex)
    http.HandleFunc("/api/swap/inner", handleInner)
    http.HandleFunc("/api/swap/outer", handleOuter)
    http.HandleFunc("/api/swap/text", handleText)
    http.HandleFunc("/api/swap/before", handleBefore)
    http.HandleFunc("/api/swap/afterbegin", handleAfterBegin)
    http.HandleFunc("/api/swap/beforeend", handleBeforeEnd)
    http.HandleFunc("/api/swap/after", handleAfter)
    http.HandleFunc("/api/swap/delete", handleDelete)

    fmt.Println("Server is running on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    currentDir, _ := os.Getwd()
    filePath := filepath.Join(currentDir, "index.html")
    http.ServeFile(w, r, filePath)
}

func handleInner(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "&lt;p&gt;This content replaced the inner HTML at %s&lt;/p&gt;", time.Now().Format(time.RFC1123))
}

func handleOuter(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "&lt;div id=\"outer-content\" class=\"content-box\"&gt;&lt;p&gt;This div replaced the entire outer HTML at %s&lt;/p&gt;&lt;/div&gt;", time.Now().Format(time.RFC1123))
}

func handleText(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This replaced the text content at %s. &lt;strong&gt;HTML tags&lt;/strong&gt; are not parsed.", time.Now().Format(time.RFC1123))
}

func handleBefore(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "&lt;p class=\"item\"&gt;This content was inserted before the target div at %s&lt;/p&gt;", time.Now().Format(time.RFC1123))
}

func handleAfterBegin(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "&lt;p class=\"item\"&gt;This content was inserted at the beginning of the target div at %s&lt;/p&gt;", time.Now().Format(time.RFC1123))
}

func handleBeforeEnd(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "&lt;p class=\"item\"&gt;This content was inserted at the end of the target div at %s&lt;/p&gt;", time.Now().Format(time.RFC1123))
}

func handleAfter(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "&lt;p class=\"item\"&gt;This content was inserted after the target div at %s&lt;/p&gt;", time.Now().Format(time.RFC1123))
}

func handleDelete(w http.ResponseWriter, r *http.Request) {
    // For delete, we don't need to send any content back
    w.WriteHeader(http.StatusOK)
}
</code></pre>
<p>运行该server后，用浏览器打开localhost:8080，你应该能看到一个包含八个不同部分的页面，每个部分演示了hx-swap的一种属性。你可以点击每个部分的按钮，观察内容如何以不同的方式更新或变化。这个综合示例展示了hx-swap的强大功能和灵活性，让你可以精确控制如何更新页面的不同部分。下面是你可以看到的效果呈现：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-7.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-8.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-9.png" alt="" /></p>
<p>以上就是htmx核心属性的用法，基于这些核心属性，我们可以实现更多更为复杂和高级的场景功能。在下一节，我们会举两个复杂一些的示例，供大家参考。</p>
<h2>3. 高级用法</h2>
<h3>3.1 基于token的身份认证</h3>
<p>在使用HTMX作为前端与后端进行交互时，通常会涉及到<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/">用户身份认证</a>及<a href="https://tonybai.com/2023/11/04/understand-go-web-authz-by-example">鉴权</a>，其中一个常见场景是通过前端获取的Token（如JWT）去访问后端的受保护的API。下面我们看看使用HTMX该如何实现这一常见功能。</p>
<p>下面是网站首页的html模板，包含用户登录的Form：</p>
<pre><code>// go-htmx/demo4/index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX Auth Example - Login&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@2.0.2"&gt;&lt;/script&gt;
    &lt;script&gt;
        htmx.on('htmx:afterRequest', function(event) {
            if (event.detail.elt.id === 'login-form') {
                var xhr = event.detail.xhr;
                if (xhr.status === 200) {
                    var response = JSON.parse(xhr.responseText);
                    if (response.success) {
                        localStorage.setItem('auth_token', response.token);
                        window.location.href = response.redirect;
                    } else {
                        document.getElementById('message').innerText = response.message;
                    }
                } else {
                    document.getElementById('message').innerText = "An error occurred. Please try again.";
                }
            }
        });
    &lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;HTMX Auth Example - Login&lt;/h1&gt;
    &lt;form id="login-form" hx-post="/login" hx-target="#message"&gt;
        &lt;label for="username"&gt;Username:&lt;/label&gt;
        &lt;input type="text" id="username" name="username" required&gt;&lt;br&gt;&lt;br&gt;
        &lt;label for="password"&gt;Password:&lt;/label&gt;
        &lt;input type="password" id="password" name="password" required&gt;&lt;br&gt;&lt;br&gt;
        &lt;button type="submit"&gt;Login&lt;/button&gt;
    &lt;/form&gt;
    &lt;div id="message"&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>这个代码片段结合了HTMX和JavaScript，处理登录表单的提交，以及登录成功后将令牌（Token）存储到浏览器的本地存储中，并在登录成功后重定向到dashboard页面。</p>
<p>这段代码监听了HTMX的htmx:afterRequest事件。此事件在HTMX请求完成（即请求已经发出并接收到响应）后触发，event.detail.elt表示触发事件的元素。代码检查该元素的id是否为login-form，确认这次请求来自登录表单。如果是其他表单或元素触发的请求，它将忽略。如果服务器的身份验证成功，它以json格式返回token和重定向地址，前端会解析响应，并将Token存储到本地存储，然后自动跳转到登录后的dashboard页面。</p>
<p>下面是dashboard页面的html模板：</p>
<pre><code>// go-htmx/demo4/dashboard.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX Auth Example - Dashboard&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@2.0.2"&gt;&lt;/script&gt;
    &lt;script&gt;
        document.addEventListener('DOMContentLoaded', function() {
            htmx.on('htmx:configRequest', function(event) {
                var token = localStorage.getItem('auth_token');
                if (token) {
                    event.detail.headers['Authorization'] = 'Bearer ' + token;
                }
            });
        });
    &lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Welcome to Your Dashboard&lt;/h1&gt;
    &lt;button hx-get="/protected" hx-target="#protected-content"&gt;Access Protected Content&lt;/button&gt;
    &lt;div id="protected-content"&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>这段代码最值得关注的地方就是在后续发出的Request中自动加入之前获取到的token。这里是使用了htmx:configRequest事件实现的。监听HTMX的htmx:configRequest事件，该事件在HTMX发出请求之前触发，它允许你修改即将发出的请求。这里的configRequest的处理逻辑是：如果Token存在，将它添加到即将发出的请求的Authorization头中，并格式化为标准的Bearer Token形式（即 “Authorization: Bearer your_token_here”）。这样，后端在处理请求时可以从请求头中提取出Token，用于验证用户身份。</p>
<p>整个示例的后端go程序如下：</p>
<pre><code>// go-htmx/demo4/main.go
package main

import (
    "encoding/json"
    "fmt"
    "html/template"
    "net/http"
    "strings"
    "sync"

    "github.com/google/uuid"
)

var (
    tokens   = make(map[string]bool)
    tokensMu sync.Mutex
)

type LoginResponse struct {
    Success  bool   `json:"success"`
    Token    string `json:"token,omitempty"`
    Message  string `json:"message"`
    Redirect string `json:"redirect,omitempty"`
}

func main() {
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/dashboard", dashboardHandler)
    http.HandleFunc("/protected", protectedHandler)
    fmt.Println("Server is running on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    http.ServeFile(w, r, "index.html")
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    username := r.FormValue("username")
    password := r.FormValue("password")

    response := LoginResponse{}

    if username == "admin" &amp;&amp; password == "password" {
        token := uuid.New().String()

        tokensMu.Lock()
        tokens[token] = true
        tokensMu.Unlock()

        response.Success = true
        response.Token = token
        response.Message = "Login successful"
        response.Redirect = "/dashboard"
    } else {
        response.Success = false
        response.Message = "Login failed. Please check your credentials and try again."
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func dashboardHandler(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("dashboard.html")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    tmpl.Execute(w, nil)
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
    authHeader := r.Header.Get("Authorization")
    if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    token := strings.TrimPrefix(authHeader, "Bearer ")

    tokensMu.Lock()
    valid := tokens[token]
    tokensMu.Unlock()

    if !valid {
        http.Error(w, "Invalid token", http.StatusUnauthorized)
        return
    }

    fmt.Fprintf(w, `&lt;div&gt;
        &lt;h2&gt;Protected Content&lt;/h2&gt;
        &lt;p&gt;This is sensitive information only for authenticated users.&lt;/p&gt;
        &lt;p&gt;Your token: %s&lt;/p&gt;
    &lt;/div&gt;`, token)
}
</code></pre>
<blockquote>
<p>注：这里仅是示例，因此只是用了一个uuid作为token，没有使用通用的jwt。</p>
</blockquote>
<p>运行程序，登录并在Dashboard中点击访问protected data，我们会看到下面图中呈现的效果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-10.png" alt="" /></p>
<p>下面我们再来看一个略复杂一些的示例，这次我们基于htmx来实现SSE(Server-Sent Event)，即服务端事件。</p>
<h3>3.2 SSE</h3>
<p>Server-Sent Events (SSE) 是一种轻量级的实时通信技术，允许服务器通过HTTP协议持续向客户端推送更新数据。与<a href="https://tonybai.com/2019/09/28/how-to-build-websockets-in-go">WebSocket</a>不同，SSE是单向通信，服务器可以推送数据到客户端，但客户端无法通过同一连接向服务器发送数据。这种机制非常适合需要频繁更新数据但对双向通信要求不高的场景，如股票价格、新闻推送、社交媒体通知等。</p>
<p>htmx对SSE的支持是通过扩展包实现的，下面就是本示例的index.html模板代码：</p>
<pre><code>// go-htmx/demo5/index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX SSE Notifications&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@1.9.6"&gt;&lt;/script&gt;
    &lt;script src="https://unpkg.com/htmx.org/dist/ext/sse.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;实时通知&lt;/h1&gt;
    &lt;div hx-ext="sse" sse-connect="/events" sse-swap="message"&gt;
        &lt;ul id="notifications"&gt;
            &lt;!-- 通知将在这里动态添加 --&gt;
        &lt;/ul&gt;
    &lt;/div&gt;

    &lt;script&gt;
        htmx.on("htmx:sseMessage", function(event) {
            var ul = document.getElementById("notifications");
            var li = document.createElement("li");
            li.innerHTML = event.detail.message;
            ul.insertBefore(li, ul.firstChild);
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>这个代码片段通过HTMX和Server-Sent Events (SSE) 实现了实时通知的功能。它会动态将服务器端发送的通知添加到页面的通知列表中。具体来说：</p>
<ul>
<li>hx-ext=”sse”：启用了HTMX的SSE扩展，用于处理 Server-Sent Events（服务器发送事件），使得浏览器可以保持与服务器的长连接，实时接收更新。</li>
<li>sse-connect=”/events”：指定了SSE连接的URL。浏览器会向/events这个路径发起SSE连接，服务器可以通过这个连接持续向客户端推送消息。</li>
<li>sse-swap=”message”：指示HTMX在收到SSE消息时触发事件处理，消息内容将使用JavaScript进行处理而不是自动更新HTML。</li>
<li>htmx.on(“htmx:sseMessage”, function(event))：监听HTMX的htmx:sseMessage事件，每当服务器通过SSE推送新消息时，该事件会触发。event.detail.message包含从服务器接收到的消息内容。</li>
<li>var ul = document.getElementById(“notifications”);：获取页面上ID为notifications的\&lt;ul&#62;元素，表示存放通知的容器。收到的通知通过htmx:sseMessage事件处理，将消息动态添加到通知列表中，并显示在网页上。</li>
</ul>
<p>下面是示例对应的Go后端程序：</p>
<pre><code>// go-htmx/demo5/main.go

func main() {
    http.HandleFunc("/", serveHTML)
    http.HandleFunc("/events", handleSSE)

    fmt.Println("Server starting on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func serveHTML(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "index.html")
}

func handleSSE(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
        return
    }

    notificationCount := 1

    for {
        notification := fmt.Sprintf("新通知 #%d: %s", notificationCount, time.Now().Format("15:04:05"))
        fmt.Fprintf(w, "data: &lt;li&gt;%s&lt;/li&gt;\n\n", notification)
        flusher.Flush()

        notificationCount++
        time.Sleep(3 * time.Second)

        if r.Context().Err() != nil {
            return
        }
    }
}
</code></pre>
<p>运行程序，打开浏览器访问localhost:8080，在加载的页面中会自动建立sse连接，页面上的通知消息区便会如下面这样每3秒一变化：</p>
<p><img src="https://tonybai.com/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-11.png" alt="" /></p>
<p>不过这个示例的程序有个“瑕疵”，那就是如果将htmx的版本从1.9.6换作最新的2.0.2，那么示例就将不工作了，翻看了一下htmx文档，应该是sseMessage这个htmx扩展属性被删除了。</p>
<p>如果要让示例更具通用性，可以将index.html换成下面的代码：</p>
<pre><code>// go-htmx/demo6/index.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;HTMX SSE Notifications&lt;/title&gt;
    &lt;script src="https://unpkg.com/htmx.org@2.0.2"&gt;&lt;/script&gt;
    &lt;style&gt;
        #notification {
            padding: 10px;
            border: 1px solid #ccc;
            background-color: #f8f8f8;
            margin-top: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;实时通知&lt;/h1&gt;
    &lt;div id="notification-container"&gt;
        &lt;div id="notification"&gt;等待通知...&lt;/div&gt;
    &lt;/div&gt;

    &lt;script&gt;
        document.body.addEventListener('htmx:load', function() {
            var notificationDiv = document.getElementById('notification');
            var evtSource = new EventSource("/events");

            evtSource.onmessage = function(event) {
                notificationDiv.textContent = event.data;
            };

            evtSource.onerror = function(err) {
                console.error("EventSource failed:", err);
            };
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>当然这个代码更多使用js来实现事件的处理。</p>
<h2>4. 小结</h2>
<p>本文探讨了Go与htmx这一全栈组合的简洁优势。对于后端开发者而言，这一组合提供了一种无需深入掌握前端技术即可开发现代Web应用的高效途径。</p>
<p>然而，从两个高级示例中可以看出，<strong>JavaScript代码仍难以完全避免</strong>，虽然数量不多，但在稍复杂的场景下依然不可或缺。</p>
<p>因此，htmx目前更多被中小型团队或个人开发者所青睐。这类开发者通常没有专职的前端人员，但希望快速构建并部署功能完善的Web应用。</p>
<p>综上所述，在我这个对前端开发了解甚少的Go开发者看来，Go与htmx的组合的确降低了开发门槛，同时提供了性能和SEO优势，使其成为现代Web开发中值得推荐的技术栈之一。不过，对于复杂的Web应用，开发者可能需要结合htmx和JavaScript，或更可能直接采用vue、react或angular等框架。</p>
<p>目前Go社区对htmx的支持也越来越多，比如<a href="https://templ.guide">html模板引擎templ</a>可以用于生成htmx模板，当然也有专有的htmx框架，比如：<a href="https://gitlab.com/go-htmx/go-htmx">ghtmx</a>、<a href="https://github.com/mikestefanello/pagoda">pagoda</a>、<a href="https://github.com/donseba/go-htmx">go-htmx</a>等。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-htmx">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://htmx.org/">htmx.org</a> &#8211; https://htmx.org/</li>
<li><a href="https://htmx.org/essays/htmx-sucks/">htmx sucks</a> &#8211; https://htmx.org/essays/htmx-sucks/</li>
<li><a href="https://hypermedia.systems/book/contents/">《HYPERMEDIA SYSTEMS》</a> &#8211; https://hypermedia.systems/book/contents/</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解API网关的主要功能特性</title>
		<link>https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example/</link>
		<comments>https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example/#comments</comments>
		<pubDate>Sun, 03 Dec 2023 09:35:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[37signals]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[Analytics]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[API-Gateway]]></category>
		<category><![CDATA[API-Managemnet]]></category>
		<category><![CDATA[APIDocumentation]]></category>
		<category><![CDATA[APIGateway]]></category>
		<category><![CDATA[APIGatewayPerformanceOptimization]]></category>
		<category><![CDATA[APIGatewayPlugins]]></category>
		<category><![CDATA[APIGatewayPolicies]]></category>
		<category><![CDATA[APIGovernance]]></category>
		<category><![CDATA[APIHealthChecks]]></category>
		<category><![CDATA[APILifecycleManagement]]></category>
		<category><![CDATA[APIMocking]]></category>
		<category><![CDATA[APIMonetization]]></category>
		<category><![CDATA[APIProxy]]></category>
		<category><![CDATA[APIRequestLogging]]></category>
		<category><![CDATA[APIRequestThrottling]]></category>
		<category><![CDATA[APIRequestTransformation]]></category>
		<category><![CDATA[APIRequestValidation]]></category>
		<category><![CDATA[APIResponseCaching]]></category>
		<category><![CDATA[APIResponseLogging]]></category>
		<category><![CDATA[APIResponseTransformation]]></category>
		<category><![CDATA[APIResponseValidation]]></category>
		<category><![CDATA[APISecurityScanning]]></category>
		<category><![CDATA[APITesting]]></category>
		<category><![CDATA[APITransformation]]></category>
		<category><![CDATA[APIVersioning]]></category>
		<category><![CDATA[API管理]]></category>
		<category><![CDATA[API网关]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[Caching]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[circuitbreaker]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[CORS(Cross-OriginResourceSharing)]]></category>
		<category><![CDATA[DeveloperPortal]]></category>
		<category><![CDATA[DHH]]></category>
		<category><![CDATA[ErrorHandling]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[JWT(JSONWebTokens)]]></category>
		<category><![CDATA[KONG]]></category>
		<category><![CDATA[lb]]></category>
		<category><![CDATA[LoadBalancing]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[monitoring]]></category>
		<category><![CDATA[OAuth]]></category>
		<category><![CDATA[OpenIDConnect]]></category>
		<category><![CDATA[RateLimiting]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[RequestRouting]]></category>
		<category><![CDATA[RESTfulAPIs]]></category>
		<category><![CDATA[ROR]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[RubyOnRail]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[serverless]]></category>
		<category><![CDATA[ServiceComposition]]></category>
		<category><![CDATA[ServiceDiscovery]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[SOA]]></category>
		<category><![CDATA[SSL/TLS]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[TrafficManagement]]></category>
		<category><![CDATA[Transformation]]></category>
		<category><![CDATA[tyk]]></category>
		<category><![CDATA[WAF(WebApplicationFirewall)]]></category>
		<category><![CDATA[yum]]></category>
		<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=4060</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example 在当今的技术领域中，“下云”的概念正逐渐抬头，像David Heinemeier Hansson(37signals公司的联合创始人, Ruby on Rails的Creator)就直接将公司所有的业务都从公有云搬迁到了自建的数据中心中。虽说大多数企业不会这么“极端”，但随着企业对云原生架构采用的广泛与深入，不可避免地面临着对云服务的依赖。云服务在过去的几年中被广泛应用于构建灵活、可扩展的应用程序和基础设施，为企业提供了许多便利和创新机会。然而，随着业务规模的增长和数据量的增加，云服务的成本也随之上升。企业开始意识到，对云服务的依赖已经成为一个值得重新评估的议题。云服务的开销可能占据了企业可用的预算的相当大部分。为了保持竞争力并更好地控制成本，企业需要寻找方法来减少对云服务的依赖，寻找更经济的解决方案，同时确保仍能获得所需的性能、安全性和可扩展性。 在这样的背景下，我们的关注点是选择一款适宜的API网关，从主流功能特性的角度来评估候选者的支持。API网关作为现代云原生应用架构中的关键组件，扮演着连接前端应用和后端服务的中间层，负责管理、控制和保护API的访问。它的功能特性对于确保API的安全性、可靠性和可扩展性至关重要。 尽管API网关并不是一个新鲜事物了，但对于那些长期依赖于云供应商的服务的人来说，它似乎变得有些“陌生”。因此，本文旨在帮助我们重新理解API网关的主要特性，并获得对API网关选型的能力，以便在停止使用云供应商服务之前，找到一个合适的替代品^_^。 1. API网关回顾 API网关是现代应用架构中的关键组件之一，它的存在简化了应用程序的架构，并为客户端提供一个单一的访问入口，并进行相关的控制、优化和管理。API网关可以帮助企业实现微服务架构、提高系统的可扩展性和安全性，并提供更好的开发者体验和用户体验。 1.1 API网关的演化 随着互联网的快速发展和企业对API的需求不断增长，API网关作为一种关键的中间层技术逐渐崭露头角并经历了一系列的演进和发展。这里将API网关的演进历史粗略分为以下几个阶段: API网关之前的早期阶段 在互联网发展的早期阶段，大多数应用程序都是以单体应用的形式存在。后来随着应用规模的扩大和业务复杂性的增加，单体应用的架构变得不够灵活和可扩展，面向服务架构（Service-Oriented Architecture，SOA）逐渐兴起，企业开始将应用程序拆分成一组独立的服务。这个时期，每个服务都是独立对外暴露API，客户端也是通过这些API直接访问服务，但这会导致一些安全性、运维和扩展性的问题。之后，企业也开始意识到需要一种中间层来管理和控制这种客户端到服务的通信行为，并确保服务的可靠性和安全性，于是开始有了API网关的概念。 API网关的兴起 早期的API网关，其主要功能就是单纯的路由和转发。API网关将请求从客户端转发到后端服务，并将后端服务的响应返回给客户端。在这个阶段，API网关的功能非常简单，主要用于解决客户端和后端服务之间的通信问题。 API网关的成熟 随着微服务架构的兴起和API应用的不断发展，企业开始将应用程序进一步拆分成更小的、独立部署的微服务。每个对外暴露的微服务都有自己的API，并通过API网关进行统一管理和访问。API网关在微服务架构中的作用变得更加重要，它的功能也逐渐丰富起来了。 在这一阶段，它不仅负责路由和转发请求，API网关还增加了安全和治理的功能，可以满足几个不同领域的微服务需求。比如：API网关可以通过身份认证、授权、访问控制等功能来保护API的安全；通过基于重试、超时、熔断的容错机制等来对API的访问进行治理；通过日志记录、基于指标收集以及Tracing等对API的访问进行观测与监控；支持实时的服务发现等。 API网关(图来自网络) API网关的云原生化 随着云原生技术的发展，如容器化和服务网格（Service Mesh）等，API网关也在不断演进和适应新的环境。在云原生环境中，API网关实现了与容器编排系统（如Kubernetes）和服务网格集成，其自身也可以作为一个云原生服务来部署，以实现更高的可伸缩性、弹性和自动化。同时，新的技术和标准也不断涌现，如GraphQL和gRPC等，API网关也增加了对这些新技术的集成和支持。 1.2 API网关的主要功能特性 从上面的演化历史我们看到：API网关的演进使其从最初简单的请求转发角色，逐渐成为整个API管理和微服务架构中的关键组件。它不仅扮演着API管理层与后端服务层之间的适配器，也是云原生架构中不可或缺的基础设施，使微服务管理更加智能化和自动化。下面是现代API网关承担的主要功能特性，我们后续也会基于这些特性进行示例说明： 请求转发和路由 身份认证和授权 流量控制和限速 高可用与容错处理 监控和可观测性 2. 那些主流的API网关 下面是来自CNCF Landscape中的主流API网关集合(截至2023.11月)，图中展示了关于各个网关的一些细节，包括star数量和背后开发的公司或组织： 主流的API网关还有各大公有云提供商的实现，比如：Amazon的API Gateway、Google Cloud的API Gateway以及上图中的Azure API Management等，但它们不在我们选择范围之内；虽然被CNCF收录，但多数API网关受到的关注并不高，超过1k star的不到30%，这些不是很受关注或dev不是那么active的项目也无法在生产环境担当关键角色；而像APISIX、Kong这两个受关注很高的网关，它们是建构在Nginx之上实现的，技术栈与我们不契合；而像EMISSARY INGRESS、Gloo等则是完全云原生化或者说是Kubernetes Native的，无法在无Kubernetes的基于VM或裸金属的环境下部署和运行。 好吧，剩下的只有几个Go实现的API Gateway了，在它们之中，我们选择用Tyk API网关来作为后续API功能演示的示例。 注：这并不代表Tyk API网关就要比其他Go实现的API [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example">本文永久链接</a> &#8211; https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example</p>
<p>在当今的技术领域中，“下云”的概念正逐渐抬头，像<a href="https://dhh.dk/">David Heinemeier Hansson</a>(37signals公司的联合创始人, Ruby on Rails的Creator)就直接<a href="https://37signals.com/podcast/leaving-the-cloud/">将公司所有的业务都从公有云搬迁到了自建的数据中心</a>中。虽说大多数企业不会这么“极端”，但随着企业对云原生架构采用的广泛与深入，不可避免地面临着对云服务的依赖。云服务在过去的几年中被广泛应用于构建灵活、可扩展的应用程序和基础设施，为企业提供了许多便利和创新机会。然而，随着业务规模的增长和数据量的增加，云服务的成本也随之上升。企业开始意识到，对云服务的依赖已经成为一个值得重新评估的议题。云服务的开销可能占据了企业可用的预算的相当大部分。为了保持竞争力并更好地控制成本，企业需要寻找方法来减少对云服务的依赖，寻找更经济的解决方案，同时确保仍能获得所需的性能、安全性和可扩展性。</p>
<p>在这样的背景下，我们的关注点是选择一款适宜的API网关，从主流功能特性的角度来评估候选者的支持。API网关作为现代云原生应用架构中的关键组件，扮演着连接前端应用和后端服务的中间层，负责管理、控制和保护API的访问。它的功能特性对于确保API的安全性、可靠性和可扩展性至关重要。</p>
<p>尽管API网关并不是一个新鲜事物了，但对于那些长期依赖于云供应商的服务的人来说，它似乎变得有些“陌生”。因此，本文旨在帮助我们重新理解API网关的主要特性，并获得对API网关选型的能力，以便在停止使用云供应商服务之前，找到一个合适的替代品^_^。</p>
<h2>1. API网关回顾</h2>
<p>API网关是现代应用架构中的关键组件之一，它的存在简化了应用程序的架构，并为客户端提供一个单一的访问入口，并进行相关的控制、优化和管理。API网关可以帮助企业实现微服务架构、提高系统的可扩展性和安全性，并提供更好的开发者体验和用户体验。</p>
<h2>1.1 API网关的演化</h2>
<p>随着互联网的快速发展和企业对API的需求不断增长，API网关作为一种关键的中间层技术逐渐崭露头角并经历了一系列的演进和发展。这里将API网关的演进历史粗略分为以下几个阶段:</p>
<ul>
<li>API网关之前的早期阶段</li>
</ul>
<p>在互联网发展的早期阶段，大多数应用程序都是<a href="https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices/">以单体应用的形式存在</a>。后来随着应用规模的扩大和业务复杂性的增加，单体应用的架构变得不够灵活和可扩展，面向服务架构（Service-Oriented Architecture，SOA）逐渐兴起，企业开始将应用程序拆分成一组独立的服务。这个时期，每个服务都是独立对外暴露API，客户端也是通过这些API直接访问服务，但这会导致一些安全性、运维和扩展性的问题。之后，企业也开始意识到需要一种中间层来管理和控制这种客户端到服务的通信行为，并确保服务的可靠性和安全性，于是开始有了API网关的概念。</p>
<ul>
<li>API网关的兴起</li>
</ul>
<p>早期的API网关，其主要功能就是单纯的路由和转发。API网关将请求从客户端转发到后端服务，并将后端服务的响应返回给客户端。在这个阶段，API网关的功能非常简单，主要用于解决客户端和后端服务之间的通信问题。</p>
<ul>
<li>API网关的成熟</li>
</ul>
<p>随着微服务架构的兴起和API应用的不断发展，企业开始将应用程序进一步拆分成更小的、独立部署的微服务。每个对外暴露的微服务都有自己的API，并通过API网关进行统一管理和访问。API网关在微服务架构中的作用变得更加重要，它的功能也逐渐丰富起来了。</p>
<p>在这一阶段，它不仅负责路由和转发请求，API网关还增加了安全和治理的功能，可以满足几个不同领域的微服务需求。比如：API网关可以通过身份认证、授权、访问控制等功能来保护API的安全；通过基于重试、超时、熔断的容错机制等来对API的访问进行治理；通过日志记录、基于指标收集以及Tracing等对API的访问进行观测与监控；支持实时的服务发现等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-3.png" alt="" /><br />
<center>API网关(图来自网络)</center></p>
<ul>
<li>API网关的云原生化</li>
</ul>
<p>随着云原生技术的发展，如容器化和服务网格（Service Mesh）等，API网关也在不断演进和适应新的环境。在云原生环境中，API网关实现了与容器编排系统（如Kubernetes）和服务网格集成，其自身也可以作为一个云原生服务来部署，以实现更高的可伸缩性、弹性和自动化。同时，新的技术和标准也不断涌现，如GraphQL和gRPC等，API网关也增加了对这些新技术的集成和支持。</p>
<h2>1.2 API网关的主要功能特性</h2>
<p>从上面的演化历史我们看到：API网关的演进使其从最初简单的请求转发角色，逐渐成为整个API管理和微服务架构中的关键组件。它不仅扮演着API管理层与后端服务层之间的适配器，也是云原生架构中不可或缺的基础设施，使微服务管理更加智能化和自动化。下面是现代API网关承担的主要功能特性，我们后续也会基于这些特性进行示例说明：</p>
<ul>
<li>请求转发和路由</li>
<li>身份认证和授权</li>
<li>流量控制和限速</li>
<li>高可用与容错处理</li>
<li>监控和可观测性</li>
</ul>
<h2>2. 那些主流的API网关</h2>
<p>下面是来自<a href="https://https://landscape.cncf.io">CNCF Landscape</a>中的主流API网关集合(截至2023.11月)，图中展示了关于各个网关的一些细节，包括star数量和背后开发的公司或组织：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-2.png" alt="" /></p>
<p>主流的API网关还有各大公有云提供商的实现，比如：<a href="https://aws.amazon.com/cn/api-gateway/">Amazon的API Gateway</a>、<a href="https://cloud.google.com/api-gateway">Google Cloud的API Gateway</a>以及上图中的Azure API Management等，但它们不在我们选择范围之内；虽然被CNCF收录，但多数API网关受到的关注并不高，超过1k star的不到30%，这些不是很受关注或dev不是那么active的项目也无法在生产环境担当关键角色；而像<a href="https://apisix.apache.org/">APISIX</a>、<a href="https://konghq.com/">Kong</a>这两个受关注很高的网关，它们是建构在Nginx之上实现的，技术栈与我们不契合；而像<a href="https://github.com/emissary-ingress/emissary">EMISSARY INGRESS</a>、Gloo等则是完全云原生化或者说是Kubernetes Native的，无法在无Kubernetes的基于VM或裸金属的环境下部署和运行。</p>
<p>好吧，剩下的只有几个Go实现的API Gateway了，在它们之中，我们选择用<a href="https://tyk.io/blog/res-api-management-vendor-comparisons/">Tyk API网关</a>来作为后续API功能演示的示例。</p>
<blockquote>
<p>注：这并<a href="https://tyk.io/blog/enter-the-leader-tyk-recognised-as-a-leader-in-gartners-2023-magic-quadrant-for-api-management/">不代表Tyk API网关就要比其他Go实现的API Gateway优秀</a>，只是它的资料比较齐全，适合在本文中作演示罢了。</p>
</blockquote>
<h2>3. API网关主要功能特性示例(Tyk API网关版本)</h2>
<h3>3.1 Tyk API网关简介</h3>
<p>记得在至少5年前就知道<a href="https://github.com/TykTechnologies/tyk">Tyk API网关</a>的存在，印象中它是使用Go语言开发的早期的那批API网关之一。Tyk从最初的纯开源项目，到如今由背后商业公司支持，以<a href="https://opensource.com/article/21/11/open-core-vs-open-source">Open Core模式开源</a>的网关，一直保持了active dev的状态。经过多年的演进，它已经一款功能强大的<a href="https://tyk.io/docs/tyk-oss-gateway/">开源兼商业API管理和网关解决方案</a>，提供了全面的功能和工具，帮助开发者有效地管理、保护和监控API。同时，Tyk API网关支持多种安装部署方式，即可以单一程序的方式放在物理机或VM上运行，也可以支持容器部署，通过<a href="https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose">docker-compose</a>拉起，亦可以通过<a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">Kubernetes Operator</a>将其部署在Kubernetes中，这也让Tyk API网关具备了在各大公有云上平滑迁移的能力。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-4.png" alt="" /></p>
<p>关于<a href="https://tyk.io/docs/tyk-oss-gateway/">Tyk API网关开源版本的功能详情</a>，可以点击左边超链接到其官网查阅，这里不赘述。</p>
<h3>3.2 安装Tyk API网关</h3>
<p>下面我们就来安装一下Tyk API网关，我们直接在VM上安装，VM上的环境是CentOS 7.9。Tyk API提供了很多中安装方法，这里<a href="https://tyk.io/docs/tyk-oss/ce-redhat-rhel-centos/">使用CentOS的yum包管理工具安装Tyk API网关</a>，大体步骤如下(演示均以root权限操作)。</p>
<h4>3.2.1 创建tyk gateway软件源</h4>
<p>默认的yum repo中是不包含tyk gateway的，我们需要在/etc/yum.repos.d下面创建一个新的源，即新建一个tyk_tyk-gateway.repo文件，其内容如下：</p>
<pre><code>[tyk_tyk-gateway]
name=tyk_tyk-gateway
baseurl=https://packagecloud.io/tyk/tyk-gateway/el/7/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/tyk/tyk-gateway/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300

[tyk_tyk-gateway-source]
name=tyk_tyk-gateway-source
baseurl=https://packagecloud.io/tyk/tyk-gateway/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/tyk/tyk-gateway/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
</code></pre>
<p>接下来我们执行下面命令来创建tyk_tyk-gateway这个repo的YUM缓存：</p>
<pre><code>$yum -q makecache -y --disablerepo='*' --enablerepo='tyk_tyk-gateway'
导入 GPG key 0x5FB83118:
 用户ID     : "https://packagecloud.io/tyk/tyk-gateway (https://packagecloud.io/docs#gpg_signing) &lt;support@packagecloud.io&gt;"
 指纹       : 9179 6215 a875 8c40 ab57 5f03 87be 71bd 5fb8 3118
 来自       : https://packagecloud.io/tyk/tyk-gateway/gpgkey
</code></pre>
<p>repo配置和缓存完毕后，我们就可以安装Tyk API Gateway了：</p>
<pre><code>$yum install -y tyk-gateway
</code></pre>
<p>安装后的tky-gateway将以一个<a href="https://tonybai.com/2016/12/27/when-docker-meets-systemd/">systemd daemon服务</a>的形式存在于主机上，程序意外退出或虚机重启后，该服务也会被systemd自动拉起。通过systemctl status命令可以查看服务的运行状态：</p>
<pre><code># systemctl status tyk-gateway
● tyk-gateway.service - Tyk API Gateway
   Loaded: loaded (/usr/lib/systemd/system/tyk-gateway.service; enabled; vendor preset: disabled)
   Active: active (running) since 日 2023-11-19 20:22:44 CST; 12min ago
 Main PID: 29306 (tyk)
    Tasks: 13
   Memory: 19.6M
   CGroup: /system.slice/tyk-gateway.service
           └─29306 /opt/tyk-gateway/tyk --conf /opt/tyk-gateway/tyk.conf

11月 19 20:34:54 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time="Nov 19 20:34:54" level=error msg="Connection to Redis faile...b-sub
11月 19 20:35:04 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time="Nov 19 20:35:04" level=error msg="cannot set key in pollerC...ured"
11月 19 20:35:04 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time="Nov 19 20:35:04" level=error msg="Redis health check failed...=main
Hint: Some lines were ellipsized, use -l to show in full.
</code></pre>
<h4>3.2.2 安装redis</h4>
<p>我们看到tyk-gateway已经成功启动，但从其服务日志来看，它在连接redis时报错了！tyk gateway默认将数据存储在redis中，为了让tyk gateway正常运行，我们还需要安装redis！这里我们使用容器的方式安装和运行一个redis服务：</p>
<pre><code>$docker pull redis:6.2.14-alpine3.18
$docker run -d --name my-redis -p 6379:6379 redis:6.2.14-alpine3.18
e5d1ec8d5f5c09023d1a4dd7d31d293b2d7147f1d9a01cff8eff077c93a9dab7
</code></pre>
<p>拉取并运行redis后，我们通过redis-cli验证一下与redis server的连接：</p>
<pre><code># docker run -it --rm redis:6.2.14-alpine3.18  redis-cli -h 192.168.0.24
192.168.0.24:6379&gt;
</code></pre>
<p>我们看到可以正常连接！但此时Tyk Gateway仍然无法与redis正常连接，我们还需要对Tyk Gateway做一些配置调整！</p>
<h4>3.2.3 配置Tyk Gateway</h4>
<p>yum默认将Tyk Gateway安装到/opt/tyk-gateway下面，这个路径下的文件布局如下：</p>
<pre><code>$tree -F -L 2 .
.
├── apps/
│   └── app_sample.json
├── coprocess/
│   ├── api.h
│   ├── bindings/
│   ├── coprocess_common.pb.go
│   ├── coprocess_mini_request_object.pb.go
│   ├── coprocess_object_grpc.pb.go
│   ├── coprocess_object.pb.go
│   ├── coprocess_response_object.pb.go
│   ├── coprocess_return_overrides.pb.go
│   ├── coprocess_session_state.pb.go
│   ├── coprocess_test.go
│   ├── dispatcher.go
│   ├── grpc/
│   ├── lua/
│   ├── proto/
│   ├── python/
│   └── README.md
├── event_handlers/
│   └── sample/
├── install/
│   ├── before_install.sh*
│   ├── data/
│   ├── init_local.sh
│   ├── inits/
│   ├── post_install.sh*
│   ├── post_remove.sh*
│   ├── post_trans.sh
│   └── setup.sh*
├── middleware/
│   ├── ottoAuthExample.js
│   ├── sampleMiddleware.js
│   ├── samplePostProcessMiddleware.js
│   ├── samplePreProcessMiddleware.js
│   ├── testPostVirtual.js
│   ├── testVirtual.js
│   └── waf.js
├── policies/
│   └── policies.json
├── templates/
│   ├── breaker_webhook.json
│   ├── default_webhook.json
│   ├── error.json
│   ├── monitor_template.json
│   └── playground/
├── tyk*
└── tyk.conf
</code></pre>
<p>其中tyk.conf就是tyk gateway的配置文件，我们先看看其默认的内容：</p>
<pre><code>$cat /opt/tyk-gateway/tyk.conf
{
  "listen_address": "",
  "listen_port": 8080,
  "secret": "xxxxxx",
  "template_path": "/opt/tyk-gateway/templates",
  "use_db_app_configs": false,
  "app_path": "/opt/tyk-gateway/apps",
  "middleware_path": "/opt/tyk-gateway/middleware",
  "storage": {
    "type": "redis",
    "host": "redis",
    "port": 6379,
    "username": "",
    "password": "",
    "database": 0,
    "optimisation_max_idle": 2000,
    "optimisation_max_active": 4000
  },
  "enable_analytics": false,
  "analytics_config": {
    "type": "",
    "ignored_ips": []
  },
  "dns_cache": {
    "enabled": false,
    "ttl": 3600,
    "check_interval": 60
  },
  "allow_master_keys": false,
  "policies": {
    "policy_source": "file"
  },
  "hash_keys": true,
  "hash_key_function": "murmur64",
  "suppress_redis_signal_reload": false,
  "force_global_session_lifetime": false,
  "max_idle_connections_per_host": 500
}
</code></pre>
<p>我们看到：storage下面存储了redis的配置信息，我们需要将redis的host配置修改为我们的VM地址：</p>
<pre><code>    "host": "192.168.0.24",
</code></pre>
<p>然后重启Tyk Gateway服务：</p>
<pre><code>$systemctl daemon-reload
$systemctl restart tyk-gateway
</code></pre>
<p>之后，我们再查看tyk gateway的运行状态：</p>
<pre><code>systemctl status tyk-gateway
● tyk-gateway.service - Tyk API Gateway
   Loaded: loaded (/usr/lib/systemd/system/tyk-gateway.service; enabled; vendor preset: disabled)
   Active: active (running) since 一 2023-11-20 06:54:07 CST; 41s ago
 Main PID: 20827 (tyk)
    Tasks: 15
   Memory: 24.8M
   CGroup: /system.slice/tyk-gateway.service
           └─20827 /opt/tyk-gateway/tyk --conf /opt/tyk-gateway/tyk.conf

11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Loading API configurations...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Tracking hostname" api_nam...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Initialising Tyk REST API ...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="API bind on custom port:0"...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Checking security policy: ...fault
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="API Loaded" api_id=1 api_n...ip=--
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Loading uptime tests..." p...k-mgr
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Initialised API Definition...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=warning msg="All APIs are protected ...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="API reload complete" prefix=main
Hint: Some lines were ellipsized, use -l to show in full.
</code></pre>
<p>从服务日志来看，现在Tyk Gateway可以正常连接redis并提供服务了！我们也可以通过下面的命令验证网关的运行状态：</p>
<pre><code>$curl localhost:8080/hello
{"status":"pass","version":"5.2.1","description":"Tyk GW","details":{"redis":{"status":"pass","componentType":"datastore","time":"2023-11-20T06:58:57+08:00"}}}
</code></pre>
<p>“/hello”是Tyk Gateway的内置路由，由Tyk网关自己提供服务。</p>
<p>到这里Tyk Gateway的安装和简单配置就结束了，接下来，我们就来看看API Gateway的主要功能特性，并借助Tyk Gateway来展示一下这些功能特性。</p>
<blockquote>
<p>注：查看Tyk Gateway的运行日志，可以使用journalctl -u tyk-gateway -f命令实时follow最新日志输出。</p>
</blockquote>
<h3>3.3 功能特性：请求转发与路由</h3>
<p>请求转发和路由是API Gateway的主要功能特性之一，API Gateway可以根据请求的路径、方法、查询参数等信息将请求转发到相应的后端服务，其内核与反向代理类似，不同之处在于API Gateway增加了“API”这层抽象，更加专注于构建、管理和增强API。</p>
<p>下面我们来看看Tyk如何配置API路由，我们首先创建一个新API。</p>
<h4>3.3.1 创建一个新API</h4>
<p>Tyk开源版支持两种创建API的方式，一种是通过<a href="https://tyk.io/docs/getting-started/create-api/#tutorial-create-an-api-with-the-tyk-gateway-api">调用Tyk的控制类API</a>，一种则是<a href="https://tyk.io/docs/getting-started/create-api/#tutorial-create-an-api-in-file-based-mode">通过传统的配置文件，放入特定目录下</a>。无论哪种方式添加完API，最终都要通过Tyk Gateway热加载(hot reload)或重启才能生效。</p>
<blockquote>
<p>注：Tyk Gateway的商业版本提供Dashboard，可以以图形化的方式管理API，并且商业版本的API定义会放在Postgres或MongoDB中，我们这里用开源版本，只能手工管理了，并且API定义只能放在文件中。</p>
</blockquote>
<p>下面，我们就来在Tyk上创建一个新的API路由，该路由示例的示意图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-5.png" alt="" /></p>
<p>在未添加新API之前，我们使用curl访问一下该API路径：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Not Found
</code></pre>
<p>Tyk Gateway由于找不到API路由，返回Not Found。接下来，我们采用调用tyk gateway API的方式来添加路由：</p>
<pre><code>$curl -v -H "x-tyk-authorization: {tyk gateway secret}" \
  -s \
  -H "Content-Type: application/json" \
  -X POST \
  -d '{
    "name": "no-authn-v1",
    "slug": "no-authn-v1",
    "api_id": "no-authn-v1",
    "org_id": "1",
    "use_keyless": true,
    "auth": {
      "auth_header_name": "Authorization"
    },
    "definition": {
      "location": "header",
      "key": "x-api-version"
    },
    "version_data": {
      "not_versioned": true,
      "versions": {
        "Default": {
          "name": "Default",
          "use_extended_paths": true
        }
      }
    },
    "proxy": {
      "listen_path": "/api/v1/no-authn",
      "target_url": "http://localhost:18081/",
      "strip_listen_path": true
    },
    "active": true
}' http://localhost:8080/tyk/apis | python -mjson.tool 

* About to connect() to localhost port 8080 (#0)
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
&gt; POST /tyk/apis HTTP/1.1
&gt; User-Agent: curl/7.29.0
&gt; Host: localhost:8080
&gt; Accept: */*
&gt; x-tyk-authorization: {tyk gateway secret}
&gt; Content-Type: application/json
&gt; Content-Length: 797
&gt;
} [data not shown]
* upload completely sent off: 797 out of 797 bytes
&lt; HTTP/1.1 200 OK
&lt; Content-Type: application/json
&lt; Date: Wed, 22 Nov 2023 05:38:40 GMT
&lt; Content-Length: 53
&lt;
{ [data not shown]
* Connection #0 to host localhost left intact
{
    "action": "added",
    "key": "no-authn-v1",
    "status": "ok"
}
</code></pre>
<p>从curl返回结果我们看到：API已经被成功添加。这时tyk gateway的安装目录/opt/tyk-gateway的子目录apps下会新增一个名为no-authn-v1.json的配置文件，这个文件内容较多，有300行，这里就不贴出来了，这个文件就是新增的no-authn <a href="https://tyk.io/docs/tyk-gateway-api/api-definition-objects/">API的定义文件</a>。</p>
<p>不过此刻，Tyk Gateway还需热加载后才能为新的API提供服务，调用下面API可以触发Tyk Gateway的热加载：</p>
<pre><code>$curl -H "x-tyk-authorization: {tyk gateway secret}" -s http://localhost:8080/tyk/reload/group | python -mjson.tool
{
    "message": "",
    "status": "ok"
}
</code></pre>
<blockquote>
<p>注：即便触发热加载成功，但如果body中的json格式错，比如多了一个结尾逗号，Tyk Gateway是不会报错的！</p>
</blockquote>
<p>API路由创建完毕并生效后，我们再来访问一下API：</p>
<pre><code>$ curl localhost:8080/api/v1/no-authn
{
    "error": "There was a problem proxying the request"
}
</code></pre>
<p>我们看到：Tyk Gateway返回的已经不是“Not Found”了！现在我们创建一下no-authn这个API服务，考虑到适配更多后续示例，这里建立这样一个http server：</p>
<pre><code>// api-gateway-examples/httpserver

func main() {
    // 解析命令行参数
    port := flag.Int("p", 8080, "Port number")
    apiVersion := flag.String("v", "v1", "API version")
    apiName := flag.String("n", "example", "API name")
    flag.Parse()                                         

    // 注册处理程序
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(*r)
        fmt.Fprintf(w, "Welcome api: localhost:%d/%s/%s\n", *port, *apiVersion, *apiName)
    })                                                                                     

    // 启动HTTP服务器
    addr := fmt.Sprintf(":%d", *port)
    log.Printf("Server listening on port %d\n", *port)
    log.Fatal(http.ListenAndServe(addr, nil))
}
</code></pre>
<p>我们启动一个该http server的实例：</p>
<pre><code>$go run main.go -p 18081 -v v1 -n no-authn
2023/11/22 22:02:42 Server listening on port 18081
</code></pre>
<p>现在我们再通过tyk gateway调用一下no-authn这个API：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
</code></pre>
<p>我们看到这次路由通了！no-authn API返回了期望的结果！</p>
<h4>3.3.2 负载均衡</h4>
<p>如果no-authn API存在多个服务实例，Tyk Gateway也可以将请求流量负载均衡到多个no-authn服务实例上去，下图是Tyk Gateway进行<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/load-balancing/">请求流量负载均衡</a>的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-6.png" alt="" /></p>
<p>要实现负责均衡，我们需要调整no-authn API的定义，这次我们直接修改/opt/tyk-gateway/apps/no-authn-v1.json，变更的配置主要有三项：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

  "proxy": {
    "preserve_host_header": false,
    "listen_path": "/api/v1/no-authn",
    "target_url": "",                  // (1) 改为""
    "disable_strip_slash": false,
    "strip_listen_path": true,
    "enable_load_balancing": true,     // (2) 改为true
    "target_list": [                   // (3) 填写no-authn服务实例列表
      "http://localhost:18081/",
      "http://localhost:18082/",
      "http://localhost:18083/"
    ],
</code></pre>
<p>修改完配置后，调用Tyk的控制类API使之生效，然后我们启动三个no-authn的API实例：</p>
<pre><code>$go run main.go -p 18081 -v v1 -n no-authn
$go run main.go -p 18082 -v v1 -n no-authn
$go run main.go -p 18083 -v v1 -n no-authn
</code></pre>
<p>接下来，我们多次调用curl访问no-authn API：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18083/v1/no-authn

$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18083/v1/no-authn
</code></pre>
<p>我们看到：Tyk Gateway在no-authn API的各个实例之间做了等权重的轮询。如果我们停掉实例3，再来访问该API，我们将得到下面结果：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Bad Request

$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Bad Request
</code></pre>
<blockquote>
<p>注：Tyk Gateway商业版通过Dashboard<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/load-balancing/">支持配置带权重的RR负载均衡算法</a>。</p>
</blockquote>
<p>我们看到：实例3已经下线，但Tyk Gateway并不会跳过该已经下线的实例，这在生产环境会给客户端带来不一致的响应。</p>
<h4>3.3.3 服务实例存活检测(uptime test)</h4>
<p>Tyk Gateway在开启负载均衡的时候，也提供了对后端服务实例的存活检测机制，当某个服务实例down了后，负载均衡机制会绕过该实例将请求发到下一个处于存活状态的实例；而当down机实例恢复后，Tyk Gateway也能及时检测到服务实例上线，并将其加入流量负载调度。</p>
<p>支持存活检测(uptime test)的API定义配置如下：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

"uptime_tests": {
    "disable": false,
    "poller_group":"",
    "check_list": [
      {
        "url": "http://localhost:18081/"
      },
      {
        "url": "http://localhost:18082/"
      },
      {
        "url": "http://localhost:18083/"
      }
    ],
    "config": {
      "enable_uptime_analytics": true,
      "failure_trigger_sample_size": 3,
      "time_wait": 300,
      "checker_pool_size": 50,
      "expire_utime_after": 0,
      "service_discovery": {
        "use_discovery_service": false,
        "query_endpoint": "",
        "use_nested_query": false,
        "parent_data_path": "",
        "data_path": "",
        "port_data_path": "",
        "target_path": "",
        "use_target_list": false,
        "cache_disabled": false,
        "cache_timeout": 0,
        "endpoint_returns_list": false
      },
      "recheck_wait": 0
    }
}

"proxy": {
    ... ...
    "enable_load_balancing": true,
    "target_list": [
      "http://localhost:18081/",
      "http://localhost:18082/",
      "http://localhost:18083/"
    ],
    "check_host_against_uptime_tests": true,
    ... ...
}
</code></pre>
<p>我们新增了uptime_tests的配置，uptime_tests的check_list中的url的值要与proxy中target_list中的值完全一样，这样Tyk Gateway才能将二者对应上。另外proxy的check_host_against_uptime_tests要设置为true。</p>
<p>这样配置并生效后，等我们将服务实例3停掉后，后续到no-authn的请求就只会转发到实例1和实例2了。而当恢复实例3运行后，Tyk Gateway又会将流量分担到实例3上。</p>
<h4>3.3.4 动态负载均衡</h4>
<p>上面负载均衡示例中target_list中的目标实例的IP和端口的手工配置的，而在云原生时代，我们经常会基于容器承载API服务实例，当容器因故退出，并重新启动一个新容器时，IP可能会发生变化，这样上述的手工配置就无法满足要求，这就对API Gateway提出了与服务发现组件集成的要求：通过服务发现组件动态获取服务实例的访问列表，进而实现<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/service-discovery/">动态负载均衡</a>。</p>
<p>Tyk Gateway内置了主流服务发现组件(比如Etcd、Consul、ZooKeeper等)的对接能力，鉴于环境所限，这里就不举例了，大家可以在Tyk Gateway的<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/service-discovery/examples/">服务发现示例文档页面</a>找到与不同服务发现组件对接时的配置示例。</p>
<h4>3.3.5 IP访问限制</h4>
<p>针对每个API，API网关还提供IP访问限制的特性，比如Tyk Gateway就提供了<a href="https://tyk.io/docs/tyk-apis/tyk-gateway-api/api-definition-objects/ip-whitelisting/">IP白名单</a>和<a href="https://tyk.io/docs/tyk-apis/tyk-gateway-api/api-definition-objects/ip-blacklisting/">IP黑名单</a>功能，通常二选一开启一种限制即可。</p>
<p>以白名单为例，即凡是在白名单中的IP才被允许访问该API。下面是白名单配置样例：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

  "enable_ip_whitelisting": true,
  "allowed_ips": ["12.12.12.12", "12.12.12.13", "12.12.12.14"],
</code></pre>
<p>生效后，当我们访问no-authn API时，会得到下面错误：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
{
    "error": "access from this IP has been disallowed"
}
</code></pre>
<p>如果开启的是黑名单，那么凡是在黑名单中的IP都被禁止访问该API，下面是黑名单配置样例：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

  "enable_ip_blacklisting": true,
  "blacklisted_ips": ["12.12.12.12", "12.12.12.13", "12.12.12.14", "127.0.0.1"],
</code></pre>
<p>生效后，当我们访问no-authn API时，会得到如下结果：</p>
<pre><code>$curl 127.0.0.1:8080/api/v1/no-authn
{
    "error": "access from this IP has been disallowed"
}
</code></pre>
<p>到目前为止，我们的API网关和定义的API都处于“裸奔”状态，因为没有对客户端进行身份认证，任何客户端都可以访问到我们的API，显然这不是我们期望的，接下来，我们就来看看API网关的一个重要功能特性：身份认证与授权。</p>
<h3>3.4 功能特性：身份认证和授权</h3>
<p>在《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example">通过实例理解Go Web身份认证的几种方式</a>》一文中，我们提到过：<strong>建立全局的安全通道是任何身份认证方式的前提</strong>。</p>
<h4>3.4.1 建立安全通道，卸载TLS证书</h4>
<p>Tyk Gateway支持在Gateway层面<a href="https://tyk.io/docs/basic-config-and-security/security/tls-and-ssl/">统一配置TLS证书</a>，同时也起到在Gateway卸载TLS证书的作用：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-7.png" alt="" /></p>
<p>这次我们要在tyk.conf中进行配置，才能在Gateway层面生效。这里我们借用《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/">通过实例理解Go Web身份认证的几种方式</a>》一文中生成的几个证书(大家可以在https://github.com/bigwhite/experiments/tree/master/authn-examples/tls-authn/make_certs下载)，并将它们放到/opt/tyk-gateway/certs/下面：</p>
<pre><code>$ls /opt/tyk-gateway/certs/
server-cert.pem  server-key.pem
</code></pre>
<p>然后，我们在/opt/tyk-gateway/tyk.conf文件中增加下面配置：</p>
<pre><code>// /opt/tyk-gateway/tyk.conf 

  "http_server_options": {
    "use_ssl": true,
    "certificates": [
      {
        "domain_name": "server.com",
        "cert_file": "./certs/server-cert.pem",
        "key_file": "./certs/server-key.pem"
      }
    ]
  }
</code></pre>
<p>之后，重启tyk gateway服务，使得tyk.conf的配置修改生效。</p>
<blockquote>
<p>注：在/etc/hosts中设置server.com为127.0.0.1。</p>
</blockquote>
<p>现在我们用之前的http方式访问一下no-authn的API：</p>
<pre><code>$curl server.com:8080/api/v1/no-authn
Client sent an HTTP request to an HTTPS server.
</code></pre>
<p>由于全局启用了HTTPS，采用http方式的请求将被拒绝。我们换成https方式访问：</p>
<pre><code>// 不验证服务端证书
$curl -k https://server.com:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn

// 验证服务端的自签证书
$curl --cacert ./inter-cert.pem https://server.com:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
</code></pre>
<h4>3.4.2 Mutual TLS双向认证</h4>
<p>在《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example">通过实例理解Go Web身份认证的几种方式</a>》一文中，我们介绍的第一种身份认证方式就是TLS双向认证，那么Tyk Gateway对MTLS的支持如何呢？<a href="https://tyk.io/docs/basic-config-and-security/security/mutual-tls/">Tyk官方文档</a>提到它既支持<a href="https://tyk.io/docs/basic-config-and-security/security/mutual-tls/client-mtls">client mTLS</a>，也支持<a href="https://tyk.io/docs/basic-config-and-security/security/mutual-tls/upstream-mtls">upstream mTLS</a>。</p>
<p>我们更关心的是client mTLS，即客户端在与Gateway建连后，Gateway会使用Client CA验证客户端的证书！我最初认为这个Client CA的配置是在tyk.conf中，但找了许久，也没有发现配置Client CA的地方。</p>
<p>在no-authn API的定义文件(no-authn-v1.json)中，我们做如下配置改动：</p>
<pre><code>  "use_mutual_tls_auth": true,
  "client_certificates": [
      "/opt/tyk-gateway/certs/inter-cert.pem"
  ],
</code></pre>
<p>但使用下面命令访问API时报错：</p>
<pre><code>$curl --key ./client-key.pem --cert ./client-cert.pem --cacert ./inter-cert.pem https://server.com:8080/api/v1/no-authn
{
    "error": "Certificate with SHA256 bc8717c0f2ea5a0b81813abb3ec42ef8f9bf60da251b87243627d65fb0e3887b not allowed"
}
</code></pre>
<p>如果将”client_certificates”的配置中的inter-cert.pem改为client-cert.pem，则是可以的，但个人感觉这很奇怪，不符合逻辑，将tyk gateway的文档、issue甚至代码翻了又翻，也没找到合理的配置client CA的位置。</p>
<p><a href="https://tyk.io/docs/apim-best-practice/api-security-best-practice/authentication/">Tyk Gateway支持多种身份认证方式</a>，下面我们来看一种使用较为广泛的方式：JWT Auth。</p>
<blockquote>
<p>主要JWT身份认证方式的原理和详情，可以参考我之前的文章《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/">通过实例理解Go Web身份认证的几种方式</a>》。</p>
</blockquote>
<h4>3.4.3 JWT Token Auth</h4>
<p>下面是我为这个示例做的一个示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-8.png" alt="" /></p>
<p>这是我们日常开发中经常遇到的场景，即通过portal用用户名和密码登录后便可以拿到一个jwt token，然后后续的访问功能API的请求仅携带该jwt token即可。API Gateway对于portal/login API不做任何身份认证；而对后续的功能API请求，通过共享的secret(也称为static secret)对请求中携带的jwt token进行签名验证。</p>
<p>portal/login API由于不进行authn，这样其配置与前面的no-authn API几乎一致，只是API名称、路径和target_list有不同：</p>
<pre><code>// apps/portal-login-v1.json

{
  "name": "portal-login-v1",
  "slug": "portal-login-v1",
  "listen_port": 0,
  "protocol": "",
  "enable_proxy_protocol": false,
  "api_id": "portal-login-v1",
  "org_id": "1",
  "use_keyless": true,
  ... ...
  "proxy": {
    "preserve_host_header": false,
    "listen_path": "/api/v1/portal/login",
    "target_url": "",
    "disable_strip_slash": false,
    "strip_listen_path": true,
    "enable_load_balancing": true,
    "target_list": [
      "http://localhost:28084"
    ],
    "check_host_against_uptime_tests": true,
  ... ...
}
</code></pre>
<p>对应的portal login API也不复杂：</p>
<pre><code>// api-gateway-examples/portal-login/main.go

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

func main() {
    // 创建一个基本的HTTP服务器
    mux := http.NewServeMux()

    username := "admin"
    password := "123456"
    key := "iamtonybai"

    // for uptime test
    mux.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    // login handler
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        // 从请求头中获取Basic Auth认证信息
        user, pass, ok := req.BasicAuth()
        if !ok {
            // 认证失败
            w.WriteHeader(http.StatusUnauthorized)
            return
        }

        // 验证用户名密码
        if user == username &amp;&amp; pass == password {
            // 认证成功，生成token
            token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
                "username": username,
                "iat":      jwt.NewNumericDate(time.Now()),
            })
            signedToken, _ := token.SignedString([]byte(key))
            w.Write([]byte(signedToken))
        } else {
            // 认证失败
            http.Error(w, "Invalid username or password", http.StatusUnauthorized)
        }
    })

    // 监听28084端口
    err := http.ListenAndServe(":28084", mux)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>运行该login API服务后，我们用curl命令获取一下jwt token：</p>
<pre><code>$curl -u 'admin:123456' -k https://server.com:8080/api/v1/portal/login
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA
</code></pre>
<p>现在我们再来建立protected API：</p>
<pre><code>// apps/protected-v1.json

{
  "name": "protected-v1",
  "slug": "protected-v1",
  "listen_port": 0,
  "protocol": "",
  "enable_proxy_protocol": false,
  "api_id": "protected-v1",
  "org_id": "1",
  "use_keyless": false,    // 设置为false, gateway才会进行jwt的验证
  ... ...
  "enable_jwt": true,      // 开启jwt
  "use_standard_auth": false,
  "use_go_plugin_auth": false,
  "enable_coprocess_auth": false,
  "custom_plugin_auth_enabled": false,
  "jwt_signing_method": "hmac",        // 设置alg为hs256
  "jwt_source": "aWFtdG9ueWJhaQ==",    // 设置共享secret: base64("iamtonybai")
  "jwt_identity_base_field": "username", // 设置代表请求中的用户身份的字段，这里我们用username
  "jwt_client_base_field": "",
  "jwt_policy_field_name": "",
  "jwt_default_policies": [
     "5e189590801287e42a6cf5ce"        // 设置security policy，这个似乎是jwt auth必须的
  ],
  "jwt_issued_at_validation_skew": 0,
  "jwt_expires_at_validation_skew": 0,
  "jwt_not_before_validation_skew": 0,
  "jwt_skip_kid": false,
  ... ...
  "version_data": {
    "not_versioned": true,
    "default_version": "",
    "versions": {
      "Default": {
        "name": "Default",
        "expires": "",
        "paths": {
          "ignored": null,
          "white_list": null,
          "black_list": null
        },
        "use_extended_paths": true,
        "extended_paths": {
          "persist_graphql": null
        },
        "global_headers": {
          "username": "$tyk_context.jwt_claims_username" // 设置转发到upstream的请求中的header字段username
        },
        "global_headers_remove": null,
        "global_response_headers": null,
        "global_response_headers_remove": null,
        "ignore_endpoint_case": false,
        "global_size_limit": 0,
        "override_target": ""
      }
    }
  },
  ... ...
  "enable_context_vars": true, // 开启上下文变量
  "config_data": null,
  "config_data_disabled": false,
  "tag_headers": ["username"], // 设置header
  ... ...
}
</code></pre>
<p>这个配置就相对复杂许多，也是翻阅了很长时间资料才验证通过的配置。JWT Auth必须有关联的policy设置，我们在tyk gateway开源版中要想设置policy，需要现在tyk.conf中做如下设置：</p>
<pre><code>// /opt/tyk-gateway/tyk.conf

  "policies": {
    "policy_source": "file",
    "policy_record_name": "./policies/policies.json"
  },
</code></pre>
<p>而policies/policies.json的内容如下：</p>
<pre><code>// /opt/tyk-gateway/policies/policies.json
{
    "5e189590801287e42a6cf5ce": {
        "rate": 1000,
        "per": 1,
        "quota_max": 100,
        "quota_renewal_rate": 60,
        "access_rights": {
            "protected-v1": {
                "api_name": "protected-v1",
                "api_id": "protected-v1",
                "versions": [
                    "Default"
                ]
            }
        },
        "org_id": "1",
        "hmac_enabled": false
    }
}

</code></pre>
<p>上述设置完毕并重启tyk gateway生效后，且protected api服务也已经启动时，我们访问一下该API服务：</p>
<pre><code>$curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA" -k https://server.com:8080/api/v1/protected
invoke protected api ok
</code></pre>
<p>我们看到curl发出的请求成功通过了Gateway的验证！并且通过protected API输出的请求信息来看，Gateway成功解析出username，并将其作为Header中的字段传递给了protected API服务实例：</p>
<pre><code>http.Request{Method:"GET", URL:(*url.URL)(0xc0002f6240), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept":[]string{"*/*"}, "Accept-Encoding":[]string{"gzip"}, "Authorization":[]string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA"}, "User-Agent":[]string{"curl/7.29.0"}, "Username":[]string{"admin"}, "X-Forwarded-For":[]string{"127.0.0.1"}}, Body:http.noBody{}, GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"localhost:28085", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"[::1]:55583", RequestURI:"/", TLS:(*tls.ConnectionState)(nil), Cancel:(&lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0002e34f0)}
</code></pre>
<p>如果不携带Authorization头字段或jwt的token是错误的，那么结果将如下所示：</p>
<pre><code>$ curl -k https://server.com:8080/api/v1/protected
{
    "error": "Authorization field missing"
}

$ curl -k -H "Authorization: Bearer xxx" https://server.com:8080/api/v1/protected
{
    "error": "Key not authorized"
}
</code></pre>
<p>一旦通过API Gateway的身份认证，上游的API服务就会拿到客户端身份，有了唯一身份后，就可以进行<a href="https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/">授权操作</a>了，其实policy设置本身也是一种授权访问控制。Tyk Gateway自身也<a href="https://tyk.io/docs/tyk-dashboard/rbac/#understanding-the-concept-of-users-and-permissions">支持RBAC等模型</a>，也支持与OPA(open policy agent)等的集成，但更多是在商业版的tyk dashboard下完成的，这里也就不重点说明了。</p>
<p>下面的Gateway的几个主要功能特性由于试验环境受限以及文章篇幅考量，我不会像上述例子这么细致的说明了，只会简单说明一下。</p>
<h3>3.5 功能特性：流量控制与限速</h3>
<p>Tyk Gateway内置提供了强大的流量控制功能，可以通过<a href="https://tyk.io/docs/basic-config-and-security/control-limit-traffic/rate-limiting/">全局级别和API级别的限速</a>来管理请求流量。此外，Tyk Gateway 还<a href="https://tyk.io/docs/basic-config-and-security/control-limit-traffic/request-quotas/">支持请求配额（request quota）</a>来限制每个用户或应用程序在一个时间周期内的请求次数。</p>
<p>流量不仅和请求速度和数量有关系，与请求的大小也有关系，Tyk Gateway还支持在全局层面和API层面<a href="https://tyk.io/docs/basic-config-and-security/control-limit-traffic/request-size-limits/">设置Request的size limit</a>，以避免超大包对网关运行造成不良影响。</p>
<h3>3.6 功能特性：高可用与容错处理</h3>
<p>在许多情况下，我们要为客户确保服务水平(service level)，比如：最大往返时间、最大响应时延等。Tyk Gateway提供了一系列功能，可帮助我们确保网关的高可用运行和SLA服务水平。</p>
<p><a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/health-check/">Tyk支持健康检查</a>，这对于确定Tyk Gateway的状态极为重要，没有健康检查，就很难知道网关的实际运行状态如何。</p>
<p>Tyk Gateway还<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/circuit-breakers/">内置了断路器(circuit breaker)</a>，这个断路器是基于比例的，因此如果y个请求中的x请求都失败了，断路器就会跳闸，例如，如果x = 10，y = 100，则阈值百分比为10%。当失败比例到达10%时，断路器就会切断流量，同时跳闸还会触发一个事件，我们可以记录和处理该事件。</p>
<p>当upstream的服务响应迟迟不归时，Tyk Gateway还可以<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/enforced-timeouts/">设置强制超时</a>，可以确保服务始终在给定时间内响应。这在高可用性系统中非常重要，因为在这种系统中，响应性能至关重要，这样才能干净利落地处理错误。</p>
<h3>3.7 功能特性：监控与可观测性</h3>
<p>微服务时代，可观测性对运维以及系统高可用的重要性不言而喻。Tyk Gateway在多年的演化过程中，也逐渐增加了对可观测的支持，</p>
<p>可观测主要分三大块：</p>
<ul>
<li>log</li>
</ul>
<p>Tyk Gateway支持设置输出日志的级别(log level)，默认是info级别。Tyk输出的是结构化日志，这使得它可以很好的与其他日志收集查询系统集成，<a href="https://tyk.io/docs/log-data/#logging">Tyk支持与主流的日志收集工具对接</a>，包括：logstash、sentry、Graylog、Syslog等。</p>
<ul>
<li>metrics</li>
</ul>
<p>度量数据是反映网关系统健康状况、错误计数和类型、IT基础设施（服务器、虚拟机、容器、数据库和其他后端组件）及其他流程的硬件资源数据的重要参考。运维团队可以通过<a href="https://tyk.io/docs/planning-for-production/monitoring/">使用监控工具来利用实时度量的数据</a>，识别运行趋势、在系统故障时设置警报、确定问题的根本原因并缓解问题。</p>
<p>Tyk Gateway内置了<a href="https://tyk.io/blog/service-level-objectives-for-your-apis-with-tyk-prometheus-and-grafana/">对主流metrics采集方案Prometheus+Grafana的支持</a>，可以在网关层面以及对API进行实时度量数据采集和展示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-9.png" alt="" /></p>
<ul>
<li>tracing</li>
</ul>
<p>Tyk Gateway从5.2版本开始<a href="https://tyk.io/docs/product-stack/tyk-gateway/advanced-configurations/distributed-tracing/open-telemetry/open-telemetry-overview/">支持了与服务Tracing界的标准：OpenTelemetry的集成</a>，这样你可以使用多种支持OpenTelemetry的Tracing后端，比如Jaeger、Datadog等。Tracing可在Gateway层面开启，也可以延展到API层面。</p>
<h2>4. 小结</h2>
<p>本文对已经相对成熟的API网关技术做了回顾，对API网关的演进阶段、主流特性以及当前市面上的主流API网关进行了简要说明，并以Go实现的Tyk Gateway社区开源版为例，以示例方式对API网关的主要功能做了介绍。</p>
<p>总体而言，Tyk Gateway是一款功能强大，社区相对活跃并有商业公司支持的产品，文档很丰富，但从实际使用层面，这些文档对Tyk社区版本的使用者来说并不友好，指导性不足(更多用商业版的Dashboard说明，与配置文件难于对应)，就像本文例子中那样，为了搞定JWT认证，笔者着实花了不少时间查阅资料，甚至阅读源码。</p>
<p>Tyk Gateway的配置设计平坦，没有层次和逻辑，感觉是随着时间随意“堆砌”上去的。并且配置文件更新时，如果出现格式问题，Tyk Gateway并不报错，让人难于确定配置是否真正生效了，只能用<a href="https://tyk.io/docs/tyk-gateway-api/">Tyk Gateway的控制API</a>去查询结果来验证，非常繁琐低效。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/api-gateway-examples">这里</a>下载，文中涉及的一些tyk gateway api和security policy的配置也可以在其中查看。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://37signals.com/podcast/leaving-the-cloud/">Leaving the Cloud</a> &#8211; https://37signals.com/podcast/leaving-the-cloud/</li>
<li><a href="https://www.infoq.com/articles/past-present-future-api-gateways/">The Past, Present, and Future of API Gateways</a> &#8211; https://www.infoq.com/articles/past-present-future-api-gateways/</li>
<li><a href="https://blog.oneuptime.com/moving-from-aws-to-bare-metal/">How moving from AWS to Bare-Metal saved us 230,000/yr</a> &#8211; https://blog.oneuptime.com/moving-from-aws-to-bare-metal/</li>
<li><a href="https://navendu.me/posts/gateway-and-mesh/">A Comprehensive Guide to API Gateways, Kubernetes Gateways, and Service Meshes</a> &#8211; https://navendu.me/posts/gateway-and-mesh/</li>
<li><a href="https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway">Use API gateways in microservices</a> &#8211; https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway</li>
<li><a href="https://blog.postman.com/the-tyk-api-gateway-and-postman/">The Tyk API Gateway and Postman</a> &#8211; https://blog.postman.com/the-tyk-api-gateway-and-postman/</li>
<li><a href="https://javascript.plainenglish.io/getting-started-to-tyk-api-gateway-with-keycloak-16307435584a">Getting Started with Tyk API Gateway with Keycloak</a> &#8211; https://javascript.plainenglish.io/getting-started-to-tyk-api-gateway-with-keycloak-16307435584a</li>
<li><a href="https://medium.com/@asoorm/observing-your-api-metrics-with-tyk-elasticsearch-kibana-74e8fd946c39">Observing your API traffic with Tyk, Elasticsearch &amp; Kibana</a> &#8211; https://medium.com/@asoorm/observing-your-api-metrics-with-tyk-elasticsearch-kibana-74e8fd946c39</li>
<li><a href="https://community.tyk.io/t/set-up-jwt-token-in-tyk-gateway/6572/9">Set up JWT token in tyk gateway</a> &#8211; https://community.tyk.io/t/set-up-jwt-token-in-tyk-gateway/6572/9</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/03/understand-api-gateway-main-functional-features-by-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go是一门面向对象编程语言吗</title>
		<link>https://tonybai.com/2023/03/12/is-go-object-oriented/</link>
		<comments>https://tonybai.com/2023/03/12/is-go-object-oriented/#comments</comments>
		<pubDate>Sun, 12 Mar 2023 12:58:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Alef]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[FAQ]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopl]]></category>
		<category><![CDATA[Go程序设计语言]]></category>
		<category><![CDATA[has-a]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[is-a]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Newsqueak]]></category>
		<category><![CDATA[oberon]]></category>
		<category><![CDATA[OO]]></category>
		<category><![CDATA[OOP]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[receiver]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Simula]]></category>
		<category><![CDATA[Smalltalk]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[type-embedding]]></category>
		<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=3819</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/03/12/is-go-object-oriented Go语言已经开源13年了，在近期TIOBE发布的2023年3月份的编程语言排行榜中，Go再次冲入前十，相较于Go在2022年底的排名提升了2个位次： 《Go语言第一课》专栏中关于Go在这两年开始飞起的“预言”也正在逐步成为现实^_^，大家学习Go的热情也在快速提升， 《Go语言第一课》专栏的学习的人数年后也快速增加，快突破2w了。 很多专栏的订阅者都是第一次接触Go，他们中的很多是来自像Java, Ruby这样的OO(面向对象)语言阵营的，他们学习Go之后的第一个问题便是：Go是一门OO语言吗？在这篇博文中，我们就来探讨一下。 一. 溯源 在公认的Go语言“圣经”《Go程序设计语言》一书中，有这样一幅Go语言与其主要的先祖编程语言的亲缘关系图： 从图中我们可以清晰看到Go语言的“继承脉络”： 从C语言那里借鉴了表达式语法、控制语句、基本数据类型、值参数传递、指针等； 从Oberon-2语言那里借鉴了package、包导入和声明的语法，而Object Oberon提供了方法声明的语法。 从Alef语言以及Newsqueak语言中借鉴了基于CSP的并发语法。 我们看到，从Go先祖溯源的情况来看，Go并没有从纯面向对象语言比如Simula、SmallTalk等那里取经。 Go诞生于2007年，开源于2009年，那正是面向对象语言和OO范式大行其道的时期。不过Go设计者们觉得经典OO的继承体系对程序设计与扩展似乎并无太多好处，还带来了较多的限制，因此在正式版本中并没有支持经典意义上的OO语法，即基于类和对象实现的封装、继承和多态这三大OO主流特性。 但这是否说明Go不是一门OO语言呢？也不是！ 带有面向对象机制的Object Oberon也是Go的先祖语言之一，虽然Object Oberon的OO语法又与我们今天常见的语法有较大差异。 就此问题，我还特意咨询了ChatGPT^_^，得到的答复如下： ChatGPT认为：Go支持面向对象，提供了对面向对象范式基本概念的支持，但支持的手段却并不是类与对象。 那么针对这个问题Go官方是否有回应呢？有的，我们来看一下。 二. 官方声音 Go官方在FAQ中就Go是否是OO语言做了简略回应： Is Go an object-oriented language? Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/is-go-object-oriented-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/03/12/is-go-object-oriented">本文永久链接</a> &#8211; https://tonybai.com/2023/03/12/is-go-object-oriented</p>
<p><a href="https://tonybai.com/2022/11/11/go-opensource-13-years/">Go语言已经开源13年了</a>，在近期<a href="https://www.tiobe.com/tiobe-index/">TIOBE</a>发布的2023年3月份的编程语言排行榜中，Go再次冲入前十，相较于Go在<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">2022年底的排名</a>提升了2个位次：</p>
<p><img src="https://tonybai.com/wp-content/uploads/is-go-object-oriented-2.png" alt="" /></p>
<p><a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>中关于Go在这两年开始飞起的“预言”也正在逐步成为现实^_^，大家学习Go的热情也在快速提升， <a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>的学习的人数年后也快速增加，快突破2w了。</p>
<p>很多专栏的订阅者都是第一次接触Go，他们中的很多是来自像Java, Ruby这样的OO(面向对象)语言阵营的，他们学习Go之后的第一个问题便是：<strong>Go是一门OO语言吗</strong>？在这篇博文中，我们就来探讨一下。</p>
<h2>一. 溯源</h2>
<p>在公认的Go语言“圣经”<a href="http://www.gopl.io">《Go程序设计语言》</a>一书中，有这样一幅Go语言与其主要的先祖编程语言的亲缘关系图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/is-go-object-oriented-4.png" alt="" /></p>
<p>从图中我们可以清晰看到Go语言的“继承脉络”：</p>
<ul>
<li>从<a href="https://tonybai.com/tag/c">C语言</a>那里借鉴了表达式语法、控制语句、基本数据类型、值参数传递、指针等；</li>
<li>从<a href="https://cseweb.ucsd.edu/~wgg/CSE131B/oberon2.htm">Oberon-2语言</a>那里借鉴了package、包导入和声明的语法，而Object Oberon提供了方法声明的语法。</li>
<li>从<a href="http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/ref">Alef语言</a>以及<a href="https://newspeaklanguage.org">Newsqueak语言</a>中借鉴了基于<a href="https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/tony-hoare/csp.html">CSP</a>的并发语法。</li>
</ul>
<p>我们看到，从Go先祖溯源的情况来看，Go并没有从纯面向对象语言比如Simula、<a href="http://en.wikipedia.org/wiki/Smalltalk">SmallTalk</a>等那里取经。</p>
<p>Go诞生于2007年，开源于2009年，那正是面向对象语言和OO范式大行其道的时期。不过Go设计者们觉得经典OO的继承体系对程序设计与扩展似乎并无太多好处，还带来了较多的限制，因此在正式版本中并没有支持经典意义上的OO语法，即基于类和对象实现的封装、继承和多态这三大OO主流特性。</p>
<p>但这是否说明Go不是一门OO语言呢？也不是！ 带有面向对象机制的<a href="http://www.projectoberon.net/">Object Oberon</a>也是Go的先祖语言之一，虽然Object Oberon的OO语法又与我们今天常见的语法有较大差异。</p>
<p>就此问题，我还特意咨询了<a href="https://chat.openai.com/chat">ChatGPT</a>^_^，得到的答复如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/is-go-object-oriented-3.png" alt="" /></p>
<p>ChatGPT认为：Go支持面向对象，提供了对面向对象范式基本概念的支持，但支持的手段却并不是类与对象。</p>
<p>那么针对这个问题Go官方是否有回应呢？有的，我们来看一下。</p>
<h2>二. 官方声音</h2>
<p><a href="https://go.dev/doc/faq#Is_Go_an_object-oriented_language">Go官方在FAQ中就Go是否是OO语言做了简略回应</a>：</p>
<pre><code>Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.
</code></pre>
<p>粗略翻译过来就是：</p>
<pre><code>Go是一种面向对象的语言吗？

是，也不是。虽然Go有类型和方法，并且允许面向对象的编程风格，但却没有类型层次。Go中的“接口”概念提供了一种不同的OO实现方案，我们认为这种方案更易于使用，而且在某些方面更加通用。还有一些可以将类型嵌入到其他类型中以提供类似子类但又不等同于子类的机制。此外，Go中的方法比C++或Java中的方法更通用：Go可以为任何数据类型定义方法，甚至是内置类型，如普通的、“未装箱的”整数。Go的方法并不局限于结构体（类）。

此外，由于去掉了类型层次，Go中的“对象”比C++或Java等语言更轻巧。
</code></pre>
<p>“是，也不是”！我们看到Go官方给出了一个“对两方都无害”的中庸的回答。那么Go社区是怎么认为的呢？我们来看看Go社区的一些典型代表的观点。</p>
<h2>三. 社区声音</h2>
<p><a href="https://rakyll.org/">Jaana Dogan</a>和<a href="https://spf13.com/">Steve Francia</a>都是前Go核心团队成员，他们在加入Go团队之前对“Go是否是OO语言”这一问题也都有自己的观点论述。</p>
<p>Jaana Dogan在<a href="https://rakyll.org/typesystem/">《The Go type system for newcomers》</a>一文中给出的观点是：<strong>Go is considered as an object-oriented language even though it lacks type hierarchy</strong>，即“Go被认为是一种面向对象的语言，即使它缺少类型层次结构”。</p>
<p>而更早一些的是Steve Francia在2014年发表的文章<a href="https://spf13.com/p/is-go-an-object-oriented-language/">《Is Go an Object Oriented language?》</a>中的结论观点：<strong>Go，没有对象或继承的面向对象编程</strong>，也可称为“无对象”的OO编程模型。</p>
<p>两者表达的遣词不同，但含义却异曲同工，即<strong>Go支持面向对象编程，但却不是通过提供经典的类、对象以及类型层次来实现的</strong>。</p>
<p>那么Go究竟是以何种方式实现对OOP的支持的呢？我们继续看！</p>
<h2>四. Go的“无对象”OO编程</h2>
<p>经典OO的三大特性是封装、继承与多态，这里我们看看Go中是如何对应的。</p>
<h3>1. 封装</h3>
<p>封装就是把数据以及操作数据的方法“打包”到一个抽象数据类型中，这个类型封装隐藏了实现的细节，所有数据仅能通过导出的方法来访问和操作。 这个抽象数据类型的实例被称为<strong>对象</strong>。经典OO语言，如Java、C++等都是通过类(class)来表达封装的概念，通过类的实例来映射对象的。熟悉Java的童鞋一定记得<a href="https://book.douban.com/subject/2130190/">《Java编程思想》</a>一书的第二章的标题：“一切都是对象”。在Java中所有属性、方法都定义在一个个的class中。</p>
<p>Go语言没有class，那么封装的概念又是如何体现的呢？来自OO语言的初学者进入Go世界后，都喜欢“对号入座”，即Go中什么语法元素与class最接近！于是他们找到了struct类型。</p>
<p>Go中的struct类型中提供了对真实世界聚合抽象的能力，struct的定义中可以包含一组字段(field)，如果从OO角度来看，你也可以将这些字段视为属性，同时，我们也可以为struct类型定义方法(method)，下面例子中我们定义了一个名为Point的struct类型，它拥有一个导出方法Length：</p>
<pre><code>type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}
</code></pre>
<p>我们看到，从语法形式上来看，与经典OO声明类的方法不同，Go方法声明并不需要放在声明struct类型的大括号中。Length方法与Point类型建立联系的纽带是一个被称为<strong>receiver参数</strong>的语法元素。</p>
<p>那么，struct是否就是对应经典OO中的类呢? 是，也不是！从数据聚合抽象来看，似乎是这样, struct类型可以拥有多个异构类型的、代表不同抽象能力的字段(比如整数类型int可以用来抽象一个真实世界物体的长度，string类型字段可以用来抽象真实世界物体的名字等)。</p>
<p>但从拥有方法的角度，不仅是struct类型，<strong>Go中除了内置类型的所有其他具名类型都可以拥有自己的方法</strong>，哪怕是一个底层类型为int的新类型MyInt：</p>
<pre><code>type MyInt int

func(a MyInt)Add(b int) MyInt {
    return a + MyInt(b)
}
</code></pre>
<h3>2. 继承</h3>
<p>就像前面说的，Go设计者在Go诞生伊始就重新评估了对经典OO的语法概念的支持，最终放弃了对诸如类、对象以及类继承层次体系的支持。也就是说：<strong>在Go中体现封装概念的类型之间都是“路人”，没有亲爹和儿子的关系的“牵绊”</strong>。</p>
<p>谈到OO中的继承，大家更多想到的是子类继承了父类的属性与方法实现。Go虽然没有像Java extends关键字那样的显式继承语法，但Go也另辟蹊径地对“继承”提供了支持。这种支持方式就是类型嵌入(type embedding)，看一个例子：</p>
<pre><code>type P struct {
    A int
    b string
}

func (P) M1() {
}

func (P) M2() {
}

type Q struct {
    c [5]int
    D float64
}

func (Q) M3() {
}

func (Q) M4() {
}

type T struct {
    P
    Q
    E int
}

func main() {
    var t T
    t.M1()
    t.M2()
    t.M3()
    t.M4()
    println(t.A, t.D, t.E)
}
</code></pre>
<p>我们看到类型T通过嵌入P、Q两个类型，“继承”了P、Q的导出方法(M1~M4)和导出字段(A、D)。</p>
<blockquote>
<p>关于类型嵌入的具体语法说明，大家可以温习一下<a href="https://mp.weixin.qq.com/s/nRkEe5v3GNTjxJNbYflRag">《十分钟入门Go语言》</a>或<a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>。</p>
</blockquote>
<p>不过实际Go中的这种“继承”机制并非经典OO中的继承，其外围类型(T)与嵌入的类型(P、Q)之间没有任何“亲缘”关系。P、Q的导出字段和导出方法只是被提升为T的字段和方法罢了，<strong>其本质是一种组合</strong>，是组合中的代理（delegate）模式的一种实现。T只是一个代理（delegate），对外它提供了它可以代理的所有方法，如例子中的M1~M4方法。当外界发起对T的M1方法的调用后，T将该调用委派给它内部的P实例来实际执行M1方法。</p>
<p>以经典OO理论话术去理解就是<strong>T与P、Q的关系不是is-a，而是has-a的关系</strong>。</p>
<h3>3. 多态</h3>
<p>经典OO中的多态是尤指<strong>运行时多态</strong>，指的是调用方法时，会根据调用方法的实际对象的类型来调用不同类型的方法实现。</p>
<p>下面是一个C++中典型多态的例子：</p>
<pre><code>#include &lt;iostream&gt;

class P {
        public:
                virtual void M() = 0;
};

class C1: public P {
        public:
                void M();
};

void C1::M() {
        std::cout &lt;&lt; "c1.M()\n";
}

class C2: public P {
        public:
                void M();
};

void C2::M() {
        std::cout &lt;&lt; "c2.M()\n";
}

int main() {
        C1 c1;
        C2 c2;
        P *p = &amp;c1;
        p-&gt;M(); // c1.M()
        p = &amp;c2;
        p-&gt;M(); // c2.M()
}
</code></pre>
<p>这段代码比较清晰，一个父类P和两个子类C1和C2。父类P有一个虚拟成员函数M，两个子类C1和C2分别重写了M成员函数。在main中，我们声明父类P的指针，然后将C1和C2的对象实例分别赋值给p并调用M成员函数，从结果来看，在运行时p实际调用的函数会根据其指向的对象实例的实际类型而分别调用C1和C2的M。</p>
<p>显然，经典OO的多态实现依托的是类型的层次关系。那么对应没有了类型层次体系的Go来说，它又是如何实现多态的呢？<strong>Go使用接口来解锁多态</strong>！</p>
<p>和经典OO语言相比，Go更强调行为聚合与一致性，而非数据。因此Go提供了对类似duck typing的支持，即基于行为集合的类型适配，但相较于ruby等动态语言，Go的静态类型机制还可以保证应用duck typing时的类型安全。</p>
<p>Go的接口类型本质就是一组方法集合(行为集合)，一个类型如果实现了某个接口类型中的所有方法，那么就可以作为动态类型赋值给接口类型。通过该接口类型变量的调用某一方法，实际调用的就是其动态类型的方法实现。看下面例子：</p>
<pre><code>type MyInterface interface {
    M1()
    M2()
    M3()
}

type P struct {
}

func (P) M1() {}
func (P) M2() {}
func (P) M3() {}

type Q int
func (Q) M1() {}
func (Q) M2() {}
func (Q) M3() {}

func main() {
    var p P
    var q Q
    var i MyInterface = p
    i.M1() // P.M1
    i.M2() // P.M2
    i.M3() // P.M3

    i = q
    i.M1() // Q.M1
    i.M2() // Q.M2
    i.M3() // Q.M3
}
</code></pre>
<p>Go这种无需类型继承层次体系、低耦合方式的多态实现，是不是用起来更轻量、更容易些呢！</p>
<h2>五. Gopher的“OO思维”</h2>
<p>到这里，来自经典OO语言阵营的小伙伴们是不是已经找到了当初在入门Go语言时“感觉到别扭”的原因了呢！这种“别扭”就在于<strong>Go对于OO支持的方式与经典OO语言的差别</strong>：秉持着经典OO思维的小伙伴一上来就要建立的继承层次体系，但Go没有，也不需要。</p>
<p>要转变为正宗的Gopher的OO思维其实也不难，那就是“<strong>prefer接口，prefer组合，将习惯了的is-a思维改为has-a思维</strong>”。</p>
<h2>六. 小结</h2>
<p>是时候给出一些结论性的观点了：</p>
<ul>
<li>Go支持OO，只是用的不是经典OO的语法和带层次的类型体系；</li>
<li>Go支持OO，只是用起来需要换种思维；</li>
<li>在Go中玩转OO的思维方式是：“优先接口、优先组合”。</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/03/12/is-go-object-oriented/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>2023年的Rust与Go[译]</title>
		<link>https://tonybai.com/2023/02/22/rust-vs-go-in-2023/</link>
		<comments>https://tonybai.com/2023/02/22/rust-vs-go-in-2023/#comments</comments>
		<pubDate>Wed, 22 Feb 2023 13:49:14 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[rustfmt]]></category>
		<category><![CDATA[scale]]></category>
		<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=3804</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/02/22/rust-vs-go-in-2023 本文译自《Rust vs Go in 2023》。 注：从2022年下半年开始，我们研发团队的产品研发不再局限于云端，车端也是将来的一个重要方向。于是我除了继续对Go语言保持常规的高度关注之外，也逐步开始留意Rust语言的发展。 Rust和Go哪个更好？Go还是Rust？在2023年，你应该为你的下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能、规模和并发性等方面如何比较？它们的共同点是什么，它们有哪些根本性的不同？让我们在这个友好而公平的Rust和Go的比较中找到答案。 Rust和Go都很棒 首先，我必须要说的是，Go和Rust都是绝对优秀的编程语言。它们都是现代的、强大的、被广泛采用的编程语言，并且都提供出色的性能。 你可能读过一些说Go比Rust好的文章，或者相反。但这真的没有意义；每一种编程语言都代表了一系列的权衡和取舍。每种语言都有自己的优化重点，所以你对语言的选择应该由适合你的东西和你想用它解决的问题决定。 在这篇文章中，我将尝试告诉你何时使用Go是理想选择以及何时使用Rust更佳。我也会试着介绍一下这两种语言的本质（如果你愿意的话，就是Go和Rust的道）。 虽然它们在语法和风格上有很大不同，但Rust和Go都是构建软件的一流工具。接下来，让我们仔细看看这两种语言。 Go和Rust的相似之处 Rust和Go有很多共同点，这也是你经常听到它们一起被提及的原因之一。两种语言的共同目标是什么呢？ Rust是一种低级静态类型的多范式编程语言，专注于安全和性能。 &#8211; Gints Dreimanis Go是一种开源的编程语言，可以轻松构建简单、可靠、高效的软件。 &#8211; go.dev 内存安全 Go和Rust都属于现代编程语言，它们的首要任务是内存安全。经过几十年对C和C++等旧语言的使用，我们可以清楚地看到，导致错误和安全漏洞的最大原因之一是不安全地或不正确地访问内存。 Rust和Go以不同的方式处理这个问题，但它们的目标都是在管理内存方面比其他语言更聪明、更安全，并帮助你写出正确和高性能的程序。 快速、紧凑的可执行文件 Go和Rust都是编译型语言，这意味着你的程序被直接翻译成可执行的机器码，因此你可以以单一二进制文件形式来部署你的程序；与Python和Ruby等解释型语言不同，你不需要将解释器和大量的库和依赖关系与你的程序一起分发，这是一个很大的优点。这也使得Rust和Go的程序与解释型语言相比都非常快。 通用语言 Rust和Go都是强大的、可扩展的通用编程语言，你可以用它们来开发各种现代软件，从网络应用到分布式微服务，或者从嵌入式微控制器到移动应用程序。 两者都有优秀的标准库、繁荣的第三方生态系统以及巨大的商业支持和庞大的用户基础。它们都已经存在了很多年，并将在未来几年内继续被广泛使用。今天学习Go或Rust将是对你时间和精力的合理投资。 务实的编程风格 Go和Rust都不是以函数式编程为主的语言（例如像Scala或Elixir），也不是完全面向对象的语言（像Java和C#）。相反，虽然Go和Rust都有与函数式和面向对象编程相关的特性，但它们是务实的语言，旨在以最合适的方式解决问题，而不是强迫你采用特定的做事方式。 如果你喜欢函数式编程风格，你会在Rust中发现更多对这种风格的支持，因为Rust在语法特性数量上要比Go更多。 我们可以讨论什么是“面向对象”语言，但可以说C++、Java或C#用户所期望的面向对象编程风格在Go或Rust中都不存在。 &#8211; Jack Mott 规模化的开发 Rust和Go都有一些有用的特性，使它们适合于大规模的编程，不管是指大型团队，还是大型代码库，或者两者兼具。 例如，C语言的程序员们多年来一直在争论将括号放在哪里，以及代码应该用制表符还是空格缩进，而Rust和Go通过使用标准的格式化工具（Go为gofmt，Rust为rustfmt）使用规范的风格自动重写你的代码，完全消除了这些问题。 这并不是说这种特殊的风格本身有多好：而是Rust和Go的程序员都喜欢这种标准化。 gofmt的风格是没有人喜欢的，但gofmt却是所有人的最爱。 &#8211; Rob Pike 两种语言的另一个高分领域是构建管道(pipeline)。两种语言都有优秀的、内置的、高性能的标准构建和依赖管理工具；不再需要与复杂的第三方构建系统搏斗，也不再需要每隔几年就学习一个新的系统。 对于早期职业生涯以Java和Ruby为背景的我而言，构建Go和Rust代码感觉就像从我的肩上卸下了一个不可能的重担。当我在谷歌工作时，遇到用Go编写的服务是一种解脱，因为我知道它很容易构建和运行。Rust也是如此，尽管我只在较小规模的Rust项目上工作过。我希望可无限配置的构建系统的时代已经过去了，所有语言都会有自己专门的构建工具，开箱即可使用。- 山姆-罗斯 Rust还是Go？ 综上可知，这两种语言都设计得很好、很强大，那么你可能会想知道那些关于两门语言的“圣战”究竟是怎么回事（我也是）。为什么人们对“Go vs.Rust”如此大惊小怪，在社交媒体上大打出手，并且写长篇博文说只有傻瓜才会使用Rust，或者Go不是真正的编程语言，或者其他什么。 这可能会让他们感觉好些，但这并不能完全帮助你，因为你正试图决定在你的项目中使用哪种语言，或者你应该学习哪种语言来推动你的编程生涯。一个明智的人不会根据谁喊得声最大来做出重要的选择。 现在让我们继续我们成熟的讨论，看看在某些领域，一个有理智的人可能更喜欢哪一种语言。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/rust-vs-go-in-2023-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/02/22/rust-vs-go-in-2023">本文永久链接</a> &#8211; https://tonybai.com/2023/02/22/rust-vs-go-in-2023</p>
<p>本文译自<a href="https://bitfieldconsulting.com/golang/rust-vs-go">《Rust vs Go in 2023》</a>。</p>
<blockquote>
<p>注：从2022年下半年开始，我们研发团队的产品研发不再局限于云端，车端也是将来的一个重要方向。于是我除了继续对Go语言保持常规的高度关注之外，也逐步开始留意Rust语言的发展。</p>
</blockquote>
<hr />
<p>Rust和Go哪个更好？Go还是Rust？在2023年，你应该为你的下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能、规模和并发性等方面如何比较？它们的共同点是什么，它们有哪些根本性的不同？让我们在这个友好而公平的Rust和Go的比较中找到答案。</p>
<h2>Rust和Go都很棒</h2>
<p>首先，我必须要说的是，<strong>Go和Rust都是绝对优秀的编程语言</strong>。它们都是现代的、强大的、被广泛采用的编程语言，并且都提供出色的性能。</p>
<p>你可能读过一些说Go比Rust好的文章，或者相反。但这真的没有意义；每一种编程语言都代表了一系列的权衡和取舍。每种语言都有自己的优化重点，所以你对语言的选择应该由适合你的东西和你想用它解决的问题决定。</p>
<p>在这篇文章中，我将尝试告诉你何时使用Go是理想选择以及何时使用Rust更佳。我也会试着介绍一下这两种语言的本质（如果你愿意的话，就是<a href="https://tonybai.com/2022/09/25/the-tao-of-go">Go和Rust的道</a>）。</p>
<p>虽然它们在语法和风格上有很大不同，但Rust和Go都是构建软件的一流工具。接下来，让我们仔细看看这两种语言。</p>
<h2>Go和Rust的相似之处</h2>
<p>Rust和Go有很多共同点，这也是你经常听到它们一起被提及的原因之一。两种语言的共同目标是什么呢？</p>
<blockquote>
<p>Rust是一种低级静态类型的多范式编程语言，专注于安全和性能。 &#8211; <a href="https://serokell.io/blog/rust-guide">Gints Dreimanis</a></p>
<p>Go是一种开源的编程语言，可以轻松构建简单、可靠、高效的软件。 &#8211; <a href="https://go.dev">go.dev</a></p>
</blockquote>
<h3>内存安全</h3>
<p>Go和Rust都属于现代编程语言，它们的首要任务是内存安全。经过几十年对C和C++等旧语言的使用，我们可以清楚地看到，导致错误和安全漏洞的最大原因之一是不安全地或不正确地访问内存。</p>
<p>Rust和Go以不同的方式处理这个问题，但它们的目标都是在管理内存方面比其他语言更聪明、更安全，并帮助你写出<a href="https://bitfieldconsulting.com/golang/crisp-code">正确</a>和<a href="https://bitfieldconsulting.com/golang/slower">高性能</a>的程序。</p>
<h3>快速、紧凑的可执行文件</h3>
<p>Go和Rust都是编译型语言，这意味着你的程序被直接翻译成可执行的机器码，因此你可以以单一二进制文件形式来部署你的程序；与<a href="https://bitfieldconsulting.com/golang/go-vs-python">Python</a>和Ruby等解释型语言不同，你不需要将解释器和大量的库和依赖关系与你的程序一起分发，这是一个很大的优点。这也使得Rust和Go的程序与解释型语言相比都非常快。</p>
<h3>通用语言</h3>
<p>Rust和Go都是强大的、可扩展的通用编程语言，你可以用它们来开发各种现代软件，从网络应用到分布式微服务，或者从嵌入式微控制器到移动应用程序。</p>
<p>两者都有优秀的标准库、繁荣的第三方生态系统以及巨大的商业支持和庞大的用户基础。它们都已经存在了很多年，并将在未来几年内继续被广泛使用。今天学习Go或Rust将是对你时间和精力的合理投资。</p>
<h3>务实的编程风格</h3>
<p>Go和Rust都不是<a href="https://bitfieldconsulting.com/golang/functional">以函数式编程为主的语言</a>（例如像Scala或Elixir），也不是完全面向对象的语言（像Java和C#）。相反，虽然Go和Rust都有与函数式和面向对象编程相关的特性，但它们是务实的语言，旨在以最合适的方式解决问题，而不是强迫你采用特定的做事方式。</p>
<p>如果你喜欢函数式编程风格，你会在Rust中发现更多对这种风格的支持，因为Rust在语法特性数量上要比Go更多。</p>
<blockquote>
<p>我们可以讨论什么是“面向对象”语言，但可以说C++、Java或C#用户所期望的面向对象编程风格在Go或Rust中都不存在。 &#8211; Jack Mott</p>
</blockquote>
<h3>规模化的开发</h3>
<p>Rust和Go都有一些有用的特性，使它们适合于大规模的编程，不管是指大型团队，还是大型代码库，或者两者兼具。</p>
<p>例如，C语言的程序员们多年来一直在争论将括号放在哪里，以及代码应该用制表符还是空格缩进，而Rust和Go通过使用标准的格式化工具（Go为gofmt，Rust为rustfmt）使用规范的风格自动重写你的代码，完全消除了这些问题。</p>
<p>这并不是说这种特殊的风格本身有多好：而是Rust和Go的程序员都喜欢这种<strong>标准化</strong>。</p>
<blockquote>
<p>gofmt的风格是没有人喜欢的，但gofmt却是所有人的最爱。 &#8211; <a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&amp;t=8m43s">Rob Pike</a></p>
</blockquote>
<p>两种语言的另一个高分领域是<strong>构建管道(pipeline)</strong>。两种语言都有优秀的、内置的、高性能的标准构建和依赖管理工具；不再需要与复杂的第三方构建系统搏斗，也不再需要每隔几年就学习一个新的系统。</p>
<blockquote>
<p>对于早期职业生涯以Java和Ruby为背景的我而言，构建Go和Rust代码感觉就像从我的肩上卸下了一个不可能的重担。当我在谷歌工作时，遇到用Go编写的服务是一种解脱，因为我知道它很容易构建和运行。Rust也是如此，尽管我只在较小规模的Rust项目上工作过。我希望可无限配置的构建系统的时代已经过去了，所有语言都会有自己专门的构建工具，开箱即可使用。- <a href="https://samwho.dev/">山姆-罗斯</a></p>
</blockquote>
<h2>Rust还是Go？</h2>
<p>综上可知，这两种语言都设计得很好、很强大，那么你可能会想知道那些关于两门语言的“圣战”究竟是怎么回事（我也是）。为什么人们对“Go vs.Rust”如此大惊小怪，在社交媒体上大打出手，并且写长篇博文说只有傻瓜才会使用Rust，或者Go不是真正的编程语言，或者其他什么。</p>
<p>这可能会让他们感觉好些，但这并不能完全帮助你，因为你正试图决定在你的项目中使用哪种语言，或者你应该学习哪种语言来推动你的编程生涯。一个明智的人不会根据谁喊得声最大来做出重要的选择。</p>
<p>现在让我们继续我们成熟的讨论，看看在某些领域，一个有理智的人可能更喜欢哪一种语言。</p>
<h2>Go与Rust的性能对比</h2>
<p>我们已经说过，Go和Rust都能生产出高性能的程序，因为它们被编译成了本地机器代码，而不必通过解释器或虚拟机。</p>
<p>然而，Rust的性能尤其突出。它可以与C和C++相媲美，这两种语言通常被认为是性能最高的编译语言，但与这些老语言不同的是，Rust还提供了内存安全和并发安全，并且基本上不会给执行速度上带去没有任何开销。Rust还允许你创建复杂的抽象，而不需要在运行时付出任何性能上的代价。</p>
<p>相比之下，尽管Go程序的性能也非常好，但Go主要是为开发速度（包括编译）而设计的，而不是执行速度。Go程序员<a href="https://bitfieldconsulting.com/golang/slower">更倾向于清晰的代码而不是快速的代码</a>。</p>
<p>Go编译器也不会花很多时间去尝试生成最有效的机器代码；它更关心的是快速编译大量代码。所以Rust通常会在运行时基准测试中击败Go。</p>
<p>Rust的运行时性能也是一致和可预测的，因为它不使用垃圾收集。Go的垃圾收集器非常高效，并且经过优化，使其“STW(停止世界)”的停顿时间尽可能短（每一个新的Go版本都会越来越短）。但是垃圾收集不可避免地在程序的行为方式中引入了一些不可预测的因素，这在某些应用中可能是一个严重的问题，例如嵌入式系统。</p>
<p>因为Rust旨在让程序员完全控制底层硬件，所以有可能将Rust程序优化到相当接近机器的最大理论性能。这使得Rust在执行速度胜过所有其他考虑因素的领域是一个很好的选择，比如游戏编程、操作系统内核、网络浏览器组件和实时控制系统。</p>
<h2>简单性</h2>
<p>如果没有人能够弄清楚如何使用一种编程语言，那么这种语言有多快也无所谓。Go语言是为了应对C++等语言不断增长的复杂性而特意设计的；它的语法非常少，关键字也非常少，事实上，功能特性也很少。</p>
<p>这意味着<a href="http://gk.link/a/10AVZ">学习Go语言</a>不需要很长时间，就可以用它来编写有用的程序。</p>
<blockquote>
<p>Go是非常容易学习的。我知道这是一个经常被吹捧的好处，但我真的很惊讶于我能够如此迅速地提高工作效率。多亏了这个语言、文档和工具，我在两天后就写出了有趣的、可提交的代码。 &#8211; <a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">一个Rust程序员对Go的早期印象</a></p>
</blockquote>
<p>这里的关键词是<strong>简单性</strong>。当然，简单并不等同于容易，但是小而简单的语言比大而复杂的语言更容易学习。Go语言没有提供那么多不同的方法来做一件事情，所以所有写得好的Go代码往往看起来都一样。快速学习一个不熟悉的服务并理解它在做什么很容易。</p>
<pre><code>fmt.Println("Gopher's Diner Breakfast Menu")
for dish, price := range menu {
    fmt.Println(dish, price)
}
</code></pre>
<p>在我的<a href="https://bitfieldconsulting.com/code-club">代码俱乐部视频系列</a>中，我正是这样做的：从GitHub上半随机地挑选Go项目，并与一群Go初学者一起探索它们，看看我们能理解多少的代码。结果总是比我们预期的要多。</p>
<p>虽然核心语言很小，但Go的标准库却非常强大。这意味着你的学习曲线也需要包括你需要的标准库的部分，而不仅仅是Go语法。</p>
<p>另一方面，将功能从语言中转移到标准库中，意味着你可以只专注于学习与你现在相关的库。</p>
<p>Go也是为大规模的软件开发而设计的，支持有大型代码库的大型团队。在这种情况下，新的开发人员能够尽快上手是非常重要的。出于这个原因，Go社区十分看重：<a href="https://bitfieldconsulting.com/golang/commandments">简单、明显、常规、直接的程序</a>。</p>
<blockquote>
<p>使用Go，你可以快速完成工作。Go是我所使用过的生产力最高的语言之一。它的口号是：今天解决实际问题。 &#8211; <a href="https://endler.dev/2017/go-vs-rust/">马蒂亚斯-恩德勒</a></p>
</blockquote>
<h2>特性</h2>
<blockquote>
<p>Rust比其他几种编程语言支持更多的复杂语法特性，因此，你可以用它实现更多。 &#8211; <a href="https://devathon.com/blog/rust-vs-go-which-programming-language-to-choose/">devathon</a></p>
</blockquote>
<p>Rust是专门设计用来帮助程序员用最少的代码做最多的事情，它包括很多强大而有用的功能特性。例如，Rust的match功能可以让你以十分简洁地方式写出灵活的、富有表现力的逻辑：</p>
<pre><code>fn is_prime(n: u64) -&gt; bool {
    match n {
        0...1 =&gt; false,
        _ =&gt; !(2..n).any(|d| n % d == 0),
    }
}
</code></pre>
<p>因为Rust做了很多事情，这意味着有很多东西需要学习，特别是在开始的时候。但这没关系：在C++或Java中也有很多东西要学，而且你不会得到Rust的高级特性，比如内存安全。</p>
<p>批评Rust是一种复杂的语言忽略了一点：它被设计成具有表现力，这意味着有很多功能，而在许多情况下，这正是你想要的编程语言。</p>
<p>当然，Rust有一个学习曲线，但一旦你开始使用它，你就会好起来。</p>
<blockquote>
<p>对于那些准备接受更复杂的语法和语义（以及可能更高的可读性成本）以换取最大可能的性能的程序员来说，Rust将与C++和D语言争夺思想份额。 &#8211; <a href="https://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors">戴夫-切尼</a></p>
</blockquote>
<p>虽然Rust采用了Go的一些特性，而Go也在采用Rust的一些特性（尤其是<a href="https://bitfieldconsulting.com/golang/generics">泛型</a>），但可以说Rust的特性很重，而Go的特性相对较轻。</p>
<h2>并发</h2>
<p>大多数语言都对并发编程（同时做多件事情）有某种形式的支持，但Go从一开始就是为这项工作而设计的。Go不使用操作系统的线程，而是提供了一个轻量级的替代方案：<strong>goroutine</strong>。</p>
<p>每个goroutine是一个独立执行的Go函数，Go调度器会将其映射到其控制下的一个操作系统线程中。这意味着调度器可以非常有效地管理大量并发的goroutine，只使用有限的操作系统线程。</p>
<p>因此，你可以在一个程序中运行数百万个并发的goroutine，而不会产生严重的性能问题。这使得Go成为高规模并发应用程序的完美选择，如网络服务器和微服务。</p>
<p>Go还具有快速、安全、高效的功能特性，可以使用channel让goroutines进行通信和共享数据。Go的并发支持感觉设计得很好，使用起来也很愉快。</p>
<p>一般来说，对并发程序进行推断是很难的，而且在任何语言中建立可靠、正确的并发程序都是一个挑战。但由于它从一开始就内置于语言中，而不是事后才想到的，Go中的并发编程是最简单、最完整的。</p>
<blockquote>
<p>Go语言可以很容易地建立一个很好的多因素的应用程序，充分利用并发性，同时作为一组微服务进行部署。Rust也可以做这些事情，但可以说它更难。 在某些方面，Rust对防止与内存有关的安全漏洞的痴迷意味着程序员必须不遗余力地执行那些在其他语言（包括Go）中会更简单的任务。 &#8211; <a href="https://sdtimes.com/softwaredev/the-developers-dilemma-choosing-between-go-and-rust/">Sonya Koptyev</a></p>
</blockquote>
<p>相比之下，Rust中的并发故事是非常新的，而且还在稳定中，但它正处于非常积极的开发中，所以请关注这个领域。例如，Rust的<a href="https://github.com/rayon-rs/rayon">rayon库</a>提供了一种非常优雅和轻量级的方式来将顺序计算转化为并行计算。</p>
<blockquote>
<p>拥有goroutines和使用channel的轻量级语法真的很好。这真的显示了语法的力量，这些小细节使并发编程比其他语言感觉好得多 &#8211; <a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">一个Rust程序员对Go的早期印象</a></p>
</blockquote>
<p>虽然在Rust中实现并发程序可能不那么简单，但还是有可能的，而且这些程序可以利用Rust的安全保证。</p>
<p>一个很好的例子是标准库的Mutex类：在Go中，你可以忘记在访问某些东西之前获得一个Mutex锁，但Rust不会让你这样做。</p>
<blockquote>
<p>Go专注于将并发性作为一个一等公民的概念。这并不是说你不能在Rust中找到Go的面向actor的并发性，但这是留给程序员的一个练习。 &#8211; <a href="https://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors">Dave Cheney</a></p>
</blockquote>
<h2>安全</h2>
<p>我们在前面看到，Go和Rust都以不同的方式来防止一大类与内存管理有关的常见编程错误。但是Rust尤其努力确保你不会做一些你不想做的不安全的事情。</p>
<blockquote>
<p>Rust的编译器非常严格和学究派，它检查你使用的每个变量和你引用的每个内存地址。它避免了可能的数据竞争条件，并告知你未定义的行为。并发和内存安全问题在Rust的安全子集中根本不可能发生。 &#8211; <a href="https://bitbucket.org/blog/why-rust">为什么是Rust？</a></p>
</blockquote>
<p>这将使Rust编程成为与几乎所有其他语言不同的体验，而且一开始可能是一种挑战。但对很多人来说，这种辛苦是值得的。</p>
<blockquote>
<p>对我来说，Rust的关键优势是一种感觉，即编译器是我的后盾，不会让它可能检测到的任何错误通过（说真的，有时感觉就像魔法一样）。 &#8211; Grzegorz Nosek</p>
</blockquote>
<p>包括Go在内的许多语言都有帮助程序员避免错误的设施，但Rust将这一点提高到了一个新的水平，因此可能不正确的程序甚至不会被编译。</p>
<blockquote>
<p>有了Rust，库程序员有很多工具来防止他/她的用户犯错。Rust让我们有能力说，我们拥有一块特定的数据；其他东西不可能声称拥有，所以我们知道没有其他东西能够修改它。我想不出以前有什么时候我被赋予过这么多工具来防止意外的误用。这是一种奇妙的感觉。 &#8211; <a href="https://samwho.dev/">山姆-罗斯</a></p>
</blockquote>
<p>“与借用检查器(borrow checker)斗争”是Rust程序员新手的常见综合症，但在大多数情况下，它所发现的问题是你的代码中真正的bug（或至少是潜在的bug）。它可能会迫使你从根本上重构你的程序，以避免遇到这些问题；而当正确性和可靠性是你的首要任务时，这是件好事。</p>
<p>一个不改变你编程方式的语言有什么意义呢？当你用其他语言工作时，Rust所教授的关于安全的课程也是有用的。</p>
<blockquote>
<p>如果你选择了Rust，通常你需要该语言提供的保证：针对空指针和数据竞争的安全，可预测的运行时行为，以及对硬件的完全控制。如果你不需要这些功能，Rust可能是你下一个项目的糟糕选择。这是因为这些保证是有代价的：入门时间。你需要戒掉坏习惯，学习新概念。有可能的是，当你开始的时候，你会经常和借用检查器斗争。 &#8211; <a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a></p>
</blockquote>
<p>你觉得Rust的编程模型有多大的挑战性，可能取决于你以前有哪些其他语言的经验。Python或Ruby程序员可能会发现它的限制性；其他人会很高兴。</p>
<blockquote>
<p>如果你是一个花了几周的时间来追寻内存安全漏洞的C/C++程序员，你会非常欣赏Rust。”与借用检查器斗争”变成了”编译器可以检测到这个？酷！” -Grzegorz Nosek</p>
</blockquote>
<h2>规模化</h2>
<blockquote>
<p>今天的服务器程序由数千万行代码组成，由数百甚至数千名程序员进行构建，而且每天都在更新。Go的设计和开发是为了使在这种环境中工作更有成效。Go的设计考虑包括严格的依赖性管理，随着系统的发展，软件架构的适应性，以及组件之间的健壮性。 &#8211; <a href="https://talks.golang.org/2012/splash.article">Rob Pike</a></p>
</blockquote>
<p>当你一个人或在小团队中处理问题时，选择简单的语言还是功能丰富的语言是一个偏好的问题。但是当软件越来越大，越来越复杂，团队越来越大时，差异就开始显现出来了。</p>
<p>对于大型应用程序和分布式系统来说，执行速度不如开发速度重要：像Go这样刻意简化的语言可以减少新开发人员的启动时间，并使他们更容易处理大型代码库的工作。</p>
<blockquote>
<p>有了Go，作为初级开发者更容易提高工作效率，而作为中级开发者则更难引入会导致后续问题的脆弱抽象。由于这些原因，Rust在企业软件开发方面不如Go有说服力。 &#8211; <a href="https://kristoff.it/blog/why-go-and-not-rust">Loris Cro</a></p>
</blockquote>
<p>当涉及到大型的软件开发时，清晰的比聪明的好。Go的局限性实际上使它比Rust等更复杂和强大的语言更适合企业和大机构。</p>
<h2>Rust和Go的不同点</h2>
<p>虽然Rust和Go都是流行的、现代的、广泛使用的语言，但它们并不是真正的竞争对手，因为它们故意针对的是完全不同的使用情况。</p>
<p><a href="https://tonybai.com/2022/09/25/the-tao-of-go">Go的整个编程方法</a>与Rust的完全不同，每一种语言都适合一些人，同时也会刺激另一些人。这完全没问题，如果Rust和Go都能以或多或少相同的方式做同样的事情，我们就不会真的需要两种不同的语言。</p>
<p>那么，我们是否可以通过发现Rust和Go所采取的截然不同的方法来了解它们各自的本性呢？让我们拭目以待。</p>
<h3>垃圾回收</h3>
<p>“要不要垃圾回收”是一个没有正确答案的问题。垃圾回收，以及一般的自动内存管理，使得开发可靠、高效的程序变得快速和容易，对于一些人来说，这至关重要。</p>
<p>但也有人说，垃圾回收及其性能开销和停顿，使程序在运行时表现得不可预测，并引入了不可接受的延迟。争论还在继续。</p>
<blockquote>
<p>Go是一种与Rust非常不同的语言。虽然两者都可以被模糊地描述为系统语言或C语言的替代品，但它们有不同的目标和应用、语言设计的风格以及优先级。垃圾回收是一个真正巨大的区别。Go中的GC使语言更简单，更小，更容易推理。在Rust中没有GC会让它变得非常快（尤其是当你需要保证延迟，而不仅仅是高吞吐量的时候），并且可以实现Go中不可能实现的功能和编程模式（或者至少是在不牺牲性能的情况下）。 &#8211; <a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">PingCAP</a></p>
</blockquote>
<h3>接近机器</h3>
<p>计算机编程的历史是一个越来越复杂的抽象的故事，它让程序员在解决问题时不用太担心底层机器的实际运作。</p>
<p>这使得程序更容易编写，也许更容易移植。但是对于许多程序来说，对硬件的访问以及对程序执行方式的精确控制更为重要。</p>
<p>Rust的目标是让程序员“更接近机器”，有更多的控制权，但Go抽象了架构细节，让程序员更接近问题。</p>
<blockquote>
<p>两种语言都有不同的适用范围。Go在编写微服务和典型的”DevOps”任务方面表现出色，但它不是一种系统编程语言。Rust对于那些看重并发性、安全性和性能的任务中更强；但它的学习曲线比Go更陡峭。 &#8211; <a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a></p>
</blockquote>
<h3>必须运行更快</h3>
<p>许多人同意，对于大多数程序来说，<a href="https://bitfieldconsulting.com/golang/slower">性能不如可读性重要</a>。但当性能确实重要时，它真的很重要。Rust做了一些设计上的权衡，以达到尽可能好的执行速度。</p>
<p>相比之下，Go更关注简单性，它愿意为此牺牲一些（运行时）性能。但是Go的构建速度是无可匹敌的，这对于大型代码库来说是非常重要的。</p>
<blockquote>
<p>Rust比Go快。在基准测试中，Rust更快，在某些情况下，甚至是数量级的快。但在你选择用Rust写所有东西之前，考虑一下Go在许多基准测试中并不落后于它，而且它仍然比Java、C#、JavaScript、Python等快得多。如果你需要的是顶级的性能，那么选择这两种语言中的任何一种，你都会在游戏中领先。如果你正在构建一个处理高负载的网络服务，你希望能够在纵向和横向上进行扩展，那么这两种语言都会非常适合你。- <a href="https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9">安德鲁-拉德</a></p>
</blockquote>
<h3>正确性</h3>
<p>另一方面，如果一个程序不需要正常工作的话，它可以任意地快。大多数代码不是为长期而写的，但有些程序能在生产中运行多长时间往往是令人惊讶的：在某些情况下，可以保持几十年。</p>
<p>在这种情况下，值得在开发中多花一点时间，以确保程序的正确性、可靠性，并在未来不需要大量的维护。</p>
<p>Go和Rust都旨在帮助你编写正确的程序，但方式不同。例如，Go提供了一个极好的内置测试框架，而Rust则专注于使用其借用检查器消除运行时的错误。</p>
<blockquote>
<p>我认为。Go适用于明天必须交付的代码，而Rust适用于必须在未来五年内保持运行不动的代码。 &#8211; Grzegorz Nosek</p>
</blockquote>
<p>虽然Go和Rust对于任何严肃的项目来说都是很好的选择，但是让自己尽可能地了解每种语言及其特点是一个好主意。</p>
<p>归根结底，别人怎么想并不重要：只有你能决定哪种语言适合你和你的团队。</p>
<blockquote>
<p>如果你想加快开发速度，也许是因为你有许多不同的服务需要编写，或者你有一个庞大的开发团队，那么Go是你的首选语言。Go把并发性作为第一等公民给你，并且不容忍不安全的内存访问（Rust也是如此），但不强迫你管理每一个细节。Go是快速和强大的，但它避免了使开发者陷入困境，而是专注于简单性和统一性。如果在另一方面，拧出每一盎司的性能是必要的，那么Rust应该是你的选择。 &#8211; <a href="https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9">安德鲁-拉德</a></p>
</blockquote>
<h2>结论</h2>
<p>我希望这篇文章能让你相信Rust和Go都值得你认真考虑。如果可能的话，你应该争取在这两种语言中至少获得一定程度的经验，因为它们对你的任何技术职业都会有极大的帮助，甚至如果你仅把编程作为一种业余爱好的话。</p>
<p>如果你只有时间投资学习一门语言，在你将Go和Rust用于各种不同类型的大小程序之前，不要做出最终决定。</p>
<p>而编程语言的知识实际上只是成为一名成功的软件工程师的一小部分。到目前为止，你需要的最重要的技能是设计、工程、架构、沟通和协作。如果你在这些方面表现出色，无论你选择哪种语言，你都会成为一名优秀的软件工程师。学习愉快!</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/02/22/rust-vs-go-in-2023/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Prometheus采不到数据了！居然是Prometheus client包的锅</title>
		<link>https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package/</link>
		<comments>https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package/#comments</comments>
		<pubDate>Wed, 15 Jun 2022 14:25:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[continus-profiling]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[fd]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[grafana]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[metrics]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[proc]]></category>
		<category><![CDATA[profiling]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[pull]]></category>
		<category><![CDATA[pyroscope]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[replace]]></category>
		<category><![CDATA[Ruby]]></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=3588</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package 在基于eBPF的新一代观测设施尚未成熟之前，我们采用了业界成熟的Prometheus+Grafana方案采集节点与应用度量指标(metrics)信息。众所周知，这样的方案是一种对应用有侵入的方案，即需要在应用内部嵌入采集度量信息及与Prometheus通信的client包。 Prometheus官方提供并维护了主流语言的client包，包括Go、Java、Python、Ruby、Rust等，如下图： Prometheus的go client端使用起来也不算复杂，总共分两步： 把你要获取的度量指标注册(Register)到Prometheus的Registry中； 建立起一个HTTP Server，暴露度量指标采集端口即可。 Prometheus采用拉模型(pull)收集时序度量数据，数据拉取行为是由Prometheus服务端来决定的，比如可以设定Prometheus拉取各个采集点的时间周期。 一般来说，这个技术栈已经很成熟，配置完启动后，马上就能看到效果。这个技术栈也很稳定，我们使用后一直运行良好，直到本周压测时遇到一个问题：Prometheus采不到数据了！ 从最初的数据由连续的线变成“断断续续”的点，见下图： 到后来干脆就无法采到任何数据了： 之前Prometheus跑的好好的，为什么现在却采不到数据了呢？这次与之前的不同之处在于我们的压测用例情景下，每个服务节点都要建立百万以上的连接，而之前仅仅是10w左右的量级。 好在我们部署了在线Continuous Profiling工具，可以查看一下压测那段时间的资源占用，如下图： 上面是一个alloc object的火焰图，我们看到Prometheus client的Registry.Gather方法占了50%的内存分配开销，这是很不正常的。继续沿着Gather函数的火焰图看，我们看到底端居然是readdir。我们应用注册的度量指标也没有哪个采集时需要readdir啊！ 要想解决这个问题只有翻Prometheus client源码了！ 我们使用的是prometheus client端的默认defaultRegistry。 从源码中可以看到：这个defaultRegistry在初始化时，默认注册了两个collector: // registry.go func init() { MustRegister(NewProcessCollector(ProcessCollectorOpts{})) MustRegister(NewGoCollector()) } 我们发现：第一个processCollector会采集如下度量指标数据： // process_collector.go func (c *processCollector) Describe(ch chan&#60;- *Desc) { ch &#60;- c.cpuTotal ch &#60;- c.openFDs ch &#60;- c.maxFDs ch &#60;- c.vsize [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package">本文永久链接</a> &#8211; https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package</p>
<p>在基于eBPF的新一代观测设施尚未成熟之前，我们采用了业界成熟的<a href="https://prometheus.io/">Prometheus</a>+<a href="https://grafana.com">Grafana</a>方案采集节点与应用<a href="https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics">度量指标(metrics)信息</a>。众所周知，这样的方案是一种<strong>对应用有侵入的方案</strong>，即需要在应用内部嵌入采集度量信息及与Prometheus通信的client包。</p>
<p>Prometheus官方提供并维护了主流语言的client包，包括Go、Java、Python、Ruby、<a href="https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together">Rust</a>等，如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-2.png" alt="" /></p>
<p>Prometheus的go client端使用起来也不算复杂，总共分两步：</p>
<ul>
<li>把你要获取的度量指标注册(Register)到Prometheus的Registry中；</li>
<li>建立起一个HTTP Server，暴露度量指标采集端口即可。</li>
</ul>
<p>Prometheus采用拉模型(pull)收集时序度量数据，数据拉取行为是由Prometheus服务端来决定的，比如可以设定Prometheus拉取各个采集点的时间周期。 一般来说，这个技术栈已经很成熟，配置完启动后，马上就能看到效果。这个技术栈也很稳定，我们使用后一直运行良好，直到本周压测时遇到一个问题：<strong>Prometheus采不到数据了</strong>！</p>
<p>从最初的数据由连续的线变成“断断续续”的点，见下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-3.png" alt="" /></p>
<p>到后来干脆就无法采到任何数据了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-4.png" alt="" /></p>
<p>之前Prometheus跑的好好的，为什么现在却采不到数据了呢？这次与之前的不同之处在于我们的压测用例情景下，每个服务节点都要建立百万以上的连接，而之前仅仅是10w左右的量级。</p>
<p>好在我们部署了<a href="https://github.com/pyroscope-io/pyroscope">在线Continuous Profiling工具</a>，可以查看一下压测那段时间的资源占用，如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-5.png" alt="" /></p>
<p>上面是一个alloc object的火焰图，我们看到Prometheus client的Registry.Gather方法占了50%的内存分配开销，这是很不正常的。继续沿着Gather函数的火焰图看，我们看到底端居然是<strong>readdir</strong>。我们应用注册的度量指标也没有哪个采集时需要readdir啊！</p>
<p>要想解决这个问题只有<strong>翻Prometheus client源码</strong>了！</p>
<p>我们使用的是prometheus client端的默认defaultRegistry。 从源码中可以看到：这个defaultRegistry在初始化时，默认注册了两个collector:</p>
<pre><code>// registry.go
func init() {
    MustRegister(NewProcessCollector(ProcessCollectorOpts{}))
    MustRegister(NewGoCollector())
}
</code></pre>
<p>我们发现：第一个processCollector会采集如下度量指标数据：</p>
<pre><code>// process_collector.go
func (c *processCollector) Describe(ch chan&lt;- *Desc) {
    ch &lt;- c.cpuTotal
    ch &lt;- c.openFDs
    ch &lt;- c.maxFDs
    ch &lt;- c.vsize
    ch &lt;- c.maxVsize
    ch &lt;- c.rss
    ch &lt;- c.startTime
}
</code></pre>
<p>在采集openFDs时，processCollector遍历了/proc/{pid}下面的fd目录：</p>
<pre><code>// process_collector_other.go
func (c *processCollector) processCollect(ch chan&lt;- Metric) {
    pid, err := c.pidFn()
    if err != nil {
        c.reportError(ch, nil, err)
        return
    }

    p, err := procfs.NewProc(pid)
    if err != nil {
        c.reportError(ch, nil, err)
        return
    }

    if stat, err := p.Stat(); err == nil {
        ch &lt;- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
        ch &lt;- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
        ch &lt;- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
        if startTime, err := stat.StartTime(); err == nil {
            ch &lt;- MustNewConstMetric(c.startTime, GaugeValue, startTime)
        } else {
            c.reportError(ch, c.startTime, err)
        }
    } else {
        c.reportError(ch, nil, err)
    }

    if fds, err := p.FileDescriptorsLen(); err == nil { // 这里获取openFDs
        ch &lt;- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
    } else {
        c.reportError(ch, c.openFDs, err)
    }

    if limits, err := p.Limits(); err == nil {
        ch &lt;- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
        ch &lt;- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
    } else {
        c.reportError(ch, nil, err)
    }
}
</code></pre>
<p>采集openFDS时，processCollector调用了FileDescriptorsLen方法，在FileDescriptorsLen方法调用的fileDescriptors方法中，我们找到了对Readdirnames的调用，见下面源码片段：</p>
<pre><code>// github.com/prometheus/procfs/proc.go

// FileDescriptorsLen returns the number of currently open file descriptors of
// a process.
func (p Proc) FileDescriptorsLen() (int, error) {
    fds, err := p.fileDescriptors()
    if err != nil {
        return 0, err
    }

    return len(fds), nil
}  

func (p Proc) fileDescriptors() ([]string, error) {
    d, err := os.Open(p.path("fd"))
    if err != nil {
        return nil, err
    }
    defer d.Close()

    names, err := d.Readdirnames(-1) // 在这里遍历目录中的文件
    if err != nil {
        return nil, fmt.Errorf("could not read %q: %w", d.Name(), err)
    }

    return names, nil
}
</code></pre>
<p>通常情况下，读取/proc/{pid}/fd目录是没有问题的，但当我们的程序上连接着100w+的连接时，意味着fd目录下有100w+的文件，逐一遍历这些文件将带来很大的开销。这就是导致Prometheus在超时时间(通常是10几秒)内无法及时采集到数据的原因。</p>
<p>那么如何解决这个问题呢？</p>
<p>临时解决方法是将registry.go文件的init函数中的MustRegister(NewProcessCollector(ProcessCollectorOpts{}))这一行注释掉！这个进程度量指标信息对我们用处不大。不过这样做的不足是我们自己需要维护一份prometheus golang client包，需要用到go mod replace，十分不便，并且不便于prometheus golang client包的版本升级。</p>
<p>一劳永逸的解决方法是：<strong>不使用默认Registry，而是使用NewRegistry函数新建一个Registry</strong>。这样我们抛开默认注册的那些度量指标，并可以自行定义我们要注册的度量指标。需要时，我们也可以将ProcessCollector加进来，这个根据不同Go程序的需要而定。</p>
<p>按这个方案修改后，那些熟悉的连续曲线就又重现在眼前了！</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>博客：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; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>手把手教你使用ANTLR和Go实现一门DSL语言（第一部分）：设计DSL语法与文法</title>
		<link>https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1/</link>
		<comments>https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1/#comments</comments>
		<pubDate>Mon, 23 May 2022 21:38:36 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[algol]]></category>
		<category><![CDATA[ANTLR]]></category>
		<category><![CDATA[antlr4]]></category>
		<category><![CDATA[ANTLR4权威指南]]></category>
		<category><![CDATA[ast]]></category>
		<category><![CDATA[bison]]></category>
		<category><![CDATA[BNF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[CFG]]></category>
		<category><![CDATA[cfront]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[CSG]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[EBNF]]></category>
		<category><![CDATA[flex]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Grammar]]></category>
		<category><![CDATA[grun]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[lex]]></category>
		<category><![CDATA[lexer]]></category>
		<category><![CDATA[Listener]]></category>
		<category><![CDATA[literal]]></category>
		<category><![CDATA[MartinFowler]]></category>
		<category><![CDATA[metric]]></category>
		<category><![CDATA[OTA]]></category>
		<category><![CDATA[parser]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[rule]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[syntax]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[variable]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[yacc]]></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>
		<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=3552</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1 在《使用ANTLR和Go实现DSL入门》一文中，我们了解了DSL与通用编程语言(GPL)的差异、DSL解析器生成工具选择以及ANTLR文法的简要书写规则，并和大家一起完成了一个CSV解析器的例子。看完上述文章后，你是不是有了打造属于自己的DSL的冲动了呢！ 那么究竟该如何设计和实现一门自己的DSL呢？在这个系列文章中，我将“手把手”地和大家一起看看设计和实现一门DSL(这里主要指外部DSL)的全流程。 结合Martin Fowler在《领域特定语言》一书中的建议，我将设计与实现一门外部DSL的过程分为如下几个步骤： 图：外部DSL设计与实现的步骤 本文是系列文章的第一篇，在这一篇中，我将先来说说前三个步骤，即为某一特定领域设计一门DSL的语法(syntax)、并编写可以解析该DSL的ANTLR文法(grammar)，生成该DSL语法的解析器并验证ANTLR文法的正确性。 到这里有朋友可能会问：“一会儿文法，一会儿又语法，它们到底有啥区别？”，别急！在设计这门DSL语法之前，我先来和大家一起简单理解一下文法与语法的区别。 一. 文法(grammar)和语法(syntax) 图：文法与语法的比较 如上图所示，语法是面向使用该编程语言的应用开发者的，就像Go语法面向的是Gopher；而文法则是面向这门编程语言的编译器或解释器(Interpreter)的核心开发者的。 我们通常用自然语言描述编程语言的语法，这样的文档一般被称为该编程语言的语言规范(language specification)，比如用于描述Go语法的Go语言规范。 但自然语言通常是不精确的，有时带有歧义。为了给出更为精确的语法描述，编程语言规范通常也会有采用某种形式语言(比如：EBNF)表示的关于这门语言语法所对应的文法，比如在Go语言规范中，我们就能看到用EBNF所描述的文法： SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } . PackageClause = "package" PackageName . PackageName = identifier . ImportDecl = "import" ( ImportSpec &#124; "(" { ImportSpec ";" } ")" ) . ImportSpec = [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1">本文永久链接</a> &#8211; https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1</p>
<p>在<a href="https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go">《使用ANTLR和Go实现DSL入门》</a>一文中，我们了解了DSL与通用编程语言(GPL)的差异、DSL解析器生成工具选择以及ANTLR文法的简要书写规则，并和大家一起完成了一个CSV解析器的例子。看完上述文章后，你是不是有了<strong>打造属于自己的DSL的冲动</strong>了呢！</p>
<p>那么究竟该如何设计和实现一门自己的DSL呢？在这个系列文章中，我将“手把手”地和大家一起看看设计和实现一门DSL(这里主要指<strong>外部DSL</strong>)的全流程。</p>
<p>结合Martin Fowler在<a href="https://book.douban.com/subject/21964984/">《领域特定语言》</a>一书中的建议，我将设计与实现一门外部DSL的过程分为如下几个步骤：</p>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-2.png" alt="" /><br />
<center>图：外部DSL设计与实现的步骤</center></p>
<p>本文是系列文章的第一篇，在这一篇中，我将先来说说前三个步骤，即为某一特定领域设计一门DSL的语法(syntax)、并编写可以解析该DSL的ANTLR文法(grammar)，生成该DSL语法的解析器并验证ANTLR文法的正确性。</p>
<p>到这里有朋友可能会问：“一会儿文法，一会儿又语法，它们到底有啥区别？”，别急！在设计这门DSL语法之前，我先来和大家一起简单理解一下文法与语法的区别。</p>
<h3>一. 文法(grammar)和语法(syntax)</h3>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-3.png" alt="" /><br />
<center>图：文法与语法的比较</center></p>
<p>如上图所示，<strong>语法是面向使用该编程语言的应用开发者的</strong>，就像Go语法面向的是Gopher；而<strong>文法则是面向这门编程语言的编译器或解释器(Interpreter)的核心开发者的</strong>。</p>
<p>我们<strong>通常用自然语言描述编程语言的语法</strong>，这样的文档一般被称为该<strong>编程语言的语言规范(language specification)</strong>，比如用于描述Go语法的<a href="https://go.dev/ref/spec">Go语言规范</a>。</p>
<p>但自然语言通常是不精确的，有时带有歧义。为了给出更为精确的语法描述，编程语言规范通常也会有采用某种形式语言(比如：EBNF)表示的关于这门语言语法所对应的文法，比如在Go语言规范中，我们就能看到用EBNF所描述的文法：</p>
<pre><code>SourceFile       = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
PackageClause    = "package" PackageName .
PackageName      = identifier .
ImportDecl       = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec       = [ "." | PackageName ] ImportPath .
ImportPath       = string_lit .
... ...
</code></pre>
<p>通常应用开发人员是不会关心这些夹带在语言规范文档中的文法描述的，只有当规范中的说明有歧义时，开发人员才会根据文法中的产生式规则去推导语法的合规形式的，当然了这一推导过程是比较“痛苦”的。</p>
<p>到这里，结合我们在<a href="https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go">《使用ANTLR和Go实现DSL入门》</a>一文中的说明，我们进一步明确了<strong>文法就是一组规则</strong>，这组规则告诉我们如何将文本流转换为语法树。如果转换失败，说明文本流中存在不符合编程语言语法的地方。</p>
<p>此外，<strong>用于描述一门编程语言语法的文法可以不止一种</strong>，每种形式语言工具都有自己的表示形式，比如针对Go语言语法，我们可以使用EBNF给出形式化的文法，也可以使用ANTLR专用的形式化文法。</p>
<p>到这里，你对文法与语法的概念是不是更深刻一些了呢！不过这时可能会有朋友站出来提问：<strong>设计一门编程语言或DSL，是先设计语法还是先设计文法呢</strong>？</p>
<p>在语言设计伊始，<strong>语法和文法设计的边界其实并非那么清晰</strong>。讨论语法是为了确定文法做准备，而一旦确定了一版文法，语法的使用形式又被进一步精确了。<strong>在编程语言/DSL设计过程中，语法与文法是交替螺旋上升的</strong>。简单的DSL语言，可能一轮迭代就完成了全部设计。复杂的通用编程语言可能要反复针对语法讨论多次，确定下来后，才会编写出新一版本的文法，依次反复迭代。</p>
<p>不过<strong>通常来说我们会先确定一版语言的语法</strong>，写出一些采用此版语言语法的样例源文件，供后续文法以及生成的解析器(Parser)验证使用。回顾Go语言的历史，我们会发现Go语言创世团队当初也是这么做的。Robert Griesemer、Rob Pike和Ken Thompson这三位大佬在Google总部的一间会议室里首先进行了一场有关Go具体设计的会议。会后的第二天，Robert Griesemer发出了一封题为“prog lang discussion”的电邮，这封电邮便成为了这门新语言的第一版设计稿，三位大佬在这门语言的一些<strong>基础语法特性与形式</strong>上达成了初步一致：</p>
<pre><code>Date: Sun, 23 Sep 2007 23:33:41 -0700
From: "Robert Griesemer" &lt;gri@google.com&gt;
To: "Rob 'Commander' Pike" &lt;r@google.com&gt;, ken@google.com
Subject: prog lang discussion
...
*** General:
Starting point: C, fix some obvious flaws, remove crud, add a few missing features
  - no includes, instead: import
  - no macros (do we need something instead?)
  - ideally only one file instead of a .h and .c file, module interface
should be extracted automatically
  - statements: like in C, though should fix 'switch' statement
  - expressions: like in C, though with caveats (do we need ',' expressions?)
  - essentially strongly typed, but probably w/ support for runtime types
  - want arrays with bounds checking on always (except perhaps in 'unsafe mode'-see section on GC)
  - mechanism to hook up GC (I think that most code can live w/ GC, but for a true systems
    programming language there should be mode w/ full control over memory allocation)
  - support for interfaces (differentiate between concrete, or implementation types, and abstract,
    or interface types)
  - support for nested and anonymous functions/closures (don't pay if not used)
  - a simple compiler should be able to generate decent code
  - the various language mechanisms should result in predictable code
...

</code></pre>
<p>基于这版设计，2008年初，Unix之父Ken Thompson实现了第一版Go编译器(文法相关)，用于验证之前的语法设计。</p>
<p>好了，在理解了文法与语法的区别后，接下来，我们就来为某一特定领域创建一门DSL语言，我们先来介绍一下这门DSL的背景与语法设计。</p>
<blockquote>
<p>注：以上提到的对文法与语法的理解仅限于计算机编程语言领域，并不一定适合自然语言领域(自然语言领域也有文法与语法的概念)。</p>
</blockquote>
<h3>二. 为《后天》中的气象学家设计一门DSL</h3>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-4.jpeg" alt="" /></p>
<blockquote>
<p>注：下面只是一个虚构的领域例子，大家无需在其合理性、可行性、科学性与严谨性上产生质疑:)。</p>
</blockquote>
<p>如果你看过灾难片专业户罗兰·艾默里奇指导的美国灾难题材电影<a href="https://movie.douban.com/subject/1308779/">《后天》</a>，你肯定会对电影里发生的威胁人类文明的灾难情节记忆犹新。不过《后天》里的情节其实离我们并不“遥远”，尤其是进入二十一世纪以来，极端异常天气在全球各个地区屡屡发生：两极高温冰川消融、北美陆地飓风以及我国2021年华北地区的极端降水等等。各国的气象学家、地球物理科学家们都在努力破解这些极端天气背后的原因，并预测全球气候的走势。他们在全球设置了诸多气象数据的采集装置，就像《后天》中部署在大西洋上的浮标那样，7&#215;24小时地监视着“地球的生命体征”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-6.jpeg" alt="" /></p>
<p>像浮标这样的采集装置内置采集程序，按照设定的规则定期向中心上报数据或发送异常事件信息。不过浮标一般都是无人值守的，一旦投放，便很难维护。一旦要进行程序升级，比如更新采集数据与上报事件的规则，就比较麻烦了。</p>
<p>如果我们为像浮标这样的采集装置设计一门DSL，让这些装置内置某种DSL引擎，这样变更采集和报警规则只需给装置远程传送一个极小数据量的规则文件即可完成升级，采集装置将按照新规则上报数据和事件。</p>
<p>好了，领域背景介绍完了，下面我们就来为气象学家们分忧，帮助他们设计一门DSL语言，用于“指挥”像浮标这样的数据收集装置按照气象学家们设定的规则上报数据与事件。</p>
<h3>三. DSL语言的语法样例</h3>
<p>我们先来构思一下这门DSL的语法。什么样的DSL是好DSL？没有固定的评价标准。</p>
<ul>
<li>自然语言 vs. 编程语言</li>
</ul>
<p>有人说DSL是给领域专家用的，应该更贴近自然语言一些，但实际情况是DSL更多还是开发人员/测试人员去写，或有开发经验的领域专家使用。所以在<a href="https://martinfowler.com/books/dsl.html">《领域特定语言》</a>一书的第二章末尾，Martin Fowler给出关于DSL的特别警示：<strong>不要试图让DSL读起来像自然语言。牢记，DSL是一种程序设计语言</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-5.png" alt="" /></p>
<p>使用DSL更像是在编程，而不是写小说。同自然语言相比，像DSL这样的程序设计语言的目标是<strong>简洁、清晰与精确</strong>。</p>
<ul>
<li>一门大的DSL vs. 多门小的DSL</li>
</ul>
<p>DSL正如其名，是领域相关的。绝大多数DSL都是非常简单、非常小的“编程语言”，比如一个算术表达式求值语言，再比如DSL一书中格兰特小姐的控制器状态机等。</p>
<p>但DSL始终存在演化成庞然大物-一门图灵完备的通用编程语言的风险，这个是要极力避免的。那么怎么识别这种风险呢？Martin Fowler告诉我们：<strong>如果一个系统整体都是用一门DSL实现的，那么这门DSL就成为了事实上的通用编程语言了</strong>。更佳的作法是<strong>切分领域，为不同领域构建不同的DSL，而不要构建一门DSL用于所有领域</strong>。</p>
<p>好了，到这里我们先了解一下虚构例子的领域需求，我们需要为这样的一个无人值守的海洋浮标设备设计一门DSL，DSL可用于描述采集设备数据采集与上报的规则。</p>
<p>科学家们对设备的采集能力描述如下：</p>
<ul>
<li>可通过传感器周期性(默认间隔一分钟)获取所在坐标位置的大气温度、水温、水流速、盐度、&#8230;.等物理指标；</li>
<li>可对传感器实时获取到的各种物理指标信息进行一元运算(向下取整、向上取整、绝对值)、算术运算(加减乘除取模)、关系运算(大于、小于&#8230;)、逻辑运算(与、或) ，构造混合这些运算的条件，当条件为真时，上报指定的物理指标信息；</li>
<li>可结合采集设备缓存的历史时刻数据(缓存能力有限，最大300分钟，即300条数据)进行综合条件判定，这里将其定义为<strong>窗口计算</strong>，判定策略包括：都不满足、全部满足和至少一项满足。</li>
</ul>
<p>面对这样的需求，我们怎么定义DSL的语法呢？外部DSL的语法设计往往会受到设计者对以往的编程语言的使用经验的影响。很多开发人员都会从自己熟悉的编程语言的语法中“借鉴”一些语法元素来构成自己的DSL。下面是我设计的一组语法样例：</p>
<pre><code>r0001: Each { || $speed &gt; 30 } =&gt; ("speed", "temperature", "salinity");

r0002: None { |,30| $temperature &gt; 5 } =&gt; ("speed", "temperature", "salinity");

r0003: None { |3,| $temperature &gt; 10 } =&gt; ("speed", "temperature", "salinity");

r0004: Any { |11,30| ($speed &lt; 5) and ($temperature &lt; 2) and (roundUp($salinity) &lt; 600) } =&gt; ("speed", "temperature", "salinity");

r0005: Each { |,| (($speed &lt; 5) and (($temperature + 1) &lt; 10)) or ((roundDown($speed) &lt;= 10) and (roundUp($salinity) &gt;= 500))} =&gt; ();
</code></pre>
<p>到这里，一些童鞋会惊讶到DSL的简单，没错！就像前面所说的，<strong>DSL就应该简单、清晰和表意精确</strong>。</p>
<p>下面我来对上面的语法样例做个简单说明：</p>
<ul>
<li>一条规则占用一行，以ruleID开头，以分号结尾；</li>
<li>ruleID与rule body之间通过冒号分隔；</li>
<li>rule body借鉴了Ruby语言中的迭代器语法：</li>
</ul>
<pre><code>#!/usr/bin/ruby

a = [1,2,3,4,5]
b = Array.new
b = a.collect{ |x| x &lt;= 4 }
puts b

输出：

true
true
true
true
false
</code></pre>
<p>在上面ruby的这种迭代器语法中，collect迭代器会将迭代数组a中每个元素，并针对每个元素进行x &lt;= 4的求值，求值结果存储在b中对应的元素位置上。我借鉴了这种形式的语法，形成支持窗口计算和表达式求值的语法。以下面语法为例：</p>
<pre><code>r0001: Each { |1,5| $speed &gt; 30 } =&gt; ("speed", "temperature", "salinity");
</code></pre>
<p>这个规则的含义是：当窗口数据，从第1项到第5项数据中的speed指标都大于30时，输出并上报当前最新的speed、temperature和salinity指标数据。</p>
<p>Each是对窗口满足策略的判定，Each表示窗口数据中每一项都符合后面的条件表达式；其他两个判定词是None和Any，None表示窗口数据中没有一项满足后面的条件表达式；Any表示窗口数据中有一项满足后面的条件表达式即可。</p>
<p>Each后面的大括号中放置了窗口范围以及条件表达式。</p>
<p>两个竖线表示要参与求值的窗口数据，窗口表示的标准形式为|low, high|，low和high是下标值(下标从1开始)，表示的窗口范围为：&#91;low, high&#93;。当省略low时，比如：|, high|表示的窗口范围为|1, high|；当low与high相同时，比如：|n, n|表示只有下标为n这一个元素参与后续求值；当省略high时，比如：|low, |表示窗口范围为|low, max|，其中max为默认设置的窗口的大小；当low与high都省略，但保留逗号时，比如：|,|，表示窗口中所有数据；当low与high都省略，逗号也省略时，比如：||，则表示|1,1|，即窗口中最新的那条数据。这种设计也部分借鉴了Go的切片下标的语法。</p>
<p>窗口后面条件表达式的求值结果要么为true，要么为false。其支持的运算符可以参考r0005规则。物理指标用<strong>$+指标名字</strong>表示，比如$speed。</p>
<p>当整个规则求值结果为真时，输出窗口中最新数据的speed、temperature和salinity这三个指标。如果最后输出指标的元组为空，则代表输出所有指标。</p>
<p>好了，大致确定了DSL语法后，我们就来根据语法样例编写对应ANTLR文法。</p>
<h3>四. 为DSL编写ANTLR文法</h3>
<p>在之前的文章中，我们也提到过，ANTLR文法规则存储在以.g4为后缀的文件中，文件名要与文件内的grammar关键字后面的名字保持一致，比如我们的文件名为Tdat.g4，那么该文件中grammar后面也必须是Tdat：</p>
<pre><code>// the grammar for tdat RuleEngine
grammar Tdat;
</code></pre>
<blockquote>
<p>注意：如果生成的解析器的目标语言为Go，那么ANTLR文法文件名必须要大写，否则生成的一些重要的结构无法被导出。</p>
</blockquote>
<p>每个ANTLR文法文件都需要一个起始语法解析规则(parser rule)，在Tdat.g4中，我们的起始规则为prog：</p>
<pre><code>// the first parser rule, also the first rule of RuleEngine grammar
// prog is a sequence of rule lines.

prog
    : ruleLine+
    ;
</code></pre>
<p>正如prog规则的注释那样，一个采集装置的完整规则文件是由一组(至少包含一条)规则行（ruleLine)组成。而每个ruleLine的组成模式也非常固定：</p>
<pre><code>ruleLine
    : ruleID ':' enumerableFunc '{' windowsRange conditionExpr '}' '=&gt;' result ';'
    ;
</code></pre>
<p>大家可以对照着前面语法样例来理解ruleLine这个规则。接下来我们自顶向下(从左向右)的将各个组成部分的规则逐一定义就好了。先来看ruleID这个最简单的规则：</p>
<ul>
<li>ruleID就是以字母开头，由数字与数字组成的文本：</li>
</ul>
<pre><code>ruleID
    : ID
    ;

// the first char of ID must be a letter
ID
    : ID_LETTER (ID_LETTER | DIGIT)*
    ;

fragment
ID_LETTER
    : 'a'..'z'|'A'..'Z'|'_'  // [a-zA-Z_]
    ;

fragment
DIGIT
    : [0-9]  // match single digit
    ;
</code></pre>
<p>像ID这样的词法规则，大家其实无需自己去从头编写，<a href="https://book.douban.com/subject/27082372/">《ANTLR 4权威指南》</a>或<a href="https://github.com/antlr/grammars-v4">antlr/grammar-v4</a>中有大量样例可供参考。</p>
<ul>
<li>enumerableFunc就是窗口判定策略，这里直接将Each、None和Any定为语言的关键字了：</li>
</ul>
<pre><code>enumerableFunc
    : 'Each'
    | 'None'
    | 'Any'
    ;
</code></pre>
<ul>
<li>windowsRange是窗口规则，它有两个候选产生式：</li>
</ul>
<pre><code>windowsRange
    : '|' INT? '|'          #WindowsWithSingleOrZeroIndex
    | '|' INT? ',' INT? '|' #WindowsWithLowAndHighIndex
    ;
</code></pre>
<p>为了便于后续解析，这里用#为每个产生式起了一个名字，这样后续ANTLR在基于Tdat.g4生成Parser代码时，就会单独针对每个名字生成一对EnterXXX和ExitXXX(以listener模式下为例)，便于我们解析。当然这里你还可以拆分的更细碎一些以进一步减少在处理Parser规则时自己写代码做判断的工作量。</p>
<ul>
<li>conditionExpr是这里最复杂的parser规则，它的求值结果永远是true或false，因此我将其产生式规则定义如下：</li>
</ul>
<pre><code>conditionExpr
    : conditionExpr logicalOp conditionExpr
    | '(' conditionExpr ')'
    | primaryExpr comparisonOp primaryExpr
    ;
</code></pre>
<p>我们看到：conditionExpr规则有三个候选产生式，它可以是带括号的自身，支持自身通过逻辑操作符(and和or)的运算，也可以是经由比较操作符计算(比如>、&lt;等)的普通表达式(primaryExpr)。</p>
<p>而普通表达式(primaryExpr)同样可以是带括号的自身，可以是经由算术运算符(比如：加减乘除等)计算的普通表达式，可以是单一的指标(METRIC)，可以是经由一元内置函数(比如：roundUp、abs等)计算的普通表达式，当然也可以仅仅是一个字面值(literal)。literal字面值支持整型、浮点(非科学记数法表示形式)和字符串(双引号括起的文本)：</p>
<pre><code>primaryExpr
    : '(' primaryExpr ')'                  #BracketExprInPrimaryExpr
    | primaryExpr arithmeticOp primaryExpr #ArithmeticExprInPrimaryExpr
    | METRIC                               #MetricInPrimaryExpr
    | builtin '(' primaryExpr ')'          #BuildinExprInPrimaryExpr
    | literal                              #RightLiteralInPrimaryExpr
    ;

arithmeticOp
    : '+'
    | '-'
    | '*'
    | '/'
    | '%'
    ;

builtin
    : 'roundUp'
    | 'roundDown'
    | 'abs'
    ;

logicalOp
    : 'or'
    | 'and'
    ;

comparisonOp
    : '&lt;'
    | '&gt;'
    | '&lt;='
    | '&gt;='
    | '=='
    | '!='
    ;

METRIC
    : '$' ID // match $speed
    ;

INT
    : DIGIT+
    ;

FLOAT
    : DIGIT+ '.' DIGIT* // match 1. 39. 3.14159 etc...
    | '.' DIGIT+        // match .1 .14159
    ;

STRING
    : '"' (ESC|.)*? '"'
    ;
</code></pre>
<ul>
<li>result规则定义了声明输出指标的形式，它是一个小括号表示的元组，指标间用逗号分隔，如果元组为空，则表示输出所有指标。</li>
</ul>
<pre><code>result
    : '(' STRING (',' STRING)* ')' # ResultWithElements
    | '(' ')'                      # ResultWithoutElements
    ;
</code></pre>
<p>好了，到这里针对这门DSL的ANTLR文法也编写完了。</p>
<h3>五. 小结</h3>
<p>在这一篇中，我们了解了开发一门DSL的基本流程，我们以一门为气象科学家打造的DSL为示例，和大家一起为该DSL设计了语法样例，并用ANTLR4的文法规则定义了这门DSL。</p>
<p>那么这个文法是否能被ANTLR正确解析并生成目标代码？通过这个文法能否正确识别出前面我们给出的语法样例呢？在下一篇“文法验证”中我将给大家揭晓答案。</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}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p><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/24/an-example-of-implement-dsl-using-antlr-and-go-part1/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
