<?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; MySQL</title>
	<atom:link href="http://tonybai.com/tag/mysql/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 08 Jun 2026 23:32:23 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go 语言观察：登顶“最受期待”榜首，JetBrains 2025报告洞悉未来趋势</title>
		<link>https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends/</link>
		<comments>https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends/#comments</comments>
		<pubDate>Thu, 23 Oct 2025 10:41:50 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API和服务集成]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cloud服务]]></category>
		<category><![CDATA[database/sql]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[Goroutine模型]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言进阶课]]></category>
		<category><![CDATA[gRPC生态]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JetBrains]]></category>
		<category><![CDATA[JetBrains2025报告]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[LanguagePromiseIndex]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[net/http]]></category>
		<category><![CDATA[pgx]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Serverless架构]]></category>
		<category><![CDATA[Systemsoftware]]></category>
		<category><![CDATA[TonyBai]]></category>
		<category><![CDATA[TypeScript]]></category>
		<category><![CDATA[Web服务]]></category>
		<category><![CDATA[中间件]]></category>
		<category><![CDATA[主要编程语言]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[云原生时代的C语言]]></category>
		<category><![CDATA[云服务提供商]]></category>
		<category><![CDATA[人才储备池]]></category>
		<category><![CDATA[使用率]]></category>
		<category><![CDATA[关系型数据库]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[地位稳固]]></category>
		<category><![CDATA[基础设施软件]]></category>
		<category><![CDATA[增长稳定性]]></category>
		<category><![CDATA[学习意愿]]></category>
		<category><![CDATA[定位精准]]></category>
		<category><![CDATA[容器化]]></category>
		<category><![CDATA[应用部署平台]]></category>
		<category><![CDATA[开发者生态系统现状]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[战略定位]]></category>
		<category><![CDATA[技术栈]]></category>
		<category><![CDATA[技术范式演进]]></category>
		<category><![CDATA[技术风向]]></category>
		<category><![CDATA[拉新能力]]></category>
		<category><![CDATA[提供API和服务]]></category>
		<category><![CDATA[数据库]]></category>
		<category><![CDATA[最受期待]]></category>
		<category><![CDATA[最想采用的下一门语言]]></category>
		<category><![CDATA[服务器/云端]]></category>
		<category><![CDATA[未来潜力]]></category>
		<category><![CDATA[未来趋势]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[核心洞察]]></category>
		<category><![CDATA[潜力巨大]]></category>
		<category><![CDATA[生态位观察]]></category>
		<category><![CDATA[用户忠诚度]]></category>
		<category><![CDATA[第一梯队]]></category>
		<category><![CDATA[简洁]]></category>
		<category><![CDATA[语言承诺指数]]></category>
		<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=5295</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends 大家好，我是Tony Bai。 近日，软件开发工具巨头 JetBrains 发布了其年2025度《开发者生态系统现状》报告，这份基于全球数万名开发者调研的数据报告，已成为洞察技术风向的关键参考之一。在今年的报告中，Go 语言的表现尤为亮眼，它不仅在“未来潜力”和“学习意愿”等前瞻性指标上独占鳌头，其在当前主流语言版图中的位置也愈发稳固。 本文将为您全方位解读这份报告，从多个维度剖析 Go 语言的现状、潜力和生态位，洞察这些趋势对每一位 Gopher 的深远影响。 核心洞察：Go 成为开发者“最想采用的下一门语言” 报告中最激动人心的发现，莫过于在“开发者最想采用的下一门语言”这项调查中，Go 语言以 11% 的得票率高居榜首。 这一数据强烈预示着 Go 语言在未来的项目选型和团队扩张中将拥有巨大的潜力。它表明 Go 简洁、高效、高并发的理念已成功捕获了大量开发者的心智。对于企业而言，这意味着 Go 的人才储备池正在快速扩大；对于开发者个人而言，掌握 Go 语言无疑是抓住了未来技术栈演进的关键脉搏。 当前使用现状：稳居主流，但非绝对主导 当然，我们也需客观看待 Go 的当前位置。在“主要编程语言”的长期使用趋势图表中，Go 的使用率稳定在 20%。 这是一个非常健康且重要的数字，它意味着 Go 已经牢固地占据了主流编程语言的一席之地，与 C# (21%) 并驾齐驱，并且领先于 Kotlin (18%) 和 Rust (12%) 等现代语言。 然而，与常年盘踞榜首的 JavaScript (61%)、Python (57%) 和 Java (49%) 相比，Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends">本文永久链接</a> &#8211; https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends</p>
<p>大家好，我是Tony Bai。</p>
<p>近日，软件开发工具巨头 JetBrains 发布了其年2025度《<a href="https://devecosystem-2025.jetbrains.com/tools-and-trends">开发者生态系统现状</a>》报告，这份基于全球数万名开发者调研的数据报告，已成为洞察技术风向的关键参考之一。在今年的报告中，Go 语言的表现尤为亮眼，它不仅在“未来潜力”和“学习意愿”等前瞻性指标上独占鳌头，其在当前主流语言版图中的位置也愈发稳固。</p>
<p>本文将为您全方位解读这份报告，从多个维度剖析 Go 语言的现状、潜力和生态位，洞察这些趋势对每一位 Gopher 的深远影响。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>核心洞察：Go 成为开发者“最想采用的下一门语言”</h2>
<p>报告中最激动人心的发现，莫过于在“开发者最想采用的下一门语言”这项调查中，<strong>Go 语言以 11% 的得票率高居榜首</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-2.png" alt="" /></p>
<p>这一数据强烈预示着 Go 语言在未来的项目选型和团队扩张中将拥有巨大的潜力。它表明 Go 简洁、高效、高并发的理念已成功捕获了大量开发者的心智。对于企业而言，这意味着 Go 的人才储备池正在快速扩大；对于开发者个人而言，掌握 Go 语言无疑是抓住了未来技术栈演进的关键脉搏。</p>
<h2>当前使用现状：稳居主流，但非绝对主导</h2>
<p>当然，我们也需客观看待 Go 的当前位置。在“主要编程语言”的长期使用趋势图表中，Go 的使用率稳定在 <strong>20%</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-3.png" alt="" /></p>
<p>这是一个非常健康且重要的数字，它意味着 Go 已经牢固地占据了主流编程语言的一席之地，与 C# (21%) 并驾齐驱，并且领先于 Kotlin (18%) 和 Rust (12%) 等现代语言。</p>
<p>然而，与常年盘踞榜首的 JavaScript (61%)、Python (57%) 和 Java (49%) 相比，Go 还有相当的差距。这恰恰反映了 Go 的战略定位：它并非一门试图“通吃”所有领域的语言。Python 在数据科学和 Web 后端拥有深厚根基，Java 在庞大的企业级应用中难以撼动，而 <strong>Go 则精准地聚焦于其核心优势领域——云原生、分布式系统和高性能后端服务</strong>。这种聚焦，正是其强大生命力的来源。</p>
<h2>增长潜力：位列“承诺指数”第一梯队</h2>
<p>JetBrains 创设的“语言承诺指数 (Language Promise Index)”综合评估了语言的增长稳定性、采用势头和用户忠诚度。在这个极具前瞻性的榜单上，<strong>Go 以 +115 的高分位列第四</strong>，与 TypeScript (+223)、Rust (+187) 和 Python (+131) 共同组成了未来增长潜力最强的“第一梯队”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-4.png" alt="" /></p>
<p>这表明，尽管 Go 的当前总使用率不如 Python 或 Java，但其<strong>增长的质量和动能</strong>却处于顶尖水平。社区活跃、用户忠诚度高、应用场景不断拓宽，这些都是 Go 未来持续攀升的坚实基础。</p>
<h2>趋势解读：为何是 Go？技术范式演进的必然选择</h2>
<p>报告中的另外几组数据，完美解释了 Go 语言为何能在当今的技术浪潮中乘风破浪。</p>
<h3>完美契合“连接型”开发范式</h3>
<p>报告指出，现代开发者的核心工作正在从构建孤立的应用，转向构建系统间的“连接性组织 (connective tissue)”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-5.png" alt="" /></p>
<ul>
<li><strong>52%</strong> 的开发者工作涉及<strong>与 API 和服务集成</strong>。</li>
<li><strong>48%</strong> 的开发者工作涉及<strong>提供 API 和服务</strong>。</li>
</ul>
<p>同时，在开发者构建的软件产品类型中，<strong>Web 服务 (29%)</strong>、<strong>Cloud 服务 (19%)</strong> 和 <strong>System software (17%)</strong> 占据了重要份额。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-6.png" alt="" /></p>
<p>这些领域恰恰是 Go 语言的核心优势区。其天生为并发而设计的 Goroutine 模型、简洁高效的 net/http 标准库以及强大的 gRPC 生态，使其成为构建高性能 API、微服务、中间件和基础设施软件的理想选择。</p>
<h3>云原生主战场的绝对优势</h3>
<p>在应用部署平台方面，<strong>40% 的应用被部署在服务器/云端</strong>，这是仅次于浏览器的第二大平台。在云服务提供商方面，AWS (43%)、GCP (22%) 和 Azure (22%) 占据了市场主导地位。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-language-leads-jetbrains-trends-7.png" alt="" /></p>
<p>Go 语言自诞生之初就被誉为“<a href="https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/">云原生时代的 C 语言</a>”，其编译后体积小、资源占用低、启动速度快的特性，使其在以 Docker 和 Kubernetes 为代表的容器化环境中，以及在 Serverless 架构下降本增效的潜力巨大。可以说，Go 是为在 AWS、GCP、Azure 等云平台上运行而生的语言。</p>
<h2>生态位观察：数据库新王登基，Gopher 需关注</h2>
<p>报告还揭示了一个对所有后端开发者都至关重要的趋势：<strong>PostgreSQL 的使用率 (50%) 预计将历史性地超越 MySQL (49%)</strong>，成为最受欢迎的关系型数据库。</p>
<p>这一变化对 Go 开发者同样具有指导意义。虽然 Go 的 database/sql 包提供了统一的数据库访问接口，但了解并熟练使用社群中性能最优、特性最丰富的 PostgreSQL 驱动（如 pgx）将变得愈发重要。关注主流数据库的演进，并及时更新自己的技术栈，是保持竞争力的关键。</p>
<h2>总结与展望</h2>
<p>JetBrains 的这份报告以翔实的数据，为我们描绘了一幅立体而清晰的 Go 语言发展图景：</p>
<ul>
<li><strong>人气高涨</strong>：它是开发者最渴望学习和使用的新语言，拥有最强的“拉新”能力。</li>
<li><strong>地位稳固</strong>：已成为使用率达 20% 的主流语言，在特定领域拥有不可替代的优势。</li>
<li><strong>潜力巨大</strong>：其高质量的增长动能使其稳居未来潜力榜的第一梯队。</li>
<li><strong>定位精准</strong>：它完美契合了以 API 集成和云原生为核心的现代软件开发范式。</li>
</ul>
<p>对于 Go 社区而言，这份报告既是肯定也是激励。它证明了 Go 的选择是正确的，其专注的领域正是软件行业发展的未来方向。对于每一位 Gopher 来说，深入理解 Go 的生态位，持续打磨在云原生和高性能后端领域的技能，无疑是投身这股浪潮、创造更大价值的最佳路径。</p>
<p>资料链接：https://devecosystem-2025.jetbrains.com/tools-and-trends</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/23/go-language-leads-jetbrains-trends/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>为什么说“接口”，而非代码或硬件堆砌，决定了系统的性能上限？</title>
		<link>https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance/</link>
		<comments>https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance/#comments</comments>
		<pubDate>Sat, 06 Sep 2025 23:22:09 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[OLTP]]></category>
		<category><![CDATA[PG]]></category>
		<category><![CDATA[Postgres]]></category>
		<category><![CDATA[pprof]]></category>
		<category><![CDATA[proto]]></category>
		<category><![CDATA[REST]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[StrangeLoop]]></category>
		<category><![CDATA[TigerBeetle]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[数据库]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5128</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance 我的《Go语言第一课》已上市，赠书活动正在进行中，欢迎点击此链接参与。 大家好，我是Tony Bai。 我们通常如何看待性能优化？答案往往是：更快的算法、更少的内存分配、更底层的并发原语、甚至用SIMD指令压榨CPU的每一个周期。我们痴迷于“引擎盖之下”的实现细节，坚信更好的代码和更强的硬件能带来更高的性能。 然而，TigerBeetle数据库创始人Joran Dirk Greef在Strange Loop上的一场精彩的演讲(https://www.youtube.com/watch?v=yKgfk8lTQuE)，用一场耗资百万美元的数据库比赛，颠覆了这一传统认知。他通过无可辩驳的基准测试数据证明：在分布式系统中，接口（Interface）的设计，而非代码实现或硬件堆砌，才是决定性能上限的真正瓶颈。 在深入探讨之前，我们必须对本文的“接口”一词进行关键澄清。对于Go开发者而言，“接口”通常指代语言层面的interface类型，一种实现行为契约以及多态的工具。但本文中所说的“接口”，则是一个更宏观、更广义的概念，它指的是系统与系统之间、或用户与系统之间进行通信的交互模式、契约与协议。你的REST API设计、gRPC的.proto文件、微服务间的调用时序，都属于这个“广义接口”的范畴。 这场演讲虽然以数据库为载体，但其揭示的“接口即天花板”的原理，对于每一位设计和使用Go API、微服务的工程师来说，都无异于一声惊雷。它迫使我们重新审视，我们日常构建的系统，是否在设计之初，就已为自己埋下了无法逾越的性能枷锁。 赛场设定：一场关于“转账”的终极对决 Greef的实验设计极其巧妙，他回归了OLTP（在线事务处理）的本质，重拾了图灵奖得主Jim Gray定义的最小交易单元：“借贷记”（Debit-Credit），即我们熟知的“转账”操作。 这个工作负载的核心是：在两个账户之间转移价值，并记录一笔历史。它的关键挑战在于竞争（Contention）。在高流量的真实世界系统中，总会有大量的交易集中在少数“热门”账户上，这就是帕累托法则（80/20原则）的体现。 传统接口：交互式事务 大多数通用数据库处理这种事务的标准接口是“交互式”的，即一个业务操作需要多次网络往返才能完成： 1. 第一步（读）：客户端发起一个网络请求，SELECT Alice和Bob的账户余额。 2. 第二步（计算）：数据返回到客户端，应用代码在本地检查余额是否充足。 3. 第三步（写）：客户端发起第二个网络请求，在一个事务中UPDATE两个账户的余额，并INSERT一条转账记录。 这个看似天经地义的流程，隐藏着一个致命的缺陷。 百万美元的“滑铁卢”：当硬件和实现都失灵 Greef设立了三组“选手”来进行一场性能对决： Postgres (单机): 经典的、备受尊重的开源数据库。 “迈凯伦” (16节点集群): 一个匿名的、顶级的云原生分布式数据库，年费超过一百万美元。 TigerBeetle: Greef自己设计的、专为OLTP优化的新一代数据库。 比赛结果令人瞠目结舌： 在零竞争下，“迈凯伦”集群的性能甚至不如单机Postgres。 随着竞争率提升，16台机器的“迈凯伦”性能暴跌，甚至出现了节点越少、性能越高的荒谬情况。 在整个高竞争测试期间，这百万美元硬件的CPU利用率从未超过12%。 为什么？ 硬件在空转，代码在等待。钱，并没有买来性能。 性能的枷锁：跨网络持有锁 问题的根源，就出在那个“交互式事务”的接口设计上。 当一个事务开始时，数据库为了保证ACID，必须锁定被操作的行。在这个接口模型中，锁的持有时间 = 数据库处理时间 + 两次网络往返（RTT）的时间 + 客户端应用的处理时间。 Greef指出，数据库内部的处理时间可能是微秒级的，但一次跨数据中心的网络往返，轻易就是几十甚至上百毫秒。这意味着，数据库中最宝贵的锁资源，其生命周期被廉价且缓慢的网络I/O牢牢绑架了。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/the-power-of-an-interface-for-performance-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance">本文永久链接</a> &#8211; https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance</p>
<blockquote>
<p>我的《<a href="https://mp.weixin.qq.com/s/l3t2B_QAKC4whwhmhNo4Fw">Go语言第一课</a>》已上市，赠书活动正在进行中，欢迎<a href="https://mp.weixin.qq.com/s/Hzyi7TminudVb2-cLzXC2A">点击此链接</a>参与。</p>
</blockquote>
<p>大家好，我是Tony Bai。</p>
<p>我们通常如何看待<a href="https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy">性能优化</a>？答案往往是：更快的算法、更少的内存分配、更底层的并发原语、甚至用<a href="https://tonybai.com/2025/08/22/go-simd-package-preview">SIMD指令</a>压榨CPU的每一个周期。我们痴迷于“引擎盖之下”的实现细节，坚信更好的代码和更强的硬件能带来更高的性能。</p>
<p>然而，TigerBeetle数据库创始人Joran Dirk Greef<a href="https://www.youtube.com/watch?v=yKgfk8lTQuE">在Strange Loop上的一场精彩的演讲</a>(https://www.youtube.com/watch?v=yKgfk8lTQuE)，用一场耗资百万美元的数据库比赛，颠覆了这一传统认知。他通过无可辩驳的基准测试数据证明：<strong>在分布式系统中，接口（Interface）的设计，而非代码实现或硬件堆砌，才是决定性能上限的真正瓶颈。</strong></p>
<p>在深入探讨之前，我们必须对本文的“接口”一词进行关键澄清。对于Go开发者而言，“接口”通常指代语言层面的interface类型，一种实现行为契约以及多态的工具。但<strong>本文中所说的“接口”，则是一个更宏观、更广义的概念</strong>，它指的是系统与系统之间、或用户与系统之间进行通信的<strong>交互模式、契约与协议</strong>。你的REST API设计、gRPC的.proto文件、微服务间的调用时序，都属于这个“广义接口”的范畴。</p>
<p>这场演讲虽然以数据库为载体，但其揭示的“接口即天花板”的原理，对于每一位设计和使用Go API、微服务的工程师来说，都无异于一声惊雷。它迫使我们重新审视，我们日常构建的系统，是否在设计之初，就已为自己埋下了无法逾越的性能枷锁。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>赛场设定：一场关于“转账”的终极对决</h2>
<p>Greef的实验设计极其巧妙，他回归了OLTP（在线事务处理）的本质，重拾了图灵奖得主Jim Gray定义的最小交易单元：<strong>“借贷记”（Debit-Credit）</strong>，即我们熟知的“转账”操作。</p>
<p>这个工作负载的核心是：在两个账户之间转移价值，并记录一笔历史。它的关键挑战在于<strong>竞争（Contention）</strong>。在高流量的真实世界系统中，总会有大量的交易集中在少数“热门”账户上，这就是<a href="https://tonybai.com/2025/04/26/13-laws-of-software-engineering">帕累托法则（80/20原则）</a>的体现。</p>
<h3>传统接口：交互式事务</h3>
<p>大多数通用数据库处理这种事务的标准接口是“交互式”的，即一个业务操作需要多次网络往返才能完成：<br />
1.  <strong>第一步（读）</strong>：客户端发起一个网络请求，SELECT Alice和Bob的账户余额。<br />
2.  <strong>第二步（计算）</strong>：数据返回到客户端，应用代码在本地检查余额是否充足。<br />
3.  <strong>第三步（写）</strong>：客户端发起第二个网络请求，在一个事务中UPDATE两个账户的余额，并INSERT一条转账记录。</p>
<p>这个看似天经地义的流程，隐藏着一个致命的缺陷。</p>
<h2>百万美元的“滑铁卢”：当硬件和实现都失灵</h2>
<p>Greef设立了三组“选手”来进行一场性能对决：</p>
<ol>
<li><strong>Postgres (单机):</strong> 经典的、备受尊重的开源数据库。</li>
<li><strong>“迈凯伦” (16节点集群):</strong> 一个匿名的、顶级的云原生分布式数据库，年费超过一百万美元。</li>
<li><strong>TigerBeetle:</strong> Greef自己设计的、专为OLTP优化的新一代数据库。</li>
</ol>
<p><strong>比赛结果令人瞠目结舌：</strong></p>
<ul>
<li>在零竞争下，“迈凯伦”集群的性能甚至<strong>不如</strong>单机Postgres。</li>
<li>随着竞争率提升，16台机器的“迈凯伦”性能暴跌，甚至出现了节点越少、性能越高的荒谬情况。</li>
<li>在整个高竞争测试期间，这百万美元硬件的CPU利用率<strong>从未超过12%</strong>。</li>
</ul>
<p><strong>为什么？</strong> 硬件在空转，代码在等待。钱，并没有买来性能。</p>
<h2>性能的枷锁：跨网络持有锁</h2>
<p>问题的根源，就出在那个“交互式事务”的<strong>接口设计</strong>上。</p>
<p>当一个事务开始时，数据库为了保证ACID，必须锁定被操作的行。在这个接口模型中，<strong>锁的持有时间 = 数据库处理时间 + 两次网络往返（RTT）的时间 + 客户端应用的处理时间。</strong></p>
<p>Greef指出，数据库内部的处理时间可能是微秒级的，但一次跨数据中心的网络往返，轻易就是几十甚至上百毫秒。这意味着，<strong>数据库中最宝贵的锁资源，其生命周期被廉价且缓慢的网络I/O牢牢绑架了。</strong></p>
<h3>阿姆达尔定律的诅咒</h3>
<p>这完美地印证了阿姆达尔定律：系统的总性能，取决于其串行部分的速度。在这个场景中，“跨网络持有锁”就是那个不可并行的、绝对的串行瓶颈。</p>
<ul>
<li>当网络延迟为1ms，竞争率为10%时，即使你的数据库是无限快的，理论性能上限也只有<strong>10,000 TPS</strong>。</li>
<li>当网络延迟上升到10ms，这个上限会骤降到<strong>1,000 TPS</strong>。</li>
</ul>
<p>无论你增加多少台机器（水平扩展），都无法打破这个由接口设计决定的物理定律。</p>
<h2>对Go API和系统设计的深刻启示</h2>
<p>这场数据库之战，对我们Go开发者来说，是一面镜子。我们必须审视自己日常的设计，是否也在不经意间构建了类似的“性能枷锁”。</p>
<h3>1. 警惕你的Go API是否“跨网络持有锁”</h3>
<p>在微服务架构中，一个常见的反模式是“编排式事务”。想象一个创建订单的流程：</p>
<pre><code class="go">// 反模式：一个跨多个网络调用、持有远端锁的接口
func CreateOrder(ctx context.Context, userID, productID int) error {
    // 步骤1：锁定库存 (通过RPC调用库存服务)
    lock, err := inventoryService.LockStock(ctx, productID, 1)
    if err != nil {
        return err
    }
    // 注意：从此刻起，该商品的库存在inventoryService中被锁定！

    // 步骤2：扣减用户余额 (通过RPC调用账户服务)
    err = accountService.Debit(ctx, userID, product.Price)
    if err != nil {
        inventoryService.UnlockStock(ctx, lock.ID) // 必须记得解锁
        return err
    }

    // 步骤3：创建订单记录
    // ...

    // 成功！最后解锁库存
    return inventoryService.UnlockStock(ctx, lock.ID)
}
</code></pre>
<p>这个CreateOrder函数，在其执行期间，<strong>跨越了多次网络调用，却一直持有着库存服务的锁</strong>。这与Postgres的交互式事务犯了完全相同的错误。这个糟糕的<strong>接口设计</strong>决定了系统的性能上限。</p>
<h3>2. TigerBeetle的解决方案：重新定义接口</h3>
<p>TigerBeetle的接口设计哲学截然不同。它不接受交互式的、一次一笔的事务。取而代之的是：<br />
-   <strong>批处理 (Batching):</strong> 客户端将成千上万个“转账”意图打包成一个大的请求。<br />
-   <strong>一次性提交 (One-Shot Commit):</strong> 将这个大包一次性发送给数据库。<br />
-   <strong>异步处理:</strong> 数据库在内部高效地处理这个批次，然后一次性返回所有结果。</p>
<p>在这个模型中，<strong>网络延迟只发生一次</strong>，且与锁的持有时间完全解耦。</p>
<h3>3. 转化为Go的设计模式：</h3>
<p>我们可以将TigerBeetle的思想应用到我们的Go服务设计中，重新定义我们的“接口”：</p>
<ul>
<li><strong>使用异步消息传递</strong>：CreateOrder服务不应直接调用其他服务并等待。它应该发布一个OrderCreationRequested事件到消息队列（如NATS或Kafka）。后续的服务订阅此事件，并以异步、解耦的方式处理各自的逻辑（通常需要Saga等模式保证最终一致性）。</li>
<li><strong>设计“意图驱动”的API</strong>：不要创建需要多次交互才能完成一个业务流程的API。取而代之，设计一个能接收完整“业务意图”的API。例如，提供一个/orders/batch_create端点，让客户端一次性提交多个订单创建的请求。</li>
<li><strong>将状态检查移至服务端</strong>：与其让客户端先读数据再决定如何写，不如提供一个API，让客户端直接声明它的意图，由服务端在一个原子操作内完成“检查并写入”。</li>
</ul>
<h2>小结</h2>
<p>Joran Greef的演讲最终以TigerBeetle在高竞争下，性能达到Postgres数千倍的结果震撼全场。这并非因为TigerBeetle的代码实现比Postgres好了几个数量级，而是因为它的<strong>接口设计</strong>从根本上绕开了阿姆达尔定律的诅咒。</p>
<p>对于Go开发者，这场演讲的启示也是深远的：</p>
<ul>
<li><strong>性能瓶颈往往在白板上就已注定</strong>：在你写下第一行代码之前，你的API设计、服务间的交互模型，可能已经为你的系统设定了无法逾越的性能天花板。</li>
<li><strong>减少网络往返，尤其是持有锁的往返，是性能优化的第一要务</strong>。</li>
<li><strong>拥抱批处理和异步化</strong>：这是打破“一次交互一件事”思维定势、实现数量级性能提升的关键。</li>
</ul>
<p>下一次，当你面对性能问题时，与其一头扎进pprof的火焰图，试图优化某个函数的CPU占用，不如先退后一步，审视你的系统和API的<strong>接口设计</strong>。或许，那个锁住你系统性能的真正枷锁，并非隐藏在代码的细枝末节里，而是明晃晃地写在你的设计文档的第一页。</p>
<hr />
<p><strong>想系统学习Go，构建扎实的知识体系？</strong></p>
<p>我的新书《Go语言第一课》是你的首选。源自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/09/07/the-power-of-an-interface-for-performance/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>日志查询从 70 小时到 10 秒？VictoriaMetrics 联创揭示 PB 级日志处理性能奥秘</title>
		<link>https://tonybai.com/2025/08/20/large-scale-logging-made-easy/</link>
		<comments>https://tonybai.com/2025/08/20/large-scale-logging-made-easy/#comments</comments>
		<pubDate>Wed, 20 Aug 2025 00:34:17 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AliaksandrValialkin]]></category>
		<category><![CDATA[bloomfilter]]></category>
		<category><![CDATA[ClickHouse]]></category>
		<category><![CDATA[fasthttp]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[grafana]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[loki]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[OLAP]]></category>
		<category><![CDATA[OLTP]]></category>
		<category><![CDATA[pb]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[TB]]></category>
		<category><![CDATA[VictoriaLogs]]></category>
		<category><![CDATA[VictoriaMetrics]]></category>
		<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=5057</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/20/large-scale-logging-made-easy 当日志规模达到 PB 级别，传统的关系型数据库（如 PostgreSQL 或 MySQL）往往力不从心，不仅性能急剧下降，运维成本也变得难以承受。在 FrOSCon 2025 大会上，VictoriaMetrics 的联合创始人兼CTO、fasthttp作者、资深 Go 工程师Aliaksandr Valialkin 发表了题为“大规模日志处理变得简单”的演讲，深入剖析了专为日志设计的数据库如何通过一系列精巧的工程设计，实现单机处理 PB 级数据的惊人性能。 本文将和大家一起听演讲，并了解其分享的核心技术——包括列式存储、时间分区、日志流索引和布隆过滤器——并看看为什么这些技术能将日志查询速度从理论上的 70 小时超大幅缩短至 10 秒，以及为何传统数据库在这场竞赛中注定落败。 什么是“大规模日志”？一个与时俱进的定义 在探讨解决方案之前，演讲者 Aliaksandr Valialkin 首先抛出了一个引人深思的问题：究竟什么是“大规模日志”？ 业界通常用每日的数据量来衡量，是 GB、TB 还是 PB？然而，这个定义是浮动的。Aliaksandr 提出了一个更具工程实践意义的定义，它将问题从抽象的数字拉回到了具体的物理约束上： 当你的日志无法装入单台计算机时，它就达到了“大规模”。 这个定义的巧妙之处在于，它将“规模”与具体的硬件能力和软件效率紧密地联系起来。一台搭载着普通硬盘、运行着 PostgreSQL 的服务器，可能在处理每日 GB 级日志时就会捉襟见肘。然而，一台配备了高速 NVMe 硬盘、拥有数百 CPU 核心和 TB 级内存的“巨兽”，在运行像 VictoriaLogs 这样的专用数据库时，其处理能力可能是前者的数千倍。在这种情况下，即便是每日 PB 级的日志，也可能不属于“大规模”的范畴。 这个定义为我们接下来的讨论奠定了基础：在诉诸昂贵且复杂的分布式集群（水平扩展）之前，我们是否已经通过选择正确的工具，充分压榨了单机（垂直扩展）的潜力？ 单机处理 PB 级日志：一场从 70 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/large-scale-logging-made-easy-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/20/large-scale-logging-made-easy">本文永久链接</a> &#8211; https://tonybai.com/2025/08/20/large-scale-logging-made-easy</p>
<p>当日志规模达到 PB 级别，传统的关系型数据库（如 PostgreSQL 或 MySQL）往往力不从心，不仅性能急剧下降，运维成本也变得难以承受。在 <a href="https://froscon.org/">FrOSCon</a> 2025 大会上，VictoriaMetrics 的联合创始人兼CTO、fasthttp作者、资深 Go 工程师<a href="https://github.com/valyala">Aliaksandr Valialkin</a> 发表了题为“<a href="https://www.youtube.com/watch?v=mhMgbMhXv80">大规模日志处理变得简单</a>”的演讲，深入剖析了专为日志设计的数据库如何通过一系列精巧的工程设计，实现单机处理 PB 级数据的惊人性能。</p>
<p>本文将和大家一起听演讲，并了解其分享的核心技术——包括列式存储、时间分区、日志流索引和布隆过滤器——并看看为什么这些技术能将日志查询速度从理论上的 70 小时超大幅缩短至 10 秒，以及为何传统数据库在这场竞赛中注定落败。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>什么是“大规模日志”？一个与时俱进的定义</h2>
<p>在探讨解决方案之前，演讲者 Aliaksandr Valialkin 首先抛出了一个引人深思的问题：<strong>究竟什么是“大规模日志”？</strong> 业界通常用每日的数据量来衡量，是 GB、TB 还是 PB？然而，这个定义是浮动的。Aliaksandr 提出了一个更具工程实践意义的定义，它将问题从抽象的数字拉回到了具体的物理约束上：</p>
<blockquote>
<p>当你的日志<strong>无法装入单台计算机</strong>时，它就达到了“大规模”。</p>
</blockquote>
<p>这个定义的巧妙之处在于，它将“规模”与具体的硬件能力和软件效率紧密地联系起来。一台搭载着普通硬盘、运行着 PostgreSQL 的服务器，可能在处理每日 GB 级日志时就会捉襟见肘。然而，一台配备了高速 NVMe 硬盘、拥有数百 CPU 核心和 TB 级内存的“巨兽”，在运行像 VictoriaLogs 这样的专用数据库时，其处理能力可能是前者的数千倍。在这种情况下，即便是每日 PB 级的日志，也可能不属于“大规模”的范畴。</p>
<p>这个定义为我们接下来的讨论奠定了基础：在诉诸昂贵且复杂的分布式集群（水平扩展）之前，我们是否已经通过选择正确的工具，充分压榨了单机（垂直扩展）的潜力？</p>
<h2>单机处理 PB 级日志：一场从 70 小时到 10 秒的性能优化之旅</h2>
<p>为了具象化地展示专用日志数据库的威力，演讲者构建了一个思想实验：在一台配备了顶级 NVMe 硬盘（理论持续读取速度 4 GB/s）的 Google Cloud 虚拟机上，查询 1 PB 的日志数据。</p>
<h3>起点：暴力扫描 (理论耗时: 70 小时)</h3>
<p>如果我们将 1 PB 的原始日志直接存储在硬盘上，并进行一次全盘扫描，理论上需要的时间是：</p>
<pre><code>1 PB / 4 GB/s ≈ 1,048,576 GB / 4 GB/s ≈ 262,144 秒 ≈ 72.8 小时
</code></pre>
<p>这在任何生产环境中都是完全无法接受的查询延迟。</p>
<h3>第一步：高压缩率带来的飞跃 (理论耗时: 4.6 小时)</h3>
<p>专用日志数据库的第一个魔法在于其惊人的数据压缩能力。根据 VictoriaLogs 用户的真实反馈，对于典型的结构化或半结构化日志，压缩比通常在<strong>8x 到 50x</strong> 之间。</p>
<p>我们取一个相对保守的 <strong>16x</strong> 压缩比。这意味着 1 PB 的原始日志，可以被压缩到仅有 <strong>64 TB</strong> 的磁盘空间——这恰好是 Google Cloud 单个虚拟机可挂载的最大磁盘容量。</p>
<p>此时，全盘扫描的时间大幅缩短：</p>
<pre><code>64 TB / 4 GB/s = 16,384 秒 ≈ 4.55 小时
</code></pre>
<p>这已经是一个巨大的进步，但对于即时的问题排查来说，仍然太慢。</p>
<h3>优化的核心基石：列式存储 (Columnar Storage)</h3>
<p>传统关系型数据库（如 PostgreSQL, MySQL）采用<strong>行式存储 (Row-oriented Storage)</strong>。这意味着一张表中，同一行记录的所有字段（列）在物理上是连续存储的。</p>
<pre><code>[Row1: ColA, ColB, ColC] [Row2: ColA, ColB, ColC] ...
</code></pre>
<p>这种存储方式在处理事务性（OLTP）负载时非常高效，因为它能一次性读取或更新整条记录。但对于日志分析这种分析性（OLAP）负载，却是灾难性的。当一个查询只需要分析 ColA 字段时，数据库仍然被迫从磁盘上读取包含 ColB 和 ColC 的完整行数据，造成了大量的 I/O 浪费。</p>
<p>专用日志数据库则借鉴了数据仓库的设计，采用<strong>列式存储 (Columnar Storage)</strong>：</p>
<p>将结构化日志按<strong>字段</strong>（列）进行拆分，将所有日志中同一个字段的值物理上连续存储在一起。</p>
<pre><code>[ColA: Row1, Row2, ...] [ColB: Row1, Row2, ...] [ColC: Row1, Row2, ...]
</code></pre>
<p>这种设计的优势是颠覆性的：</p>
<ol>
<li><strong>I/O 效率</strong>：当查询只涉及 ColA 和 ColB 时，数据库<strong>只需读取这两列的数据</strong>，完全跳过 ColC，I/O 量可以减少几个数量级。</li>
<li><strong>压缩效率</strong>：同一列的数据具有极高的相似性。例如，log_level 列只包含 “info”, “warn”, “error” 等少数几个值；http_status 列只包含 200, 404, 500 等数字。将这些同质化的数据放在一起，其压缩效果远非混合了各种类型数据的行式存储可比。专用数据库还能根据每列的数据特征（如常量、枚举、时间戳、IP 地址等）自动选择最优的<strong>专用编码 (Specialized Codex)</strong>，进一步提升压缩率，有时甚至能达到上千倍。</li>
</ol>
<p>回到我们的实验，假设查询只涉及所有日志字段中的一小部分，需要读取的数据量从 64 TB 减少到了 <strong>4 TB</strong>。查询时间随之骤降至：</p>
<pre><code>4 TB / 4 GB/s = 1024 秒 ≈ 17 分钟
</code></pre>
<p>仅仅列式存储还不够，为了避免全列扫描，还需要更智能的数据组织方式。</p>
<h3>第二步：按时间分区 (理论耗时: 1 分 40 秒)</h3>
<p>日志数据天然带有强烈的时间属性。几乎所有的日志查询都会带上时间范围。专用日志数据库利用这一点，将数据按时间（例如，每小时或每天）进行<strong>物理分区</strong>。每个分区可以是一个独立的目录或文件。</p>
<p>当一个查询带有 time > T1 AND time &lt; T2 的条件时，数据库可以<strong>在查询开始前就完全跳过</strong>时间范围之外的所有数据分区，无需读取任何磁盘块。</p>
<p>假设我们的服务保留了 30 天的日志，而我们的查询只关心其中 3 天的数据。需要扫描的数据量等比例减少 90%：</p>
<pre><code>4 TB * (3 / 30) = 400 GB
</code></pre>
<p>查询时间进一步缩短至：</p>
<pre><code>400 GB / 4 GB/s = 100 秒 ≈ 1 分 40 秒
</code></pre>
<h3>第三步：按日志流 (Log Stream) 索引 (理论耗时: 10 秒)</h3>
<p>另一个重要的日志维度是其来源。演讲者将“日志流”定义为<strong>来自单个应用实例的、按时间排序的日志序列</strong>。例如，在一个 Kubernetes 集群中，每个 pod 的每个 container 都会产生一个独立的日志流。</p>
<p>通过为每个日志流（通常由 service, hostname, pod_name 等标签组合定义）建立索引，数据库可以在查询时，只扫描那些与查询条件（例如 service=”api-gateway”）匹配的流。</p>
<p>假设我们的系统中有 1000 个日志流，而查询只涉及其中的 100 个。需要扫描的数据量再次减少 90%：</p>
<pre><code>400 GB * (100 / 1000) = 40 GB
</code></pre>
<p>查询时间最终缩短至惊人的：</p>
<pre><code>40 GB / 4 GB/s = 10 秒
</code></pre>
<p>我们成功地将一个理论上需要 70 小时的查询，通过一系列精巧的工程设计，在单台机器上优化到了 10 秒以内！</p>
<h3>第四步：为“大海捞针”准备的布隆过滤器 (Bloom Filters)</h3>
<p>对于需要查找唯一或稀有子串（如 trace_id, user_id, ip_address）的“大海捞针”式查询，全量扫描即使优化后也可能很慢。为此，专用数据库引入了布隆过滤器。</p>
<p>布隆过滤器是一种空间效率极高的概率性数据结构，它可以快速地告诉你一个元素<strong>“绝对不存在”</strong>或<strong>“可能存在”</strong>于一个集合中。它可能会有误报（说“可能存在”但实际不存在），但绝不会漏报。</p>
<p>通过为每个数据块（block）中的所有词元（word tokens）构建一个布隆过滤器，数据库可以在查询时：</p>
<ol>
<li>先检查数据块的布隆过滤器。</li>
<li>如果过滤器显示目标 trace_id <strong>绝对不存在</strong>于此块中，则<strong>完全跳过对该数据块的读取和解压</strong>。</li>
</ol>
<p>这可以将此类查询的性能再次提升<strong>高达 100 倍</strong>，实现亚秒级的响应。一个 64 TB 的压缩日志，其布隆过滤器索引的大小可能在 640 GB 到 6.4 TB 之间，这是一个典型的空间换时间策略。</p>
<h2>为何传统数据库在海量日志场景中注定失败？</h2>
<p>演讲清晰地指出了 PostgreSQL 或 MySQL 在处理大规模日志时的几个根本性缺陷，这些缺陷导致它们无法与专用数据库竞争。</p>
<ol>
<li><strong>行式存储的原罪</strong>：如前所述，这导致了严重的 I/O 浪费和低下的压缩率。</li>
<li><strong>随机 I/O 的噩梦</strong>：由于缺乏自动的、基于日志特性的物理分区，查询一个时间范围内的特定日志流，在行式数据库中会退化成对磁盘上数百万个不同位置的<strong>随机读取</strong>。考虑到机械硬盘和 SSD 的随机 I/O 性能远低于顺序读取，这将导致灾难性的性能表现。</li>
<li><strong>B-Tree 索引的“水土不服”</strong>：
<ul>
<li><strong>体积庞大</strong>：B-Tree 索引的大小通常与数据本身的大小在同一个数量级。对于 PB 级数据，索引本身就需要 TB 级的内存才能高效工作，这在成本上是不可接受的。</li>
<li><strong>不适合分析型扫描</strong>：B-Tree 擅长快速定位单条或少数几条记录，但对于需要扫描数百万行的分析型日志查询，其效率远低于专用日志数据库的稀疏索引（例如，仅索引每个数据块的起始/结束时间戳和流 ID）。</li>
</ul>
</li>
<li><strong>致命的写放大 (Write Amplification)</strong>：传统数据库为了维护事务性和索引，会频繁地在磁盘上进行小块数据的<strong>原地更新</strong>（in-place updates）。这在现代 SSD 和 NVMe 硬盘上会触发“读取-修改-写入”的内部操作，一个 4KB 的逻辑写入可能导致 512KB 的物理写入，极其低效且会严重损耗硬盘寿命。而专用日志数据库通常采用<strong>仅追加（append-only）</strong>的写入模式，数据块一旦写入便不可变，这与现代存储硬件的工作原理完美契合。</li>
</ol>
<h2>日志系统技术选型的建议</h2>
<p>在深入探讨了 VictoriaLogs 的设计哲学后，Aliaksandr Valialkin 还在演讲的最后分享了他对当前主流开源日志数据库的看法，并回答了现场观众的提问。这部分内容为我们提供了宝贵的技术选型参考。</p>
<h3>主流开源日志数据库横向对比</h3>
<p>当决定从传统数据库迁移时，开发者通常面临以下几个选择：</p>
<ol>
<li>
<p><strong>Elasticsearch</strong>：</p>
<ul>
<li><strong>优点</strong>：功能强大，生态成熟，是全文搜索领域的王者。</li>
<li><strong>缺点</strong>：资源消耗巨大，尤其是内存。Aliaksandr 指出，要在 Elasticsearch 中存储 PB 级的日志，“准备好为基础设施花费数千万美元”。其横向扩展的运维复杂度也相对较高。</li>
</ul>
</li>
<li>
<p><strong>Grafana Loki</strong>：</p>
<ul>
<li><strong>优点</strong>：设计理念新颖，只索引元数据（标签），不索引日志内容，旨在降低存储成本。与 Grafana 无缝集成。</li>
<li><strong>缺点</strong>：运维和配置相对复杂。更重要的是，它在处理<strong>高基数（high cardinality）</strong>日志字段（如 trace_id, user_id）时存在性能问题，这正是许多现代可观测性场景的核心需求。</li>
</ul>
</li>
<li>
<p><strong>ClickHouse</strong>：</p>
<ul>
<li><strong>优点</strong>：一个极其快速的开源列式分析数据库，性能卓越。</li>
<li><strong>缺点</strong>：灵活性是一把双刃剑。要用好 ClickHouse 存储日志，你需要成为半个专家，深入理解如何正确地设计表结构、选择分区键、设置排序键等，配置门槛较高。</li>
</ul>
</li>
<li>
<p><strong>VictoriaLogs</strong> (演讲者推荐)：</p>
<ul>
<li><strong>优点</strong>：吸收了上述方案的优点，同时致力于<strong>简化运维</strong>。它内置了所有前面提到的优化技术，并且默认开启，无需复杂配置。其架构设计使其能够轻松处理高基数数据，并实现了从树莓派到大型服务器的平滑扩展，而无需调整配置。</li>
</ul>
</li>
</ol>
<h3>现场 Q&amp;A 精华：深入 VictoriaLogs</h3>
<p>现场观众的提问也帮助我们进一步了解了 VictoriaLogs 的一些关键特性和未来规划：</p>
<ul>
<li>
<p><strong>Q: 为什么选择Go？</strong></p>
<ul>
<li><strong>A:</strong> 在过去十多年里，演讲者主要使用 Go 语言编写代码。Go 是他的首选编程语言。他喜欢 Go，因为Go是一门非常简洁且富有生产力的语言。用 Go 编写高性能的代码很容易，而且与其他之前使用的编程语言相比，Go 的代码通常更容易阅读和维护。演讲者喜欢编写有用的开源软件，并且喜欢让这些软件能够开箱即用，不需要查阅大量文档，也不需要进行复杂的配置。这是许多开源项目所欠缺的一个特性，但演讲者认为它对最终用户至关重要。他喜欢创建为速度和低资源消耗而优化的服务器。这也是他创建 VictoriaMetrics 的原因，它是一个用于指标（也称为时间序列数据）的开源数据库，非常高效和快速。最近，他又创建了 VictoriaLogs，这是另一个专门用于存储日志的数据库。</li>
</ul>
</li>
<li>
<p><strong>Q: VictoriaLogs 是否提供 UI？</strong></p>
<ul>
<li><strong>A:</strong> 是的。它内置了一个用于快速日志调查的 Web UI，并且提供了功能完备的 <strong>Grafana 插件</strong>，允许用户构建任意复杂的仪表盘。其查询语言是自研的 <strong>LogSQL</strong>，被设计得比 Loki 的 LogQL 等更强大，支持在单次查询中进行复杂的数据转换和多维度统计计算。</li>
</ul>
</li>
<li>
<p><strong>Q: 是否支持日志不可篡改（immutability）？</strong></p>
<ul>
<li><strong>A:</strong> VictoriaLogs <strong>不支持对已存日志的修改</strong>，只支持未来的删除操作（且该功能可被禁用），这在一定程度上保证了数据的不可篡改性。但它目前没有提供基于密码学的签名验证功能。</li>
</ul>
</li>
<li>
<p><strong>Q: 多租户支持如何？</strong></p>
<ul>
<li><strong>A:</strong> VictoriaLogs <strong>原生支持多租户</strong>，并且可以轻松处理数万级别的租户，这与 Loki 等因架构设计而在租户数量上受限的系统形成了对比。</li>
</ul>
</li>
<li>
<p><strong>Q: 对于更大的存储需求（如单个 EC2 实例挂载 450TB 磁盘），你会如何选择？</strong></p>
<ul>
<li><strong>A:</strong> 演讲者建议，虽然技术上可行，但他会选择<strong>水平扩展</strong>。他认为单节点存储的数据量最好有一个平衡点（例如 <strong>16TB</strong> 的压缩数据），因为过大的单节点会给备份和恢复带来巨大的运维挑战（可能需要数小时）。</li>
</ul>
</li>
<li>
<p><strong>Q: 未来的路线图是什么？</strong></p>
<ul>
<li><strong>A:</strong> 近期最重要的主线功能是<strong>支持将历史日志分层存储到对象存储（如 S3）中</strong>。系统将能够透明地将冷数据归档到更廉价的存储，并在查询时无缝地拉取，进一步降低成本。至于是否会支持完全无本地磁盘、直接读写对象存储的模式，团队表示会在此功能实现后再做评估，因为需要解决对象存储带来的高延迟问题。</li>
</ul>
</li>
</ul>
<h2>小结：为你的工作选择正确的工具</h2>
<p>Aliaksandr Valialkin 的分享为所有处理大规模数据的 Go 开发者提供了清晰、深刻的工程指引：<strong>不要试图用一把锤子（通用关系型数据库）去拧所有的螺丝。理解问题的本质，并选择专为该问题设计的工具。</strong></p>
<p>对于日志处理，这意味着：</p>
<ul>
<li><strong>拥抱专用数据库</strong>：当你每天的日志量超过 TB 级别，或者发现现有的日志系统运维成本高昂、查询缓慢时，从 PostgreSQL/MySQL 迁移到像 VictoriaLogs、ClickHouse 或 Loki 这样的专用系统，将带来数量级的成本节约和性能提升。</li>
<li><strong>优先垂直扩展</strong>：在投入到复杂且昂贵的水平扩展（分布式集群）之前，先通过使用正确的单机软件，充分压榨现代硬件的潜力。这不仅能节省成本，还能极大地降低运维的复杂性。</li>
</ul>
<p>正如演讲者所倡导的“小数据”运动理念：<strong>许多所谓的“大数据”问题，在正确的工具和架构面前，完全可以在单台计算机上被更简单、更高效地解决。</strong> 对于追求性能、效率和简洁性的 Go 开发者而言，这不仅是一次技术分享，更是一堂关于工程哲学的深刻课程。</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/20/large-scale-logging-made-easy/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起</title>
		<link>https://tonybai.com/2025/08/06/go-new-engine-of-old-languages/</link>
		<comments>https://tonybai.com/2025/08/06/go-new-engine-of-old-languages/#comments</comments>
		<pubDate>Wed, 06 Aug 2025 00:09:10 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[fpm]]></category>
		<category><![CDATA[FrankenPHP]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JS]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[supervisor]]></category>
		<category><![CDATA[TS]]></category>
		<category><![CDATA[TypeScript]]></category>
		<category><![CDATA[typescript-go]]></category>
		<category><![CDATA[Web]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5001</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/06/go-new-engine-of-old-languages 大家好，我是Tony Bai。 我先来描述一种编程语言生态，请你猜猜它是谁： 它诞生于 1995 年，旨在为当时一个叫“万维网”的新平台构建应用。起初只是个小项目，却在互联网泡沫中野蛮生长，成为史上用户最广的语言之一。它曾被“严肃”的程序员们嘲笑了几十年，但最终得到了科技巨头的加持，迎来了事业的第二春。如今，它正迈向 30 岁，而其生态中最重要的一环——它的一个超集语言的编译器，正在被 Go 语言 重写以驱动未来。 你的第一反应，很可能是 JavaScript 生态。完全正确。这个超集语言，就是 TypeScript。 但这段描述，同样完美地适用于另一个名字：PHP。它也诞生于 1995 年，同样在 Web 浪潮中崛起，同样被嘲笑，同样迎来了第二春，而现在，一个基于 Go 语言 的新项目，也正在驱动着它的未来。 这两种语言，就像是同一枚硬币的两面，共同定义了 Web 编程的客户端与服务器端。而今天，我想和你聊的，正是它们故事中那个令人意想不到的、与我们 Gopher 息息相关的交集——Go 语言的角色。 编程语言中的“丰田卡罗拉” 在深入主题之前，我们必须先理解 PHP 的生态位。一篇精彩的博文将其比作编程语言中的“丰田卡罗拉”——无聊、坚固、简单、实惠。 它或许永远不会出现在技术发布会最酷炫的 Demo 上，但它和它经典的 LAMP（Linux, Apache, MySQL, PHP）组合，让全世界数以百万计的普通开发者，能以最低的成本、最可靠的方式，解决一个最实际的问题：搭建一个能用的网站。 C++ 的创造者 Bjarne Stroustrup 有一句名言：“世界上只有两种语言：一种是被人拼命吐槽的，另一种是没人用的。” PHP 显然属于前者。它曾被嘲笑为“糟糕设计的集合体”，但它也支撑着全球 70% 以上的网站。这个数字，无论你用何种挑剔的眼光审视，都无法否认其巨大的成功和顽强的生命力。 Go：一个意想不到的“新引擎” 多年以来，PHP 和 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-new-engine-of-old-languages-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/06/go-new-engine-of-old-languages">本文永久链接</a> &#8211; https://tonybai.com/2025/08/06/go-new-engine-of-old-languages</p>
<p>大家好，我是Tony Bai。</p>
<p>我先来描述一种编程语言生态，请你猜猜它是谁：</p>
<blockquote>
<p>它诞生于 1995 年，旨在为当时一个叫“万维网”的新平台构建应用。起初只是个小项目，却在互联网泡沫中野蛮生长，成为史上用户最广的语言之一。它曾被“严肃”的程序员们嘲笑了几十年，但最终得到了科技巨头的加持，迎来了事业的第二春。如今，它正迈向 30 岁，而其生态中最重要的一环——它的一个超集语言的编译器，正在被 <strong>Go 语言</strong> 重写以驱动未来。</p>
</blockquote>
<p>你的第一反应，很可能是 <strong>JavaScript</strong> 生态。完全正确。这个超集语言，就是 TypeScript。</p>
<p>但这段描述，同样完美地适用于另一个名字：<strong>PHP</strong>。它也诞生于 1995 年，同样在 Web 浪潮中崛起，同样被嘲笑，同样迎来了第二春，而现在，一个基于 <strong>Go 语言</strong> 的新项目，也正在驱动着它的未来。</p>
<p>这两种语言，就像是同一枚硬币的两面，共同定义了 Web 编程的客户端与服务器端。而今天，我想和你聊的，正是它们故事中那个令人意想不到的、与我们 Gopher 息息相关的交集——Go 语言的角色。</p>
<h2>编程语言中的“丰田卡罗拉”</h2>
<p>在深入主题之前，我们必须先理解 PHP 的生态位。<a href="https://deprogrammaticaipsum.com/the-toyota-corolla-of-programming/">一篇精彩的博文</a>将其比作<strong>编程语言中的“丰田卡罗拉”——无聊、坚固、简单、实惠。</strong></p>
<p>它或许永远不会出现在技术发布会最酷炫的 Demo 上，但它和它经典的 LAMP（Linux, Apache, MySQL, PHP）组合，让全世界数以百万计的普通开发者，能以最低的成本、最可靠的方式，解决一个最实际的问题：搭建一个能用的网站。</p>
<p>C++ 的创造者 Bjarne Stroustrup 有一句名言：“世界上只有两种语言：一种是被人拼命吐槽的，另一种是没人用的。”</p>
<p>PHP 显然属于前者。它曾被嘲笑为“糟糕设计的集合体”，但它也支撑着全球 70% 以上的网站。这个数字，无论你用何种挑剔的眼光审视，都无法否认其巨大的成功和顽强的生命力。</p>
<h2>Go：一个意想不到的“新引擎”</h2>
<p>多年以来，PHP 和 JavaScript 这两个庞大的生态，在各自的轨道上独立演进。但最近，一个令人瞩目的趋势正在浮现：<strong>Go 语言，正在成为驱动这两个“老旧”生态进行现代化改造的“新引擎”。</strong></p>
<p><strong>案例一：FrankenPHP &#8211; 用 Go 为 PHP “换心”</strong></p>
<p>如果你经历过在容器时代部署 PHP 应用的痛苦，你一定对 Nginx + FPM + Supervisor 这套复杂而脆弱的“三件套”记忆犹新。配置繁琐、性能瓶颈、进程管理困难，每一个都是噩梦。</p>
<p>现在，<strong>FrankenPHP</strong> 出现了。这是一个用 Go 语言编写的、全新的、高性能的 PHP 应用服务器，<a href="https://thephp.foundation/blog/2025/06/08/php-30/">最近已被 PHP 基金会正式采纳</a>。</p>
<p>它的革命性在于：</p>
<ol>
<li><strong>部署极简</strong>：它是一个<strong>单一的静态 Go 二进制文件</strong>。部署一个 PHP 应用，现在只需要一个包含这个二进制文件和你的 PHP 代码的、极其简单的 Dockerfile。Nginx, FPM, Supervisor 通通被扔进了历史的垃圾堆。</li>
<li><strong>性能卓越</strong>：它内置了一个基于 Caddy（另一个伟大的 Go 项目）的高性能 HTTP 服务器，并提供了全新的执行模型，性能远超传统模式。</li>
<li><strong>能力强大</strong>：Go 强大的并发能力和成熟的网络库，让 FrankenPHP 天生具备了现代应用服务器所需的一切。</li>
</ol>
<p>是 Go 语言，以一种釜底抽薪的方式，解决了 PHP 生态在云原生时代最大的部署和运维难题。</p>
<p><strong>案例二：新版 TypeScript 编译器 &#8211; 用 Go 提速</strong></p>
<p>无独有偶，在 Web 的另一端，JavaScript 生态也迎来了 Go 语言的赋能。微软最近宣布了一个激动人心的项目：用 <a href="https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg">Go 语言来重写 TypeScript 编译器</a>。</p>
<p>TypeScript 作为 JavaScript 的超集，已经成为构建大型、复杂前端和后端应用的事实标准。它的编译器，是整个生态中至关重要的基础设施。</p>
<p>为什么选择 Go？答案同样简单而直接：<strong>性能</strong>，<a href="https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg">当然也有其他一些考虑</a>。</p>
<p>编译器本质上是极其消耗 CPU 的密集型任务。随着 TypeScript 项目日益庞大和复杂，原有的编译器性能逐渐成为瓶颈。而 Go 语言，凭借其接近 C/C++ 的运行效率、卓越的并发模型以及内存安全保证，成为了构建下一代高性能编译器的理想选择。</p>
<h2>Go 语言的新角色：从“建新城”到“改旧都”</h2>
<p>这两个案例，揭示了 Go 语言一个正在崛起的新角色。</p>
<p>过去，我们谈论 Go，更多的是用它来<strong>构建全新的云原生微服务</strong>——我们用它在一片空地上“建新城”。但现在，我们看到，Go 凭借其三大核心优势，正在成为<strong>改造和赋能现有庞大技术生态的“基础设施底座”</strong>。我们开始用它来“改造旧都”。</p>
<p>这三大优势是：</p>
<ol>
<li><strong>极致的性能</strong>：对于需要压榨性能的系统工具（如编译器、服务器），Go 提供了一个远比 C/C++ 更安全、更具生产力的选择。</li>
<li><strong>无与伦比的部署简便性</strong>：静态链接的单一二进制文件，是为容器和 DevOps 时代而生的“终极交付物”。</li>
<li><strong>现代化的并发模型</strong>：Goroutine 和 Channel，为解决现代软件中无处不在的并发问题，提供了最优雅、最高效的语言级方案。</li>
</ol>
<p>Go 语言，正在从一个单纯的应用开发语言，下沉为更底层的、为其他生态提供核心动力的“引擎层”。</p>
<h2>结论：拥抱务实，而非追逐光环</h2>
<p>PHP 的故事，以及它与 Go 的这段奇妙姻缘，带给我们最深刻的启示，是一种超越语言之争的<strong>工程实用主义精神</strong>。</p>
<p>真正的技术进步，不仅仅在于创造全新的、闪闪发光的东西，更在于用更强大的工具，去务实地优化、改造和盘活那些已经支撑着世界运转的庞大系统。这是一种更深沉、更具影响力的贡献。</p>
<p>而 Go 语言，正在这个伟大的进程中，扮演着越来越重要的角色。作为 Gopher，我们不仅在“建新城”，我们也在为这个数字世界的“旧都”，换上一个更强劲、更可靠的“新引擎”。这，或许是 Go 语言未来最激动人心的篇章之一。</p>
<p>资料链接：https://deprogrammaticaipsum.com/the-toyota-corolla-of-programming/</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/06/go-new-engine-of-old-languages/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来</title>
		<link>https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems/</link>
		<comments>https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems/#comments</comments>
		<pubDate>Tue, 01 Jul 2025 00:46:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Agentic]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Akka]]></category>
		<category><![CDATA[Craft]]></category>
		<category><![CDATA[DataFusion]]></category>
		<category><![CDATA[DuckDB]]></category>
		<category><![CDATA[Gin]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-kit]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gollum]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kratos]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[minio]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[parquet]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[S3]]></category>
		<category><![CDATA[SnowFlake]]></category>
		<category><![CDATA[spark]]></category>
		<category><![CDATA[sqlite]]></category>
		<category><![CDATA[Temporal]]></category>
		<category><![CDATA[TiDB]]></category>
		<category><![CDATA[Unison]]></category>
		<category><![CDATA[WasmCloud]]></category>
		<category><![CDATA[workflow]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[分布式系统]]></category>
		<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=4860</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems 大家好，我是Tony Bai。 身处技术浪潮之中，我们每个人或许都曾有过这样的焦虑：新的数据库、新的编程模型、新的 AI 框架层出不穷，我该如何选择？选错了，会不会让团队陷入泥潭，给自己留下难以偿还的技术债？ 最近，特斯拉首席工程师 Colin Breck 在 Craft 2025 大会上做了一场题为《预测分布式系统的未来》的精彩分享。他并没有给出非黑即白的答案，而是提供了一个极其强大的思维武器，来帮助我们拨开迷雾，做出更有效的工程决策。这个武器，就是源自亚马逊创始人 Jeff Bezos 的——“单向门 vs. 双向门”决策框架。 今天，我们就以这个框架为钥匙，跟随 Colin 的思路，去打开分布式系统的未来之门。 决策的“导航仪”：单向门 vs. 双向门 在深入技术细节之前，我们必须先理解这个核心框架。它将决策分为两类： 单向门 (One-Way Door)： 这类决策后果严重，且难以逆转，甚至根本无法回头。一旦你迈进了这扇门，想再出来就要付出巨大的代价。对于“单向门”决策，Bezos 的建议是：必须极其谨慎，放慢速度，召集最相关的人，尽可能多地收集信息再做决定。 双向门 (Two-Way Door)： 这类决策的影响不大，即使做错了，也可以轻松地“退出来”，再选择另一扇门。它的试错成本很低。对于“双向门”决策，应该快速、轻量地由个人或小团队做出，以保持高效率。 这个框架最大的价值在于，它提醒我们警惕一个致命的错误：把一个“单向门”决策，当作“双向门”来草率处理。 这种失误，可能会让你的组织背上沉重的技术包袱，长达数年。 现在，让我们带着这个“导航仪”，去审视 Colin 预测的分布式系统三大趋势。 趋势一：对象存储 —— 充满“双向门”的乐园 Colin 的第一个预测是，对象存储（以 S3 为代表）正在从过去的分析型负载，越来越多地走向事务型和操作型负载，成为下一代数据库和系统的基石。 为什么这个趋势如此确定？因为它为我们创造了大量的“双向门”。 过去，我们选择一个数据库（比如 MySQL），我们的数据、查询方式、扩展模式都被这个“整体”方案深度绑定。想从 MySQL 迁移到 PostgreSQL？这是一项艰巨的任务，更像一扇“单向门”。 而基于对象存储的新架构正在“解体”(Disaggregation) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/predicting-the-future-of-distributed-systems-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems">本文永久链接</a> &#8211; https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems</p>
<p>大家好，我是Tony Bai。</p>
<p>身处技术浪潮之中，我们每个人或许都曾有过这样的焦虑：新的数据库、新的编程模型、新的 AI 框架层出不穷，我该如何选择？选错了，会不会让团队陷入泥潭，给自己留下难以偿还的技术债？</p>
<p>最近，特斯拉首席工程师 Colin Breck 在 Craft 2025 大会上做了一场题为《<a href="https://www.youtube.com/watch?v=NEkO7nUmhzU">预测分布式系统的未来</a>》的精彩分享。他并没有给出非黑即白的答案，而是提供了一个极其强大的思维武器，来帮助我们拨开迷雾，做出更有效的工程决策。这个武器，就是源自亚马逊创始人 Jeff Bezos 的——<strong>“单向门 vs. 双向门”决策框架</strong>。</p>
<p>今天，我们就以这个框架为钥匙，跟随 Colin 的思路，去打开分布式系统的未来之门。</p>
<h2>决策的“导航仪”：单向门 vs. 双向门</h2>
<p>在深入技术细节之前，我们必须先理解这个核心框架。它将决策分为两类：</p>
<ol>
<li>
<p><strong>单向门 (One-Way Door)：</strong> 这类决策后果严重，且难以逆转，甚至根本无法回头。一旦你迈进了这扇门，想再出来就要付出巨大的代价。对于“单向门”决策，Bezos 的建议是：<strong>必须极其谨慎，放慢速度，召集最相关的人，尽可能多地收集信息再做决定。</strong></p>
</li>
<li>
<p><strong>双向门 (Two-Way Door)：</strong> 这类决策的影响不大，即使做错了，也可以轻松地“退出来”，再选择另一扇门。它的试错成本很低。对于“双向门”决策，<strong>应该快速、轻量地由个人或小团队做出，以保持高效率。</strong></p>
</li>
</ol>
<p>这个框架最大的价值在于，它提醒我们警惕一个致命的错误：<strong>把一个“单向门”决策，当作“双向门”来草率处理。</strong> 这种失误，可能会让你的组织背上沉重的技术包袱，长达数年。</p>
<p>现在，让我们带着这个“导航仪”，去审视 Colin 预测的分布式系统三大趋势。</p>
<h2>趋势一：对象存储 —— 充满“双向门”的乐园</h2>
<p>Colin 的第一个预测是，对象存储（以 S3 为代表）正在从过去的分析型负载，越来越多地走向事务型和操作型负载，成为下一代数据库和系统的基石。</p>
<p>为什么这个趋势如此确定？<strong>因为它为我们创造了大量的“双向门”。</strong></p>
<p>过去，我们选择一个数据库（比如 MySQL），我们的数据、查询方式、扩展模式都被这个“整体”方案深度绑定。想从 MySQL 迁移到 PostgreSQL？这是一项艰巨的任务，更像一扇“单向门”。</p>
<p>而基于对象存储的新架构正在“解体”(Disaggregation) 传统数据库，将其拆分为多个可自由组合的组件：</p>
<ul>
<li><strong>统一的存储层：</strong> S3 API 已成为事实标准。你可以用 AWS S3，也可以用 Google Cloud Storage，或者在本地部署 <a href="https://tonybai.com/2020/03/16/build-high-performance-object-storage-with-minio-part1-prototype/">MinIO</a>。更换存储后端的门是“双向”的。</li>
<li><strong>开放的数据格式：</strong> <a href="https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6">Parquet</a>、ORC等开放格式让你的数据不再被数据库私有格式锁定。今天你可以用 Spark 分析它，明天可以用 DuckDB 查询它，后天可以加载到 Snowflake。更换计算引擎的门是“双向”的。</li>
<li><strong>可插拔的计算/查询引擎：</strong> DuckDB、DataFusion 这类库的崛起，让我们能像使用 SQLite 一样，直接对 S3 上的 Parquet 文件执行高性能 SQL 查询。这个查询引擎不满意？换一个！这扇门也是“双向”的。</li>
</ul>
<p><strong>这种架构的核心是互操作性与可移植性。它通过标准化和解耦，极大地降低了我们的决策风险和迁移成本。</strong> 正因为到处都是“双向门”，开发者可以放心大胆地拥抱这个趋势。</p>
<h2>趋势二：新编程模型 —— 遍布“单向门”的迷宫</h2>
<p>与对象存储的清晰图景相反，Colin 认为下一代编程模型的未来则要模糊得多，充满了艰难的“单向门”决策。</p>
<p>我们当前的开发模式（容器 + 应用代码 + 一堆库）存在很多问题：每个应用都在重复解决持久化、重试、状态管理等难题；安全补丁也难以管理。</p>
<p>为了解决这些问题，涌现出了一批新的编程模型，例如：</p>
<ul>
<li><strong>持久化工作流平台：</strong> 如 Temporal</li>
<li><strong>分布式应用运行时：</strong> 如 Akka Platform、WasmCloud</li>
<li><strong>独特的运行时环境：</strong> 如 <a href="https://github.com/trivago/gollum">Gollum</a>、<a href="https://github.com/unisonweb/unison">Unison</a></li>
</ul>
<p>它们的目标很宏大：让开发者只关心业务逻辑，把持久化执行、状态管理、部分失败处理等分布式难题下沉到基础设施。</p>
<p>但选择其中任何一个，都几乎是一个不可逆的“单向门”决策。为什么？</p>
<ol>
<li><strong>巨大的投资：</strong> 这不仅是金钱投入，更是整个团队的学习成本和思维模式的转变。</li>
<li><strong>深度锁定：</strong> 你的核心业务逻辑将与平台的 API 和抽象深度绑定，想迁移出去？难于登天。</li>
<li><strong>生态系统风险：</strong> 这个平台或框架五年后还活着吗？如果它死掉了，你的系统怎么办？</li>
</ol>
<p>正因为这些决策都是沉重的“单向门”，大多数团队宁愿继续使用 Kubernetes + 应用容器这种“我们已经知道”的模式，也不愿轻易踏入这个迷宫。</p>
<h2>趋势三：AI 工程化 —— 可能是打开“单向门”的催化剂</h2>
<p>那么，僵局如何打破？Colin 认为，催化剂可能就是 AI。</p>
<p>他一针见血地指出：<strong>“所谓的 AI 工程化（Operationalizing AI），其本质就是系统工程。”</strong></p>
<p>那些时髦的术语背后，无论是 AI 工作流（AI Workflows）还是智能体（Agentic AI），其核心都是在解决经典的分布式系统难题：如何管理长周期任务、如何保证持久化执行、如何处理状态、如何容错……正如那句经典吐槽：“到35岁，你应该已经重复造过工作流引擎、任务队列和对象关系映射的轮子了。”</p>
<p>AI 的浪潮带来了巨大的需求压力和创新动力，使得人们<strong>愿意去冒更大的风险，去尝试那些能解决这些复杂问题的“单向门”方案</strong>。一个创业公司为了快速实现一个复杂的 AI Agent，可能会选择直接拥抱 Temporal，因为从头造轮子的成本更高。</p>
<p>但这同样是一个陷阱。Colin 警告说，要警惕那些看似“先跑起来再说”的“双向门”决策，比如随便搭一个临时的任务队列来驱动 AI 应用。这种决策很可能在未来演变成一笔巨大的、难以偿还的技术债，最终变成一个你当初没意识到的“单向门”。</p>
<h2>给 Gopher 的启示：用“门”的思维审视我们的技术栈</h2>
<p>这个决策框架对我们 Gopher 来说，同样具有极强的指导意义。我们可以用它来审视日常的技术选型：</p>
<ul>
<li><strong>选择 Web 框架（Gin vs. 标准库）：</strong> 这更像一个“双向门”。Gin 遵循了标准库的 http.Handler接口，即使以后想换，迁移成本也是可控的。</li>
<li><strong>引入一个新的数据库（PostgreSQL vs. TiDB）：</strong> 这更偏向“单向门”。它涉及到数据模型、ORM、运维、团队知识储备等方方面面，一旦深入使用，更换成本极高。</li>
<li><strong>采用一个微服务框架（Go-kit vs. Kratos）：</strong> 这也接近“单向门”。它会深度影响你的项目结构、RPC 方式、服务治理逻辑，更换起来伤筋动骨。</li>
</ul>
<p>反观 Go 语言自身的设计哲学——<strong>简洁、小接口、组合优于继承</strong>——是不是正是在鼓励我们创造更多的“双向门”？Go 避免了庞大而笨重的“全家桶”式框架，而是提供小而美的标准库和可组合的组件，让我们能以更低的锁定风险构建系统。这本身就是一种降低决策成本的智慧。</p>
<h2>小结：决策的智慧，在于选择正确的“门”</h2>
<p>Colin Breck 的分享，并没有给我们一张未来的藏宝图，而是给了我们一个更宝贵的东西：<strong>一个决策的指南针</strong>。</p>
<p>技术世界里没有绝对的“好”与“坏”，只有在特定场景下的“合适”与“不合适”。“单向门 vs. 双向门”框架的价值，不在于帮你找到唯一的正确答案，而在于帮你为不同类型的决策，建立起正确的决策流程。</p>
<p>对于那些充满不确定性、一旦走错就万劫不复的“单向门”，请务必保持敬畏，放慢脚步。而对于那些无伤大雅的“双向门”，不妨大胆尝试，快速迭代。</p>
<p>正如 Colin 在结尾引用的那句话：“让我们的抽象保持流动性。” 这或许不仅是对技术架构的建议，更是对我们决策方式的邀请——<strong>去寻找和创造尽可能多的“双向门”，以降低风险、拥抱变化，并保护我们最宝贵的投资：时间和精力。</strong></p>
<p>你最近面临过哪些“单向门”或“双向门”决策？你是如何思考的？欢迎在评论区分享你的故事。</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-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/07/01/predicting-the-future-of-distributed-systems/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go开发者必知：五大缓存策略详解与选型指南</title>
		<link>https://tonybai.com/2025/04/28/five-cache-strategies/</link>
		<comments>https://tonybai.com/2025/04/28/five-cache-strategies/#comments</comments>
		<pubDate>Mon, 28 Apr 2025 14:22:18 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Cache-Aside]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[lazyloading]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[mermaid]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Read-Through]]></category>
		<category><![CDATA[TTL]]></category>
		<category><![CDATA[Write-Around]]></category>
		<category><![CDATA[Write-Back]]></category>
		<category><![CDATA[Write-Behind]]></category>
		<category><![CDATA[Write-Through]]></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=4636</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/28/five-cache-strategies 大家好，我是Tony Bai。 在构建高性能、高可用的后端服务时，缓存几乎是绕不开的话题。无论是为了加速数据访问，还是为了减轻数据库等主数据源的压力，缓存都扮演着至关重要的角色。对于我们 Go 开发者来说，选择并正确地实施缓存策略，是提升应用性能的关键技能之一。 目前业界主流的缓存策略有多种，每种都有其独特的适用场景和优缺点。今天，我们就来探讨其中五种最常见也是最核心的缓存策略：Cache-Aside、Read-Through、Write-Through、Write-Behind (Write-Back) 和Write-Around，并结合Go语言的特点和示例（使用内存缓存和SQLite），帮助大家在实际项目中做出明智的选择。 0. 准备工作：示例代码环境与结构 为了清晰地演示这些策略，本文的示例代码采用了模块化的结构，将共享的模型、缓存接口、数据库接口以及每种策略的实现分别放在不同的包中。我们将使用Go语言，配合一个简单的内存缓存（带 TTL 功能）和一个 SQLite 数据库作为持久化存储。 示例项目的结构如下： $tree -F ./go-cache-strategy ./go-cache-strategy ├── go.mod ├── go.sum ├── internal/ │   ├── cache/ │   │   └── cache.go │   ├── database/ │   │   └── database.go │   └── models/ │   └── models.go ├── main.go └── strategy/ ├── cacheaside/ [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/28/five-cache-strategies">本文永久链接</a> &#8211; https://tonybai.com/2025/04/28/five-cache-strategies</p>
<p>大家好，我是Tony Bai。</p>
<p>在构建高性能、高可用的后端服务时，缓存几乎是绕不开的话题。无论是为了加速数据访问，还是为了减轻数据库等主数据源的压力，缓存都扮演着至关重要的角色。对于我们 Go 开发者来说，选择并正确地实施缓存策略，是提升应用性能的关键技能之一。</p>
<p>目前业界主流的缓存策略有多种，每种都有其独特的适用场景和优缺点。今天，我们就来探讨其中五种最常见也是最核心的缓存策略：Cache-Aside、Read-Through、Write-Through、Write-Behind (Write-Back) 和Write-Around，并结合Go语言的特点和示例（使用内存缓存和SQLite），帮助大家在实际项目中做出明智的选择。</p>
<h2>0. 准备工作：示例代码环境与结构</h2>
<p>为了清晰地演示这些策略，本文的示例代码采用了模块化的结构，将共享的模型、缓存接口、数据库接口以及每种策略的实现分别放在不同的包中。我们将使用Go语言，配合一个简单的<strong>内存缓存</strong>（带 TTL 功能）和一个 <strong>SQLite 数据库</strong>作为持久化存储。</p>
<p>示例项目的结构如下：</p>
<pre><code>$tree -F ./go-cache-strategy
./go-cache-strategy
├── go.mod
├── go.sum
├── internal/
│   ├── cache/
│   │   └── cache.go
│   ├── database/
│   │   └── database.go
│   └── models/
│       └── models.go
├── main.go
└── strategy/
    ├── cacheaside/
    │   └── cacheaside.go
    ├── readthrough/
    │   └── readthrough.go
    ├── writearound/
    │   └── writearound.go
    ├── writebehind/
    │   └── writebehind.go
    └── writethrough/
        └── writethrough.go
</code></pre>
<p>其中核心组件包括：</p>
<ul>
<li>internal/models: 定义共享数据结构 (如 User, LogEntry)。</li>
<li>internal/cache: 定义 Cache 接口及 InMemoryCache 实现。</li>
<li>internal/database: 定义 Database 接口及 SQLite DB 实现。</li>
<li>strategy/xxx: 每个子目录包含一种缓存策略的核心实现逻辑。</li>
</ul>
<p><strong>注意：</strong> 文中仅展示各策略的核心实现代码片段。<strong>完整的、可运行的示例项目代码在Github上，大家可以通过文末链接访问。</strong></p>
<p>接下来，我们将详细介绍五种缓存策略及其Go实现片段。</p>
<h2>1. Cache-Aside (旁路缓存/懒加载Lazy Loading)</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-2.png" alt="" /></p>
<p>这是最常用、也最经典的缓存策略。<strong>核心思想是：应用程序自己负责维护缓存。</strong></p>
<p><strong>工作流程：</strong></p>
<ol>
<li>应用需要读取数据时，<strong>先</strong>检查缓存中是否存在。</li>
<li><strong>缓存命中 (Hit):</strong> 如果存在，直接从缓存返回数据。</li>
<li><strong>缓存未命中 (Miss):</strong> 如果不存在，应用从主数据源（如数据库）读取数据。</li>
<li>读取成功后，应用将数据<strong>写入缓存</strong>（设置合理的过期时间）。</li>
<li>最后，应用将数据返回给调用方。</li>
</ol>
<p><strong>Go示例 (核心实现 &#8211; strategy/cacheaside/cacheaside.go):</strong></p>
<pre><code class="go">package cacheaside

import (
    "context"
    "fmt"
    "log"
    "time"

    "cachestrategysdemo/internal/cache"
    "cachestrategysdemo/internal/database"
    "cachestrategysdemo/internal/models"
)

const userCacheKeyPrefix = "user:" // Example prefix

// GetUser retrieves user info using Cache-Aside strategy.
func GetUser(ctx context.Context, userID string, db database.Database, memCache cache.Cache, ttl time.Duration) (*models.User, error) {
    cacheKey := userCacheKeyPrefix + userID

    // 1. Check cache first
    if cachedVal, found := memCache.Get(cacheKey); found {
        if user, ok := cachedVal.(*models.User); ok {
            log.Println("[Cache-Aside] Cache Hit for user:", userID)
            return user, nil
        }
        memCache.Delete(cacheKey) // Remove bad data
    }

    // 2. Cache Miss
    log.Println("[Cache-Aside] Cache Miss for user:", userID)

    // 3. Fetch from Database
    user, err := db.GetUser(ctx, userID)
    if err != nil {
        return nil, fmt.Errorf("failed to get user from DB: %w", err)
    }
    if user == nil {
        return nil, nil // Not found
    }

    // 4. Store data into cache
    memCache.Set(cacheKey, user, ttl)
    log.Println("[Cache-Aside] User stored in cache:", userID)

    // 5. Return data
    return user, nil
}

</code></pre>
<p><strong>优点:</strong><br />
*   实现相对简单直观。<br />
*   对读密集型应用效果好，缓存命中时速度快。<br />
*   缓存挂掉不影响应用读取主数据源（只是性能下降）。</p>
<p><strong>缺点:</strong><br />
*   首次请求（冷启动）或缓存过期后，会有一次缓存未命中，延迟较高。<br />
*   存在数据不一致的风险：需要额外的缓存失效策略。<br />
*   应用代码与缓存逻辑耦合。</p>
<p><strong>使用场景:</strong> 读多写少，能容忍短暂数据不一致的场景。</p>
<h3>2. Read-Through (穿透读缓存)</h3>
<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-3.png" alt="" /></p>
<p><strong>核心思想：应用程序将缓存视为主要数据源，只与缓存交互。缓存内部负责在未命中时从主数据源加载数据。</strong></p>
<p><strong>工作流程：</strong></p>
<ol>
<li>应用向缓存请求数据。</li>
<li>缓存检查数据是否存在。</li>
<li><strong>缓存命中:</strong> 直接返回数据。</li>
<li><strong>缓存未命中:</strong> 缓存<strong>自己</strong>负责从主数据源加载数据。</li>
<li>加载成功后，缓存将数据存入自身，并返回给应用。</li>
</ol>
<p><strong>Go 示例 (模拟实现 &#8211; strategy/readthrough/readthrough.go):</strong></p>
<p>Read-Through 通常依赖缓存库自身特性。这里我们通过封装 Cache 接口模拟其行为。</p>
<pre><code class="go">package readthrough

import (
    "context"
    "fmt"
    "log"
    "time"

    "cachestrategysdemo/internal/cache"
    "cachestrategysdemo/internal/database"
)

// LoaderFunc defines the function signature for loading data on cache miss.
type LoaderFunc func(ctx context.Context, key string) (interface{}, error)

// Cache wraps a cache instance to provide Read-Through logic.
type Cache struct {
    cache      cache.Cache // Use the cache interface
    loaderFunc LoaderFunc
    ttl        time.Duration
}

// New creates a new ReadThrough cache wrapper.
func New(cache cache.Cache, loaderFunc LoaderFunc, ttl time.Duration) *Cache {
    return &amp;Cache{cache: cache, loaderFunc: loaderFunc, ttl: ttl}
}

// Get retrieves data, using the loader on cache miss.
func (rtc *Cache) Get(ctx context.Context, key string) (interface{}, error) {
    // 1 &amp; 2: Check cache
    if cachedVal, found := rtc.cache.Get(key); found {
        log.Println("[Read-Through] Cache Hit for:", key)
        return cachedVal, nil
    }

    // 4: Cache Miss - Cache calls loader
    log.Println("[Read-Through] Cache Miss for:", key)
    loadedVal, err := rtc.loaderFunc(ctx, key) // Loader fetches from DB
    if err != nil {
        return nil, fmt.Errorf("loader function failed for key %s: %w", key, err)
    }
    if loadedVal == nil {
        return nil, nil // Not found from loader
    }

    // 5: Store loaded data into cache &amp; return
    rtc.cache.Set(key, loadedVal, rtc.ttl)
    log.Println("[Read-Through] Loaded and stored in cache:", key)
    return loadedVal, nil
}

// Example UserLoader function (needs access to DB instance and key prefix)
func NewUserLoader(db database.Database, keyPrefix string) LoaderFunc {
    return func(ctx context.Context, cacheKey string) (interface{}, error) {
        userID := cacheKey[len(keyPrefix):] // Extract ID
        // log.Println("[Read-Through Loader] Loading user from DB:", userID)
        return db.GetUser(ctx, userID)
    }
}
</code></pre>
<p><strong>优点:</strong><br />
*   应用代码逻辑更简洁，将数据加载逻辑从应用中解耦出来。<br />
*   代码更易于维护和测试（可以单独测试 Loader）。</p>
<p><strong>缺点:</strong><br />
*   强依赖缓存库或服务是否提供此功能，或需要自行封装。<br />
*   首次请求延迟仍然存在。<br />
*   数据不一致问题依然存在。</p>
<p><strong>使用场景:</strong> 读密集型，希望简化应用代码，使用的缓存系统支持此特性或愿意自行封装。</p>
<h3>3. Write-Through (穿透写缓存)</h3>
<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-4.png" alt="" /></p>
<p><strong>核心思想：数据一致性优先！应用程序更新数据时，同时写入缓存和主数据源，并且两者都成功后才算操作完成。</strong></p>
<p><strong>工作流程：</strong></p>
<ol>
<li>应用发起写请求（新增或更新）。</li>
<li>应用<strong>先</strong>将数据写入主数据源（或缓存，顺序可选）。</li>
<li>如果第一步成功，应用<strong>再</strong>将数据写入另一个存储（缓存或主数据源）。</li>
<li>第二步写入成功（或至少尝试写入）后，操作完成，向调用方返回成功。</li>
<li><strong>通常以主数据源写入成功为准</strong>，缓存写入失败一般只记录日志。</li>
</ol>
<p><strong>Go 示例 (核心实现 &#8211; strategy/writethrough/writethrough.go):</strong></p>
<pre><code class="go">package writethrough

import (
    "context"
    "fmt"
    "log"
    "time"

    "cachestrategysdemo/internal/cache"
    "cachestrategysdemo/internal/database"
    "cachestrategysdemo/internal/models"
)

const userCacheKeyPrefix = "user:" // Example prefix

// UpdateUser updates user info using Write-Through strategy.
func UpdateUser(ctx context.Context, user *models.User, db database.Database, memCache cache.Cache, ttl time.Duration) error {
    cacheKey := userCacheKeyPrefix + user.ID

    // Decision: Write to DB first for stronger consistency guarantee.
    log.Println("[Write-Through] Writing to database first for user:", user.ID)
    err := db.UpdateUser(ctx, user)
    if err != nil {
        // DB write failed, do not proceed to cache write
        return fmt.Errorf("failed to write to database: %w", err)
    }
    log.Println("[Write-Through] Successfully wrote to database for user:", user.ID)

    // Now write to cache (best effort after successful DB write).
    log.Println("[Write-Through] Writing to cache for user:", user.ID)
    memCache.Set(cacheKey, user, ttl)
    // If strict consistency cache+db is needed, distributed transaction is required (complex).
    // For simplicity, assume cache write is best-effort. Log potential errors.

    return nil
}
</code></pre>
<p><strong>优点:</strong><br />
*   数据一致性相对较高。<br />
*   读取时（若命中）能获取较新数据。</p>
<p><strong>缺点:</strong><br />
*   写入延迟较高。<br />
*   实现需考虑失败处理（特别是DB成功后缓存失败的情况）。<br />
*   缓存可能成为写入瓶颈。</p>
<p><strong>使用场景:</strong> 对数据一致性要求较高，可接受一定的写延迟。</p>
<h3>4. Write-Behind / Write-Back (回写 / 后写缓存)</h3>
<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-5.png" alt="" /></p>
<p><strong>核心思想：写入性能优先！应用程序只将数据写入缓存，缓存立即返回成功。缓存随后异步地、批量地将数据写入主数据源。</strong></p>
<p><strong>工作流程：</strong></p>
<ol>
<li>应用发起写请求。</li>
<li>应用将数据写入缓存。</li>
<li><strong>缓存立即</strong>向应用返回成功。</li>
<li>缓存将此写操作放入一个队列或缓冲区。</li>
<li>一个<strong>独立的后台任务</strong>在稍后将队列中的数据<strong>批量</strong>写入主数据源。</li>
</ol>
<p><strong>Go 示例 (核心实现 &#8211; strategy/writebehind/writebehind.go):</strong></p>
<pre><code class="go">package writebehind

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "cachestrategysdemo/internal/cache"
    "cachestrategysdemo/internal/database"
    "cachestrategysdemo/internal/models"
)

// Config holds configuration for the Write-Behind strategy.
type Config struct {
    Cache     cache.Cache
    DB        database.Database
    KeyPrefix string
    TTL       time.Duration
    QueueSize int
    BatchSize int
    Interval  time.Duration
}

// Strategy holds the state for the Write-Behind implementation.
type Strategy struct {
    // ... (fields: cache, db, updateQueue, wg, stopOnce, cancelCtx/Func, dbWriteMutex, config fields) ...
    // Fields defined in the full code example provided previously
    cache       cache.Cache
    db          database.Database
    updateQueue chan *models.User
    wg          sync.WaitGroup
    stopOnce    sync.Once
    cancelCtx   context.Context
    cancelFunc  context.CancelFunc
    dbWriteMutex sync.Mutex // Simple lock for batch DB writes
    keyPrefix   string
    ttl         time.Duration
    batchSize   int
    interval    time.Duration
}

// New creates and starts a new Write-Behind strategy instance.
// (Implementation details in full code example - initializes struct, starts worker)
func New(cfg Config) *Strategy {
    // ... (Initialization code as provided previously) ...
    // For brevity, showing only the function signature here.
    // It sets defaults, creates the context/channel, and starts the worker goroutine.
    // Returns the *Strategy instance.
    // ... Full implementation in GitHub Repo ...
    panic("Full implementation required from GitHub Repo") // Placeholder
}

// UpdateUser queues a user update using Write-Behind strategy.
func (s *Strategy) UpdateUser(ctx context.Context, user *models.User) error {
    cacheKey := s.keyPrefix + user.ID
    s.cache.Set(cacheKey, user, s.ttl) // Write to cache immediately

    // Add to async queue
    select {
    case s.updateQueue &lt;- user:
        return nil // Return success to the client immediately
    default:
        // Queue is full! Handle backpressure.
        log.Printf("[Write-Behind] Error: Update queue is full. Dropping update for user: %s\n", user.ID)
        return fmt.Errorf("update queue overflow for user %s", user.ID)
    }
}

// dbWriterWorker processes the queue (Implementation details in full code example)
func (s *Strategy) dbWriterWorker() {
    // ... (Worker loop logic: select on queue, ticker, context cancellation) ...
    // ... (Calls flushBatchToDB) ...
    // ... Full implementation in GitHub Repo ...
}

// flushBatchToDB writes a batch to the database (Implementation details in full code example)
func (s *Strategy) flushBatchToDB(ctx context.Context, batch []*models.User) {
    // ... (Handles batch write logic using s.db.BulkUpdateUsers) ...
    // ... Full implementation in GitHub Repo ...
}

// Stop gracefully shuts down the Write-Behind worker.
// (Implementation details in full code example - signals context, waits for WaitGroup)
func (s *Strategy) Stop() {
    // ... (Stop logic using stopOnce, cancelFunc, wg.Wait) ...
    // ... Full implementation in GitHub Repo ...
}
</code></pre>
<p><strong>优点:</strong><br />
*   写入性能极高。<br />
*   降低主数据源压力。</p>
<p><strong>缺点:</strong><br />
*   <strong>数据丢失风险。</strong><br />
*   <strong>最终一致性。</strong><br />
*   实现复杂度高。</p>
<p><strong>使用场景:</strong> 对写性能要求极高，写操作非常频繁，能容忍数据丢失风险和最终一致性。</p>
<h3>5. Write-Around (绕写缓存)</h3>
<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-6.png" alt="" /></p>
<p><strong>核心思想：写操作直接绕过缓存，只写入主数据源。读操作时才将数据写入缓存（通常结合 Cache-Aside）。</strong></p>
<p><strong>工作流程：</strong></p>
<ol>
<li><strong>写路径:</strong> 应用发起写请求，直接将数据写入主数据源。</li>
<li><strong>读路径 (通常是Cache-Aside):</strong> 应用需要读取数据时，先检查缓存。如果未命中，则从主数据源读取，然后将数据存入缓存，最后返回。</li>
</ol>
<p><strong>Go 示例 (核心实现 &#8211; strategy/writearound/writearound.go):</strong></p>
<pre><code class="go">package writearound

import (
    "context"
    "fmt"
    "log"
    "time"

    "cachestrategysdemo/internal/cache"
    "cachestrategysdemo/internal/database"
    "cachestrategysdemo/internal/models"
)

const logCacheKeyPrefix = "log:" // Example prefix for logs

// WriteLog writes log entry directly to DB, bypassing cache.
func WriteLog(ctx context.Context, entry *models.LogEntry, db database.Database) error {
    // 1. Write directly to DB
    log.Printf("[Write-Around Write] Writing log directly to DB (ID: %s)\n", entry.ID)
    err := db.InsertLogEntry(ctx, entry) // Use the appropriate DB method
    if err != nil {
        return fmt.Errorf("failed to write log to DB: %w", err)
    }
    return nil
}

// GetLog retrieves log entry, using Cache-Aside for reading.
func GetLog(ctx context.Context, logID string, db database.Database, memCache cache.Cache, ttl time.Duration) (*models.LogEntry, error) {
    cacheKey := logCacheKeyPrefix + logID

    // 1. Check cache (Cache-Aside read path)
    if cachedVal, found := memCache.Get(cacheKey); found {
        if entry, ok := cachedVal.(*models.LogEntry); ok {
            log.Println("[Write-Around Read] Cache Hit for log:", logID)
            return entry, nil
        }
        memCache.Delete(cacheKey)
    }

    // 2. Cache Miss
    log.Println("[Write-Around Read] Cache Miss for log:", logID)

    // 3. Fetch from Database
    entry, err := db.GetLogByID(ctx, logID) // Use the appropriate DB method
    if err != nil { return nil, fmt.Errorf("failed to get log from DB: %w", err) }
    if entry == nil { return nil, nil /* Not found */ }

    // 4. Store data into cache
    memCache.Set(cacheKey, entry, ttl)
    log.Println("[Write-Around Read] Log stored in cache:", logID)

    // 5. Return data
    return entry, nil
}
</code></pre>
<p><strong>优点:</strong><br />
*   避免缓存污染。<br />
*   写性能好。</p>
<p><strong>缺点:</strong><br />
*   首次读取延迟高。<br />
*   可能存在数据不一致（读路径上的 Cache-Aside 固有）。</p>
<p><strong>使用场景:</strong> 写密集型，且写入的数据不太可能在短期内被频繁读取的场景。</p>
<h2>总结与选型</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/five-cache-strategies-7.png" alt="" /></p>
<p><strong>没有银弹！</strong> 选择哪种缓存策略，最终取决于你的具体业务场景对<strong>性能、数据一致性、可靠性和实现复杂度</strong>的权衡。</p>
<p><strong>本文涉及的完整可运行示例代码已托管至GitHub，你可以通过<a href="https://github.com/bigwhite/experiments/tree/master/go-cache-strategy">这个链接</a>访问。</strong></p>
<p>希望这篇详解能帮助你在 Go 项目中更自信地选择和使用缓存策略。<strong>你最常用哪种缓存策略？在 Go 中实现时遇到过哪些坑？欢迎在评论区分享交流！</strong></p>
<h2>>注：本文代码由AI生成，可编译运行，但仅用于演示和辅助文章理解，切勿用于生产！</h2>
<p><strong>原「Gopher部落」已重装升级为「Go &amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！</strong></p>
<p>我们致力于打造一个高品质的 <strong>Go 语言深度学习</strong> 与 <strong>AI 应用探索</strong> 平台。在这里，你将获得：</p>
<ul>
<li><strong>体系化 Go 核心进阶内容:</strong> 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。</li>
<li><strong>前沿 Go+AI 实战赋能:</strong> 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。</li>
<li><strong>星主 Tony Bai 亲自答疑:</strong> 遇到难题？星主第一时间为你深度解析，扫清学习障碍。</li>
<li><strong>高活跃 Gopher 交流圈:</strong> 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。</li>
<li><strong>独家资源与内容首发:</strong> 技术文章、课程更新、精选资源，第一时间触达。</li>
</ul>
<p>衷心希望「Go &amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" 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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/28/five-cache-strategies/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解SQL查询语句的执行顺序</title>
		<link>https://tonybai.com/2024/07/20/sql-query-execution-order/</link>
		<comments>https://tonybai.com/2024/07/20/sql-query-execution-order/#comments</comments>
		<pubDate>Fri, 19 Jul 2024 21:59:47 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[from]]></category>
		<category><![CDATA[GroupBy]]></category>
		<category><![CDATA[Having]]></category>
		<category><![CDATA[join]]></category>
		<category><![CDATA[limit]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[OrderBy]]></category>
		<category><![CDATA[query]]></category>
		<category><![CDATA[Schema]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[where]]></category>
		<category><![CDATA[关系代数]]></category>
		<category><![CDATA[关系数据库]]></category>
		<category><![CDATA[投影]]></category>
		<category><![CDATA[数据库]]></category>
		<category><![CDATA[表连接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4226</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/07/20/sql-query-execution-order SQL查询语句是关系数据库操作的核心。SQL查询语句有简有繁，简单的SQL查询语句，比如： SELECT column1, column2 FROM table_name WHERE condition; 对于这种查询语句，即便初学者也十分容易理解和掌握。但复杂的SQL查询语句，比如： SELECT department, AVG(salary) AS avg_salary FROM employee_table WHERE department IN ('IT', 'HR', 'Finance') GROUP BY department HAVING AVG(salary) &#62; (SELECT AVG(salary) FROM employee_table) ORDER BY avg_salary DESC LIMIT 3; 这种包含了SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY和LIMIT等多个子句的复杂查询语句，即便是有多年开发经验的开发人员，如果不清楚各个子句的执行顺序，也很容易写错，并导致非预期的查询结果。 关于SQL查询语句的执行顺序，互联网上有很多类似下面这样的速查表式(cheetsheet)的图： 这些图对理解SQL查询语句的执行顺序很有帮助。但对于初学者来说，如果再有一个配套的实例就更完美了。在这篇文章中，我就来为说明SQL查询语句的执行顺序补充一个实例，期望能帮助大家更好地学习和理解SQL查询语句的执行顺序。 1. 实例的Schema和初始数据 本文将使用两个表：departments 表和employees表来演示查询操作。以下是这两个表的创建和初始数据插入语句： 注：本文试验环境使用的是MySQL数据库，关于MySQL数据库的安装和运行方法，可以参考我之前的一篇文章《通过实例理解Go访问和操作数据库的几种方式》。 DROP DATABASE IF EXISTS [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/sql-query-execution-order-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/07/20/sql-query-execution-order">本文永久链接</a> &#8211; https://tonybai.com/2024/07/20/sql-query-execution-order</p>
<p>SQL查询语句是关系数据库操作的核心。SQL查询语句有简有繁，简单的SQL查询语句，比如：</p>
<pre><code>SELECT column1, column2
FROM table_name
WHERE condition;
</code></pre>
<p>对于这种查询语句，即便初学者也十分容易理解和掌握。但复杂的SQL查询语句，比如：</p>
<pre><code>SELECT department, AVG(salary) AS avg_salary
FROM employee_table
WHERE department IN ('IT', 'HR', 'Finance')
GROUP BY department
HAVING AVG(salary) &gt; (SELECT AVG(salary) FROM employee_table)
ORDER BY avg_salary DESC
LIMIT 3;
</code></pre>
<p>这种包含了SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY和LIMIT等多个子句的复杂查询语句，即便是有多年开发经验的开发人员，<strong>如果不清楚各个子句的执行顺序</strong>，也很容易写错，并导致非预期的查询结果。</p>
<p>关于SQL查询语句的执行顺序，互联网上有很多类似下面这样的速查表式(cheetsheet)的图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/sql-query-execution-order-2.png" alt="" /></p>
<p>这些图对理解SQL查询语句的执行顺序很有帮助。但对于初学者来说，如果再有一个配套的实例就更完美了。在这篇文章中，我就来为说明SQL查询语句的执行顺序补充一个实例，期望能帮助大家更好地学习和理解SQL查询语句的执行顺序。</p>
<h2>1. 实例的Schema和初始数据</h2>
<p>本文将使用两个表：departments 表和employees表来演示查询操作。以下是这两个表的创建和初始数据插入语句：</p>
<blockquote>
<p>注：本文试验环境使用的是MySQL数据库，关于MySQL数据库的安装和运行方法，可以参考我之前的一篇文章《<a href="https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go">通过实例理解Go访问和操作数据库的几种方式</a>》。</p>
</blockquote>
<pre><code>DROP DATABASE IF EXISTS example_db;
CREATE DATABASE example_db;
use example_db;

CREATE TABLE departments (
    dept_id INT PRIMARY KEY,
    dept_name VARCHAR(255)
);

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    name VARCHAR(255),
    salary DECIMAL(10, 2),
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
</code></pre>
<p>我们事先在表中预置一些初始数据：</p>
<pre><code>INSERT INTO departments (dept_id, dept_name) VALUES
(1, 'HR'),
(2, 'Engineering'),
(3, 'Marketing');

INSERT INTO employees (emp_id, name, salary, dept_id) VALUES
(1, 'Alice', 60000, 1),
(2, 'Bob', 50000, 1),
(3, 'Carol', 70000, 2),
(4, 'Dave', 55000, 2),
(5, 'Eve', 40000, 3),
(6, 'Frank', 80000, 2),
(7, 'Grace', 45000, 3),
(8, 'Heidi', 75000, 2),
(9, 'Ivan', 48000, 1),
(10, 'Judy', 51000, 3);
</code></pre>
<p>到这里，试验环境和数据就就绪了！</p>
<h2>2. SQL查询语句</h2>
<p>接下来，我们来编写一个复杂一些的查询语句，作为本文要分析的目标：</p>
<pre><code>SELECT d.dept_name, AVG(e.salary) AS avg_salary
FROM employees as e
JOIN departments as d ON e.dept_id = d.dept_id
WHERE e.salary &gt; 10000
GROUP BY d.dept_name
HAVING AVG(e.salary) &gt; 50000
ORDER BY avg_salary DESC
LIMIT 3;
</code></pre>
<p>这条SQL查询语句的功能大致是从employees和departments两个表中查询每个部门(dept)的平均工资。那么这条语句究竟是怎么做到这一点的呢？我们通过下面对SQL语句执行顺序的<strong>step by step</strong>分析来一看究竟。</p>
<h2>3. SQL查询语句执行顺序</h2>
<p>在编写SQL查询语句时，理解其执行顺序是至关重要的。因为，<strong>SQL语句中各个子句的执行顺序与它们在语句中的出现顺序并不一致</strong>，比如像本文前面那张图展示的那样，查询语句中最先出现的select子句这样的<a href="https://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples/">投影操作</a>其实是在后面执行的。</p>
<p>通常情况下，就像上图中所示，SQL查询语句的执行顺序如下：</p>
<pre><code>FROM和JOIN -&gt; WHERE -&gt; GROUP BY -&gt; HAVING -&gt; SELECT -&gt; ORDER BY -&gt; LIMIT
</code></pre>
<p>下面我们就基于上述实例，对执行顺序中的每个子句进行分析。首先来看一下FROM/JOIN。</p>
<h3>3.1 FROM和JOIN</h3>
<p>SQL查询语句中的FROM和JOIN子句是最先执行的：</p>
<pre><code>FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
</code></pre>
<p>它们为后续的其他子句提供了操作的对象数据集合。Join会先根据指定的连接条件(通常是等值条件，比如这里的e.dept_id = d.dept_id)来连接两个表，只有满足连接条件的行才会被保留在结果集中。From将从这个联结后的大表中查询满足条件的数据。</p>
<p>执行后的中间结果如下：</p>
<pre><code>+--------+-------+----------+---------+-------------+
| emp_id | name  | salary   | dept_id | dept_name   |
+--------+-------+----------+---------+-------------+
|      1 | Alice | 60000.00 |       1 | HR          |
|      2 | Bob   | 50000.00 |       1 | HR          |
|      9 | Ivan  | 48000.00 |       1 | HR          |
|      3 | Carol | 70000.00 |       2 | Engineering |
|      4 | Dave  | 55000.00 |       2 | Engineering |
|      6 | Frank | 80000.00 |       2 | Engineering |
|      8 | Heidi | 75000.00 |       2 | Engineering |
|      5 | Eve   | 40000.00 |       3 | Marketing   |
|      7 | Grace | 45000.00 |       3 | Marketing   |
|     10 | Judy  | 51000.00 |       3 | Marketing   |
+--------+-------+----------+---------+-------------+
</code></pre>
<h3>3.2 WHERE</h3>
<p>接下来来到了WHERE。</p>
<p>WHERE子句的作用是对FROM和JOIN提供的数据集合进行筛选，只保留满足某些条件的记录(行)，它相当于对上面JOIN表后的中间结果的数据集合施加了一个过滤器，只有满足过滤条件（这里是salary > 10000）的记录才会进入下一个中间结果的数据集合中。这样也可以减少后续子句操作的数据量，提高查询效率。</p>
<p>具体到这个示例上，WHERE子句如下：</p>
<pre><code>WHERE e.salary &gt; 10000
</code></pre>
<p>由于上面中间结果中每位雇员的工资(salary)都大于10000，因此这一步过滤之后，实际得到的中间结果与上面的表格中的数据是一样的。</p>
<h3>3.3 GROUP BY</h3>
<p>接下来执行的是GROUP BY。</p>
<p>这里GROUP BY子句的作用是将上述查询的中间结果集按照指定的列(dept_name)进行分组。使用GROUP BY进行分组的前提是SELECT投影的列必须是可分组的列，比如这里的dept_name和dept_id。如果SELECT投影的列是不可分组的列，比如这里的emp_id、name等，查询语句就会报错！</p>
<p>在我们的实例中，使用的是dept_name对上述查询的中间结果集进行分组和聚合运算的：</p>
<pre><code>GROUP BY d.dept_name
</code></pre>
<p>该子句会根据部门名称(dept_name)进行分组，计算每个组的平均工资(AVG(e.salary))的计算也是在这时执行的，以下是执行后的中间结果：</p>
<pre><code>+---------+-------------+--------------+
| dept_id | dept_name   | avg_salary   |
+---------+-------------+--------------+
|       1 | HR          | 52666.666667 |
|       2 | Engineering | 70000.000000 |
|       3 | Marketing   | 45333.333333 |
+---------+-------------+--------------+
</code></pre>
<blockquote>
<p>注：这里包含了可分组的字段dept_id。关于这个字段是否真实包含在中间结果中可能与各个数据库的实现有关。</p>
</blockquote>
<h3>3.4 HAVING</h3>
<p>HAVING子句在数据分组之后起作用，用于过滤分组后的结果。这个与执行选择关系操作Where过滤在作用时机上有所不同，WHERE子句在数据被分组之前起作用，用于过滤原始数据。</p>
<p>本例应用的HAVING条件如下：</p>
<pre><code>HAVING AVG(e.salary) &gt; 50000
</code></pre>
<p>即过滤出平均工资超过50000的组。下面是HAVING子句作用后的中间结果：</p>
<pre><code>+---------+-------------+--------------+
| dept_id | dept_name   | avg_salary   |
+---------+-------------+--------------+
|       1 | HR          | 52666.666667 |
|       2 | Engineering | 70000.000000 |
+---------+-------------+--------------+
</code></pre>
<h3>3.5 SELECT</h3>
<p>SELECT是我们最熟悉的关系代数操作了，也叫投影，用于选择所需的列。</p>
<p>在本实例中，我们选择了dept_name和avg_salary：</p>
<pre><code>SELECT d.dept_name, AVG(e.salary) AS avg_salary
</code></pre>
<p>该子句作用后的中间结果如下：</p>
<pre><code>+-------------+--------------+
| dept_name   | avg_salary   |
+-------------+--------------+
| HR          | 52666.666667 |
| Engineering | 70000.000000 |
+-------------+--------------+
</code></pre>
<h3>3.6 ORDER BY</h3>
<p>最后执行的是排序子句，对中间结果按特定字段的升序或降序进行排列，这里我们按平均工资降序排列：</p>
<pre><code>ORDER BY avg_salary DESC
</code></pre>
<p>得到的中间结果如下：</p>
<pre><code>+-------------+--------------+
| dept_name   | avg_salary   |
+-------------+--------------+
| Engineering | 70000.000000 |
| HR          | 52666.666667 |
+-------------+--------------+
</code></pre>
<h3>3.7 LIMIT</h3>
<p>最后，LIMIT子句用于限制结果集的记录数量，这里限制输出3个：</p>
<pre><code>LIMIT 3
</code></pre>
<p>由于上面的中间结果已经仅剩2条记录，因此上面的中间结果就是最终结果：</p>
<pre><code>+-------------+--------------+
| dept_name   | avg_salary   |
+-------------+--------------+
| Engineering | 70000.000000 |
| HR          | 52666.666667 |
+-------------+--------------+
</code></pre>
<h2>4. 小结</h2>
<p>在这篇文章中，我们通过实例，从FROM/JOIN开始，逐步分析了WHERE、GROUP BY、HAVING、SELECT、ORDER BY和LIMIT子句的执行顺序，并提供了中间结果的输出。这个实例的分步讲解可以让大家清晰地理解SQL查询语句的执行顺序，有助于大家更好地理解复杂的SQL查询语句，为编写复杂且高效的SQL查询语句打下坚实的基础。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/07/20/sql-query-execution-order/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解Go访问和操作数据库的几种方式</title>
		<link>https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go/</link>
		<comments>https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go/#comments</comments>
		<pubDate>Sun, 14 Jul 2024 22:09:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Conn]]></category>
		<category><![CDATA[CRUD]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[DatabaseSystemConcepts]]></category>
		<category><![CDATA[db]]></category>
		<category><![CDATA[dbml]]></category>
		<category><![CDATA[delete]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ent]]></category>
		<category><![CDATA[entgo]]></category>
		<category><![CDATA[FaceBook]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gorm]]></category>
		<category><![CDATA[insert]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[Schema]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlc]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[Stmt]]></category>
		<category><![CDATA[update]]></category>
		<category><![CDATA[代码生成]]></category>
		<category><![CDATA[对象关系映射]]></category>
		<category><![CDATA[微服务]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4220</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go 关系数据库操作是Go应用开发中的重要一环，尤其是Go Web应用、微服务等。作为Gopher，我们需要了解几种主流的数据库访问和操作方法，以便在项目中做出适当的选择。 我个人在日常开发中较少涉及CRUD类应用，因此使用Go访问和操作数据库的机会并不多，在这方面也算是有一些“短板”。通过在这篇文章中对数据库访问方式进行全面的梳理，我也算是补全一下技能树，同时也能为读者小伙伴提供一些参考。 我搜集了目前Go社区的主流数据库访问和操作方式，大致有如下几种： 使用Go标准库database/sql+特定数据库的driver，外加像sqlx这种无缝兼容的功能增强包 使用对象关系映射ORM，如GORM等 使用代码生成+ ORM方式，如sqlc、Fackbook开源的Ent等。 在这篇文章中，我会建立一个简单的关系数据库实例，并用一个简单的学校院系选课关系模型作为示例，分别用上述几种方法实现数据库访问以及CRUD操作，并对比各种方式的操作性能。通过对比，你可以了解每种方法的特点。希望这些例子能帮助各位读者在实际项目中更好地处理数据库操作。 1. 建立示例数据库和数据库模式(schema) 为了便于后续代码示例的讲解和实现，我们先来建立示例数据库并定义数据库模式。 1.1 基于容器启动MySQL数据库服务 在本文中，我们选择关系数据库界的主流代表MySQL数据库。基于容器，我们可以很方便地启动MySQL数据库服务： $docker pull mysql:latest $docker run -d --name mysql-db -v /path/to/host/mysqldata:/var/lib/mysql -p 4407:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:latest 由于做了volume挂载，MySQL容器内部的数据文件将会存储在主机的/path/to/host/mysqldata目录下，即使容器被删除或重新创建，数据文件也不会丢失。你可以根据实际情况替换/path/to/host/mysqldata为你想要存储MySQL数据的主机目录路径。 如果容器启动成功，我们可以通过容器内的mysql client工具连接到MySQL数据库中： $docker exec -it mysql-db mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-the-ways-to-access-databases-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go">本文永久链接</a> &#8211; https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go</p>
<p>关系数据库操作是Go应用开发中的重要一环，尤其是Go Web应用、微服务等。作为Gopher，我们需要了解几种主流的数据库访问和操作方法，以便在项目中做出适当的选择。</p>
<p>我个人在日常开发中较少涉及CRUD类应用，因此使用Go访问和操作数据库的机会并不多，在这方面也算是有一些“短板”。通过在这篇文章中对数据库访问方式进行全面的梳理，我也算是补全一下技能树，同时也能为读者小伙伴提供一些参考。</p>
<p>我搜集了目前Go社区的主流数据库访问和操作方式，大致有如下几种：</p>
<ul>
<li>使用Go标准库database/sql+特定数据库的driver，外加像<a href="https://github.com/jmoiron/sqlx">sqlx</a>这种无缝兼容的功能增强包</li>
<li>使用对象关系映射ORM，如<a href="https://github.com/go-gorm/gorm">GORM</a>等</li>
<li>使用代码生成+ ORM方式，如<a href="https://github.com/sqlc-dev/sqlc">sqlc</a>、Fackbook开源的<a href="https://github.com/ent/ent">Ent</a>等。</li>
</ul>
<p>在这篇文章中，我会建立一个简单的关系数据库实例，并用一个简单的学校院系选课关系模型作为示例，分别用上述几种方法实现数据库访问以及CRUD操作，并对比各种方式的操作性能。通过对比，你可以了解每种方法的特点。希望这些例子能帮助各位读者在实际项目中更好地处理数据库操作。</p>
<h2>1. 建立示例数据库和数据库模式(schema)</h2>
<p>为了便于后续代码示例的讲解和实现，我们先来建立示例数据库并定义数据库模式。</p>
<h3>1.1 基于容器启动MySQL数据库服务</h3>
<p>在本文中，我们选择关系数据库界的主流代表<a href="https://www.mysql.com/">MySQL数据库</a>。基于容器，我们可以很方便地启动MySQL数据库服务：</p>
<pre><code>$docker pull mysql:latest
$docker run -d --name mysql-db -v /path/to/host/mysqldata:/var/lib/mysql -p 4407:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:latest
</code></pre>
<p>由于做了volume挂载，MySQL容器内部的数据文件将会存储在主机的/path/to/host/mysqldata目录下，即使容器被删除或重新创建，数据文件也不会丢失。你可以根据实际情况替换/path/to/host/mysqldata为你想要存储MySQL数据的主机目录路径。</p>
<p>如果容器启动成功，我们可以通过容器内的mysql client工具连接到MySQL数据库中：</p>
<pre><code>$docker exec -it mysql-db mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.2.0 MySQL Community Server - GPL

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

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

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

mysql&gt;
</code></pre>
<p>我们在MySQL中创建example_db数据库供后面的数据库建表和数据操作使用：</p>
<pre><code>mysql&gt; CREATE DATABASE example_db;
Query OK, 1 row affected (0.01 sec)

mysql&gt; SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| example_db         |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)
</code></pre>
<h3>1.2 建立数据库模式</h3>
<p>接下来，我将借用并简化<a href="https://book.douban.com/subject/35501216/">《Database System Concepts,7th》</a>一书中提供的示例数据库的Schema，创建本文后续代码示例使用的数据库表。简化后的Schema将涵盖常见的CRUD操作需求，同时保证数据库结构清晰易懂。下面是简化后的数据模式对应的E-R图(基于在线https://dbdiagram.io/工具生成，dbml源文件在database-access/schema.dbml)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-the-ways-to-access-databases-in-go-2.png" alt="" /></p>
<p>这个Schema包括department(院系表)、instructor(教师表)、course(课程信息表)、student(学生信息表)和enrollment(学生选课信息)。下面是建表语句：</p>
<pre><code>// database-access/schema.sql
DROP DATABASE IF EXISTS example_db;
CREATE DATABASE example_db;

CREATE TABLE department (
    dept_id INT AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    PRIMARY KEY (dept_id)
);

CREATE TABLE instructor (
    instr_id INT AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    dept_id INT,
    PRIMARY KEY (instr_id),
    FOREIGN KEY (dept_id) REFERENCES department(dept_id)
);

CREATE TABLE course (
    course_id INT AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL,
    dept_id INT,
    PRIMARY KEY (course_id),
    FOREIGN KEY (dept_id) REFERENCES department(dept_id)
);

CREATE TABLE student (
    student_id INT AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    dept_id INT,
    PRIMARY KEY (student_id),
    FOREIGN KEY (dept_id) REFERENCES department(dept_id)
);

CREATE TABLE enrollment (
    student_id INT,
    course_id INT,
    semester VARCHAR(6),
    year INT,
    PRIMARY KEY (student_id, course_id, semester, year),
    FOREIGN KEY (student_id) REFERENCES student(student_id),
    FOREIGN KEY (course_id) REFERENCES course(course_id)
);
</code></pre>
<p>通过mysql client工具执行上述语句后，我们就完成了表的创建：</p>
<pre><code>mysql&gt; show tables;
+----------------------+
| Tables_in_example_db |
+----------------------+
| course               |
| department           |
| enrollment           |
| instructor           |
| student              |
+----------------------+
5 rows in set (0.00 sec)
</code></pre>
<p>不过在开始使用Go语言来访问并操作这些数据表之前，我们先定义一些基本的数据库表操作的示例，后续每种Go访问和操作数据库的方式都会基于这些示例并实现这些示例中的操作。</p>
<h2>2. 定义数据库表操作示例</h2>
<h3>2.1 插入数据（Create）</h3>
<p>向department表中插入数据：</p>
<pre><code>INSERT INTO department (name) VALUES ('Computer Science');
INSERT INTO department (name) VALUES ('Mathematics');
</code></pre>
<p>向instructor表中插入数据：</p>
<pre><code>INSERT INTO instructor (name, dept_id) VALUES ('John Doe', 1);
INSERT INTO instructor (name, dept_id) VALUES ('Jane Smith', 2);
</code></pre>
<p>向course表中插入数据：</p>
<pre><code>INSERT INTO course (title, dept_id) VALUES ('Database Systems', 1);
INSERT INTO course (title, dept_id) VALUES ('Calculus', 2);
</code></pre>
<p>向student表中插入数据：</p>
<pre><code>INSERT INTO student (name, dept_id) VALUES ('Alice', 1);
INSERT INTO student (name, dept_id) VALUES ('Bob', 2);
</code></pre>
<p>向enrollment表中插入数据：</p>
<pre><code>INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (1, 1, 'Fall', 2024);
INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (2, 2, 'Fall', 2024);
</code></pre>
<h3>2.2 查询数据（Retrieve）</h3>
<p>查询所有学生的信息：</p>
<pre><code>SELECT * FROM student;
</code></pre>
<p>查询某个院系的课程信息：</p>
<pre><code>SELECT * FROM course WHERE dept_id = 1;
</code></pre>
<p>查询某个学生的选课信息：</p>
<pre><code>SELECT * FROM enrollment WHERE student_id = 1;
</code></pre>
<h3>2.3 更新数据（Update）</h3>
<p>更新某个学生的姓名：</p>
<pre><code>UPDATE student SET name = 'Alice Johnson' WHERE student_id = 1;
</code></pre>
<p>更新某个课程的标题：</p>
<pre><code>UPDATE course SET title = 'Advanced Database Systems' WHERE course_id = 1;
</code></pre>
<h3>2.4 删除数据（Delete）</h3>
<p>删除某个学生的选课记录：</p>
<pre><code>DELETE FROM enrollment WHERE student_id = 1 AND course_id = 1 AND semester = 'Fall' AND year = 2024;
</code></pre>
<p>删除某个课程：</p>
<pre><code>DELETE FROM course WHERE course_id = 1;
</code></pre>
<p>通过上述定义的这些示例操作，我们可以对数据库进行基本的增删改查操作。接下来，我们就来正式介绍Go访问和操作数据库的几种主流方式，并分别用这些方式来实现上述示例的CRUD操作。</p>
<p>我们先来看一下基于Go标准库的数据库访问和操作方式。</p>
<h2>3. 采用Go标准库的数据库访问方式</h2>
<p>Go标准库中提供了一个database/sql包，它定义了一些接口和方法，用于访问关系数据库。这个包提供了一个抽象层，可以与各种不同的关系数据库驱动程序进行交互。比如database/sql包定义了一些接口，如DB、Conn、Stmt等，用于表示数据库连接、语句执行等操作。数据库驱动包需要实现这些接口，并提供了具体的数据库交互逻辑。</p>
<p>Go应用使用database/sql包时，应用首先需要导入数据库驱动程序，然后使用sql.Open函数连接到数据库。这个函数返回一个*sql.DB对象，代表数据库连接。之后，Go应用便可以使用DB对象执行各种SQL操作,如DB.Query、DB.Exec等。这些函数会调用驱动程序中实现的具体方法来与数据库交互。 对于对于复杂的数据库查询操作，Go应用还可以使用DB对象创建&#42;sql.Stmt对象，后者表示预编译好的SQL语句，这样可以提高操作性能。</p>
<p>总的来说，database/sql包提供了一个标准化的接口，让应用程序可以方便地访问不同的关系数据库，而不需要关心底层的实现细节。这使得Go程序可以跨数据库平台运行。</p>
<p>下面我们就基于<a href="github.com/go-sql-driver/mysql">go-sql-driver/mysql</a>提供的MySQL驱动来实现对MySQL中示例表的各种操作。</p>
<h3>3.1 初始化数据库连接</h3>
<p>我们首先需要在代码中初始化数据库连接。以下是初始化代码示例：</p>
<pre><code>// database-access/stdlib/main.go
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql" // 注册mysql driver
    "log"
)

func main() {
    dsn := "root:123456@tcp(127.0.0.1:4407)/example_db"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 测试数据库连接
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to the database successfully!")
}
</code></pre>
<p>拿到数据库实例(&#42;sql.DB对象)后，我们便可以基于该实例对数据库表进行各种操作了！接下来，我们逐一看一下。</p>
<h3>3.2 插入数据（Create）</h3>
<p>以下是通过Go标准库database/sql包方式插入数据的代码示例：</p>
<pre><code>func insertData(db *sql.DB) {
    // 插入department数据
    _, err := db.Exec("INSERT INTO department (name) VALUES ('Computer Science'), ('Mathematics')")
    if err != nil {
        log.Fatal(err)
    }

    // 插入instructor数据
    _, err = db.Exec("INSERT INTO instructor (name, dept_id) VALUES ('John Doe', 1), ('Jane Smith', 2)")
    if err != nil {
        log.Fatal(err)
    }

    // 插入course数据
    _, err = db.Exec("INSERT INTO course (title, dept_id) VALUES ('Database Systems', 1), ('Calculus', 2)")
    if err != nil {
        log.Fatal(err)
    }

    // 插入student数据
    _, err = db.Exec("INSERT INTO student (name, dept_id) VALUES ('Alice', 1), ('Bob', 2)")
    if err != nil {
        log.Fatal(err)
    }

    // 插入enrollment数据
    _, err = db.Exec("INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (1, 1, 'Fall', 2024), (2, 2, 'Fall', 2024)")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data inserted successfully!")
}
</code></pre>
<h3>3.3 查询数据（Retrieve）</h3>
<p>以下是查询数据的代码示例：</p>
<pre><code>func queryData(db *sql.DB) {
    // 查询所有学生的信息
    rows, err := db.Query("SELECT * FROM student")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var studentID int
        var name string
        var deptID int
        err := rows.Scan(&amp;studentID, &amp;name, &amp;deptID)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Student ID: %d, Name: %s, Department ID: %d\n", studentID, name, deptID)
    }

    // 查询某个院系的课程信息
    rows, err = db.Query("SELECT * FROM course WHERE dept_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var courseID int
        var title string
        var deptID int
        err := rows.Scan(&amp;courseID, &amp;title, &amp;deptID)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Course ID: %d, Title: %s, Department ID: %d\n", courseID, title, deptID)
    }

    // 查询某个学生的选课信息
    rows, err = db.Query("SELECT * FROM enrollment WHERE student_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var studentID int
        var courseID int
        var semester string
        var year int
        err := rows.Scan(&amp;studentID, &amp;courseID, &amp;semester, &amp;year)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", studentID, courseID, semester, year)
    }
}
</code></pre>
<h3>3.4 更新数据（Update）</h3>
<p>以下是更新数据的代码示例：</p>
<pre><code>func updateData(db *sql.DB) {
    // 更新某个学生的姓名
    _, err := db.Exec("UPDATE student SET name = 'Alice Johnson' WHERE student_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }

    // 更新某个课程的标题
    _, err = db.Exec("UPDATE course SET title = 'Advanced Database Systems' WHERE course_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data updated successfully!")
}
</code></pre>
<h3>3.5 删除数据（Delete）</h3>
<p>以下是删除数据的代码示例：</p>
<pre><code>func deleteData(db *sql.DB) {
    // 删除某个学生的选课记录
    _, err := db.Exec("DELETE FROM enrollment WHERE student_id = ? AND course_id = ? AND semester = ? AND year = ?", 1, 1, "Fall", 2024)
    if err != nil {
        log.Fatal(err)
    }

    // 删除某个课程
    _, err = db.Exec("DELETE FROM course WHERE course_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data deleted successfully!")
}
</code></pre>
<blockquote>
<p>注：上述示例的完整代码可以参见database-access/stdlib/main.go。</p>
</blockquote>
<p>通过上述代码示例，我们展示了如何使用Go标准库和MySQL驱动程序来进行数据库连接和基本的CRUD操作。我们看到直接使用Go标准库的database/sql包来访问和操作数据库确实是比较基础和原始的方式，基本上是手动拼接SQL语句和处理结果，这种方式确实比较低级和繁琐。</p>
<p>sqlx包在一定程度上提升了Go标准库访问数据库的体验，并完全兼容database/sql包的接口，接下来，我们就来看看如何使用database/sql的扩展库sqlx来访问和操作数据库。</p>
<h3>3.6 使用sqlx扩展库访问MySQL数据库</h3>
<p><a href="https://github.com/jmoiron/sqlx">sqlx</a>是一个扩展库，它在Go的标准database/sql库之上提供了一系列扩展。sqlx版本的sql.DB、sql.TX、sql.Stmt等所有接口都保留了底层接口不变，这意味着它们的接口是标准库接口的超集，这使得我们可以无缝地将现有使用database/sql的代码集成到sqlx中。sqlx的主要扩展功能包括：</p>
<ul>
<li>将查询结果中的行数据直接解析到结构体(支持嵌入式结构体)、map和切片中，无需手工解析；</li>
<li>支持命名参数查询（Named queries），包括预编译语句(prepared statement)；</li>
<li>提供一些常用的辅助函数，如Get、Select方法可以快速从查询结果转换为结构体/切片。</li>
</ul>
<p>sqlx在保持database/sql接口不变的情况下，提供了许多额外的功能和便利性，使得在Go中访问关系型数据库变得更加简单高效。下面是使用sqlx实现的上面示例操作的完整代码：</p>
<pre><code>// database-access/sqlx/main.go

package main

import (
    "fmt"
    "log"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jmoiron/sqlx"
)

func main() {
    dsn := "root:123456@tcp(127.0.0.1:4407)/example_db"
    db, err := sqlx.Connect("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    fmt.Println("Connected to the database successfully!")

    insertData(db)
    queryData(db)
    updateData(db)
    queryData(db) // 查看更新后的数据
    deleteData(db)
    queryData(db) // 查看删除后的数据
}

func insertData(db *sqlx.DB) {
    // 插入department数据
    _, err := db.NamedExec(`INSERT INTO department (name) VALUES (:name)`, []map[string]interface{}{
        {"name": "Computer Science"},
        {"name": "Mathematics"},
    })
    if err != nil {
        log.Fatal(err)
    }

    // 插入instructor数据
    _, err = db.NamedExec(`INSERT INTO instructor (name, dept_id) VALUES (:name, :dept_id)`, []map[string]interface{}{
        {"name": "John Doe", "dept_id": 1},
        {"name": "Jane Smith", "dept_id": 2},
    })
    if err != nil {
        log.Fatal(err)
    }

    // 插入course数据
    _, err = db.NamedExec(`INSERT INTO course (title, dept_id) VALUES (:title, :dept_id)`, []map[string]interface{}{
        {"title": "Database Systems", "dept_id": 1},
        {"title": "Calculus", "dept_id": 2},
    })
    if err != nil {
        log.Fatal(err)
    }

    // 插入student数据
    _, err = db.NamedExec(`INSERT INTO student (name, dept_id) VALUES (:name, :dept_id)`, []map[string]interface{}{
        {"name": "Alice", "dept_id": 1},
        {"name": "Bob", "dept_id": 2},
    })
    if err != nil {
        log.Fatal(err)
    }

    // 插入enrollment数据
    _, err = db.NamedExec(`INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (:student_id, :course_id, :semester, :year)`, []map[string]interface{}{
        {"student_id": 1, "course_id": 1, "semester": "Fall", "year": 2024},
        {"student_id": 2, "course_id": 2, "semester": "Fall", "year": 2024},
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data inserted successfully!")
}

type Student struct {
    StudentID int    `db:"student_id"`
    Name      string `db:"name"`
    DeptID    int    `db:"dept_id"`
}

type Course struct {
    CourseID int    `db:"course_id"`
    Title    string `db:"title"`
    DeptID   int    `db:"dept_id"`
}

type Enrollment struct {
    StudentID int    `db:"student_id"`
    CourseID  int    `db:"course_id"`
    Semester  string `db:"semester"`
    Year      int    `db:"year"`
}

func queryData(db *sqlx.DB) {
    // 查询所有学生的信息
    var students []Student
    err := db.Select(&amp;students, "SELECT * FROM student")
    if err != nil {
        log.Fatal(err)
    }
    for _, student := range students {
        fmt.Printf("Student ID: %d, Name: %s, Department ID: %d\n", student.StudentID, student.Name, student.DeptID)
    }

    // 查询某个院系的课程信息
    var courses []Course
    err = db.Select(&amp;courses, "SELECT * FROM course WHERE dept_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    for _, course := range courses {
        fmt.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.CourseID, course.Title, course.DeptID)
    }

    // 查询某个学生的选课信息
    var enrollments []Enrollment
    err = db.Select(&amp;enrollments, "SELECT * FROM enrollment WHERE student_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    for _, enrollment := range enrollments {
        fmt.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.StudentID, enrollment.CourseID, enrollment.Semester, enrollment.Year)
    }
}

func updateData(db *sqlx.DB) {
    // 更新某个学生的姓名
    _, err := db.NamedExec("UPDATE student SET name = :name WHERE student_id = :student_id", map[string]interface{}{
        "name":       "Alice Johnson",
        "student_id": 1,
    })
    if err != nil {
        log.Fatal(err)
    }

    // 更新某个课程的标题
    _, err = db.NamedExec("UPDATE course SET title = :title WHERE course_id = :course_id", map[string]interface{}{
        "title":     "Advanced Database Systems",
        "course_id": 1,
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data updated successfully!")
}

func deleteData(db *sqlx.DB) {
    // 删除某个学生的选课记录
    _, err := db.NamedExec("DELETE FROM enrollment WHERE student_id = :student_id AND course_id = :course_id AND semester = :semester AND year = :year", map[string]interface{}{
        "student_id": 1,
        "course_id":  1,
        "semester":   "Fall",
        "year":       2024,
    })
    if err != nil {
        log.Fatal(err)
    }

    // 删除某个课程
    _, err = db.NamedExec("DELETE FROM course WHERE course_id = :course_id", map[string]interface{}{
        "course_id": 1,
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data deleted successfully!")
}
</code></pre>
<p>我们看到：相较于直接使用database/sql，sqlx的named query/exec和直接将结果写入结构体/map/slices的确非常方便！ 代码也显得更加简洁、易读。</p>
<p>不过要说方便和易读，对象关系映射(ORM)方式说自己第二，没人敢说是第一。下面我们就来看看在Go中访问和操作数据库最常使用的方式：ORM方式。</p>
<h2>4. 使用ORM库访问数据库</h2>
<p>ORM（Object-Relational Mapping）是一种通过对象方式来操作数据库的方法，它将数据库中的表映射为程序中的对象，使开发者可以使用面向对象的方式操作数据库。使用ORM库可以简化数据库操作，提高开发效率，同时也能减少手写SQL带来的错误风险。</p>
<p>Go社区有几个很受欢迎的ORM库，比如gorm、<a href="https://gitea.com/xorm/xorm">xorm</a>等。接下来我将以最常用的Go ORM库GORM来说明一下如何使用ORM访问和操作数据库。</p>
<p>GORM是一个功能强大的Go ORM库，它提供了丰富的特性，如自动迁移(migrate)、关联、钩子、事务、复合主键等。GORM支持多种数据库，包括MySQL、PostgreSQL、SQLite等。</p>
<p>和采用原生database/sql的方式不同，使用ORM方式访问数据库，我们首先先要定义表对应的对象，即创建对象模型。</p>
<h3>4.1 创建对象模型</h3>
<p>下面的各个结构体类型对应的就是示例中各个表，gorm通过struct field tag来将结构体字段与表的列字段对应在一起：</p>
<pre><code>// database-access/gorm/main.go

type Department struct {
    ID   uint   `gorm:"primaryKey"`
    Name string `gorm:"size:100;not null"`
}

type Instructor struct {
    ID     uint   `gorm:"primaryKey"`
    Name   string `gorm:"size:100;not null"`
    DeptID uint
    Dept   Department `gorm:"foreignKey:DeptID"`
}

type Course struct {
    ID     uint   `gorm:"primaryKey"`
    Title  string `gorm:"size:100;not null"`
    DeptID uint
    Dept   Department `gorm:"foreignKey:DeptID"`
}

type Student struct {
    ID     uint   `gorm:"primaryKey"`
    Name   string `gorm:"size:100;not null"`
    DeptID uint
    Dept   Department `gorm:"foreignKey:DeptID"`
}

type Enrollment struct {
    ID        uint `gorm:"primaryKey"`
    StudentID uint
    CourseID  uint
    Semester  string  `gorm:"size:50;not null"`
    Year      int     `gorm:"not null"`
    Student   Student `gorm:"foreignKey:StudentID"`
    Course    Course  `gorm:"foreignKey:CourseID"`
    CreatedAt time.Time
    UpdatedAt time.Time
}
</code></pre>
<h3>4.2 CRUD操作示例</h3>
<p>下面就是基于上面定义的ORM模型进行CRUD操作的示例代码：</p>
<pre><code>// database-access/gorm/main.go
func main() {
    dsn := "root:123456@tcp(127.0.0.1:4407)/example_db?charset=utf8mb4&amp;parseTime=True&amp;loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &amp;gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            SingularTable: true,
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    // 自动迁移模式
    db.AutoMigrate(&amp;Department{}, &amp;Instructor{}, &amp;Course{}, &amp;Student{}, &amp;Enrollment{})

    // 执行CRUD操作
    createData(db)
    queryData(db)
    updateData(db)
    deleteData(db)
}

func createData(db *gorm.DB) {
    // 创建院系
    cs := Department{Name: "Computer Science"}
    math := Department{Name: "Mathematics"}
    db.Create(&amp;cs)
    db.Create(&amp;math)

    // 创建教师
    db.Create(&amp;Instructor{Name: "John Doe", DeptID: cs.ID})
    db.Create(&amp;Instructor{Name: "Jane Smith", DeptID: math.ID})

    // 创建课程
    db.Create(&amp;Course{Title: "Database Systems", DeptID: cs.ID})
    db.Create(&amp;Course{Title: "Calculus", DeptID: math.ID})

    // 创建学生
    db.Create(&amp;Student{Name: "Alice", DeptID: cs.ID})
    db.Create(&amp;Student{Name: "Bob", DeptID: math.ID})

    // 学生选课
    db.Create(&amp;Enrollment{StudentID: 1, CourseID: 1, Semester: "Fall", Year: 2024})
    db.Create(&amp;Enrollment{StudentID: 2, CourseID: 2, Semester: "Fall", Year: 2024})
}

func queryData(db *gorm.DB) {
    // 查询所有学生
    var students []Student
    db.Find(&amp;students)
    for _, student := range students {
        log.Printf("Student ID: %d, Name: %s, Department ID: %d\n", student.ID, student.Name, student.DeptID)
    }

    // 查询某个院系的课程
    var courses []Course
    db.Where("dept_id = ?", 1).Find(&amp;courses)
    for _, course := range courses {
        log.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.ID, course.Title, course.DeptID)
    }

    // 查询某个学生的选课信息
    var enrollments []Enrollment
    db.Where("student_id = ?", 1).Find(&amp;enrollments)
    for _, enrollment := range enrollments {
        log.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.StudentID, enrollment.CourseID, enrollment.Semester, enrollment.Year)
    }
}

func updateData(db *gorm.DB) {
    // 更新学生姓名
    db.Model(&amp;Student{}).Where("id = ?", 1).Update("name", "Alice Johnson")

    // 更新课程标题
    db.Model(&amp;Course{}).Where("id = ?", 1).Update("title", "Advanced Database Systems")
}

func deleteData(db *gorm.DB) {
    // 删除选课记录
    db.Where("course_id = ?", 1).Delete(&amp;Enrollment{})

    // 删除课程
    db.Where("id = ?", 1).Delete(&amp;Course{})
}
</code></pre>
<p>我们看到GORM提供了大量的便捷方法，可以大幅度简化SQL操作。例如，插入记录只需调用Create方法，而不需要手写SQL语句。GORM还提供了多种钩子函数（如BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate等），可以在特定操作前后执行自定义逻辑。这对于实现复杂业务逻辑非常有帮助。示例里没有使用钩子函数，大家可以自行试验。</p>
<p>日常使用数据库，查询操作占比最大，GORM的查询构造器允许开发人员使用链式方法调用来构造复杂的查询条件，例如，我们可以使用Where, Or, Order, Limit, Offset等方法来构建查询。</p>
<p>GORM还提供了AutoMigrate方法，可以根据模型结构自动创建或更新数据库表，这在开发环境中十分实用，减少了手动管理数据库结构的复杂性。</p>
<p>此外，GORM支持一对一、一对多和多对多等多种关联关系，并且可以通过简单的模型定义和方法调用来操作这些关系。就像例子中那样，我们可以通过定义foreignKey来自动管理外键约束。</p>
<p>总之，ORM方式的数据库访问和操作大幅降低了开发人员使用数据库的复杂性，提高了生产效率。不过由于引入了一层新的抽象，在表数据量较大的情况下，ORM方式的性能相对于原生SQL要低一些，这个我们在后面的对比各种方式的性能一节会再说。</p>
<p>Go开发人员在使用数据库时，往往希望能够在以下几个方面达到平衡：</p>
<ul>
<li>性能</li>
</ul>
<p>Go标准库的database/sql包提供了直接操作SQL语句的方式，可以发挥底层数据库引擎的性能优势。相比之下，ORM库在一定程度上会增加性能开销。</p>
<ul>
<li>开发体验</li>
</ul>
<p>ORM 库能够提供更高级的抽象和自动化，简化了许多数据库操作的样板代码，使得开发体验更加友好，生产力也相对较高。</p>
<ul>
<li>类型安全</li>
</ul>
<p>ORM库通常能够提供更好的类型安全性，减少手动拼接SQL语句时出错的风险。</p>
<p>简单来说，就是我们希望“既要..，也要&#8230;，还要&#8230;”，于是便有了以代码生成方式访问和操作数据库的代表sqlc。接下来我们就来看看如何sqlc是如何用代码生成方式来访问和操作数据库的。</p>
<h2>5. 使用代码生成方式访问数据库</h2>
<p>sqlc是一个强大的工具，它可以将针对数据库的操作，比如SQL查询等，直接生成类型安全的Go代码。它不仅保留了SQL的灵活性和可读性，同时也提供了编译时的类型检查，可以避免手写SQL代码中的错误。</p>
<h3>5.1 安装sqlc</h3>
<p>和上面的两种方式不同，使用sqlc，我们需要首先安装sqlc <a href="https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go">cmdline工具</a>，这个工具用来基于sqlc定义的一套SQL <a href="https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1">dsl语法</a>生成相应的Go代码。</p>
<p>通过下面命令可以实现sqlc工具的安装：</p>
<pre><code>$go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
</code></pre>
<p>安装后，输入下面命令验证一下sqlc的安装结果，如果输出下面内容，则说明安装ok了：</p>
<pre><code>$sqlc -h
Usage:
  sqlc [command]

Available Commands:
  compile     Statically check SQL for syntax and type errors
  completion  Generate the autocompletion script for the specified shell
  createdb    Create an ephemeral database
  diff        Compare the generated files to the existing files
  generate    Generate source code from SQL
  help        Help about any command
  init        Create an empty sqlc.yaml settings file
  push        Push the schema, queries, and configuration for this project
  verify      Verify schema, queries, and configuration for this project
  version     Print the sqlc version number
  vet         Vet examines queries

Flags:
  -f, --file string   specify an alternate config file (default: sqlc.yaml)
  -h, --help          help for sqlc
      --no-remote     disable remote execution (default: false)
      --remote        enable remote execution (default: false)

Use "sqlc [command] --help" for more information about a command.
</code></pre>
<h3>5.2 初始化和配置sqlc项目</h3>
<p>下面是sqlc的代码生成上的输入与输出示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-the-ways-to-access-databases-in-go-3.png" alt="" /></p>
<p>我们看到要生成Go代码，我们需要提供三个输入文件，其中sqlc.yaml是sqlc项目的主配置文件，它是个<a href="https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/">yaml格式文件</a>，在我们这个示例中，它的内容如下：</p>
<pre><code>// database-access/sqlc/sqlc.yaml
version: "2"
sql:
  - name: "db"
    engine: "mysql"
    queries: "queries.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "db"
        out: "db"
</code></pre>
<p>这个文件可以使用sqlc init生成一个模板，然后再向其中填写具体内容。上述sqlc.yaml的内容不难理解，其中engine表示生成的代码将用于与MySQL交互，schema是数据库模式文件，queries.sql中定义了与数据库的所有交互语句，而在gen段中，package是输出的代码的包名，而out则是输出到哪个目录下。</p>
<p>接下来，我们再来看看schema.sql和queries.sql。</p>
<h3>5.3 创建数据库模式和查询文件</h3>
<p>schema.sql文件的内容与我们</p>
<pre><code>-- schema.sql

CREATE TABLE department (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE instructor (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

CREATE TABLE course (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

CREATE TABLE student (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

CREATE TABLE enrollment (
    student_id INT,
    course_id INT,
    semester VARCHAR(50) NOT NULL,
    year INT NOT NULL,
    PRIMARY KEY (student_id, course_id, semester, year),
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);

</code></pre>
<p>没错，这就是一些建表语句，后续sqlc执行生成命令时会参考这些表以及约束。queries.sql则是我们要使用的数据库dml语句：</p>
<pre><code>-- name: CreateDepartment :execresult
INSERT INTO department (
  name
) VALUES (
  ?
);

-- name: GetDepartments :many
SELECT id, name FROM department;

-- name: CreateInstructor :execresult
INSERT INTO instructor (
  name, dept_id
) VALUES (
  ?, ?
);

-- name: GetInstructors :many
SELECT id, name, dept_id FROM instructor;

-- name: CreateCourse :execresult
INSERT INTO course (
  title, dept_id
) VALUES (
  ?, ?
);

-- name: GetCoursesByDept :many
SELECT id, title, dept_id FROM course WHERE dept_id = ?;

-- name: CreateStudent :execresult
INSERT INTO student (
  name, dept_id
) VALUES (
  ?, ?
);

-- name: GetStudents :many
SELECT id, name, dept_id FROM student;

-- name: EnrollStudent :execresult
INSERT INTO enrollment (
  student_id, course_id, semester, year
) VALUES (
  ?, ?, ?, ?
);

-- name: GetEnrollmentByStudent :many
SELECT student_id, course_id, semester, year FROM enrollment WHERE student_id = ?;

-- name: UpdateStudentName :exec
UPDATE student SET name = ?
WHERE id = ?;

-- name: UpdateCourseTitle :exec
UPDATE course SET title = ?
WHERE id = ?;

-- name: DeleteStudent :exec
DELETE FROM student
WHERE id = ?;

-- name: DeleteCourse :exec
DELETE FROM course
WHERE id = ?;

-- name: DeleteEnrollmentByCourseID :exec
DELETE FROM enrollment
WHERE course_id = ?;
</code></pre>
<p>务必注意：针对不同的数据库，queries.sql中使用的语法<strong>有所不同</strong>，关于queries.sql的DSL语法形式的详细内容，可参考<a href="https://docs.sqlc.dev/en/latest/index.html">sqlc docs</a>。</p>
<h3>5.4 生成代码</h3>
<p>在项目sqlc根目录下运行下面sqlc命令可以在指定的db目录下生成包名为db的Go代码：</p>
<pre><code>$sqlc generate
$tree db
db
├── db.go
├── models.go
└── queries.sql.go
</code></pre>
<p>其中queries.sql.go是对应queries.sql中所有dml操作的方法。下面摘录queries.sql.go的代码片段：</p>
<pre><code>// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.26.0
// source: queries.sql

package db

import (
    "context"
    "database/sql"
)

const createCourse = `-- name: CreateCourse :execresult
INSERT INTO course (
  title, dept_id
) VALUES (
  ?, ?
)
`

type CreateCourseParams struct {
    Title  string
    DeptID sql.NullInt32
}

func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (sql.Result, error) {
    return q.db.ExecContext(ctx, createCourse, arg.Title, arg.DeptID)
}

const createDepartment = `-- name: CreateDepartment :execresult
INSERT INTO department (
  name
) VALUES (
  ?
)
`

func (q *Queries) CreateDepartment(ctx context.Context, name string) (sql.Result, error) {
    return q.db.ExecContext(ctx, createDepartment, name)
}
... ...
</code></pre>
<p>我们看到，queries.sql中的操作都以Queries类型的方法形式存在，在后面的使用过程中，我们可以体会这种方式带来的编码时的便利。</p>
<h3>5.5 使用生成的代码访问和操作数据库</h3>
<p>下面是使用生成的Go代码进行数据库操作的示例，我们需要先初始化数据库连接并创建Queries实例，然后基于创建的Queries实例的方法进行数据库表操作：</p>
<pre><code>// database-access/sqlc/main.go

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"

    "demo/db"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    dsn := "root:123456@tcp(127.0.0.1:4407)/example_db"
    conn, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    queries := db.New(conn)

    // 执行CRUD操作
    createData(queries)
    queryData(queries)
    updateData(queries)
    deleteData(queries)
}

func createData(queries *db.Queries) {
    ctx := context.Background()

    // 创建部门
    _, err := queries.CreateDepartment(ctx, "Computer Science")
    if err != nil {
        log.Fatal(err)
    }
    _, err = queries.CreateDepartment(ctx, "Mathematics")
    if err != nil {
        log.Fatal(err)
    }

    // 创建教师
    _, err = queries.CreateInstructor(ctx, db.CreateInstructorParams{Name: "John Doe", DeptID: sql.NullInt32{1, true}})
    if err != nil {
        log.Fatal(err)
    }
    _, err = queries.CreateInstructor(ctx, db.CreateInstructorParams{Name: "Jane Smith", DeptID: sql.NullInt32{2, true}})
    if err != nil {
        log.Fatal(err)
    }

    // 创建课程
    _, err = queries.CreateCourse(ctx, db.CreateCourseParams{Title: "Database Systems", DeptID: sql.NullInt32{1, true}})
    if err != nil {
        log.Fatal(err)
    }
    _, err = queries.CreateCourse(ctx, db.CreateCourseParams{Title: "Calculus", DeptID: sql.NullInt32{2, true}})
    if err != nil {
        log.Fatal(err)
    }

    // 创建学生
    _, err = queries.CreateStudent(ctx, db.CreateStudentParams{Name: "Alice", DeptID: sql.NullInt32{1, true}})
    if err != nil {
        log.Fatal(err)
    }
    _, err = queries.CreateStudent(ctx, db.CreateStudentParams{Name: "Bob", DeptID: sql.NullInt32{2, true}})
    if err != nil {
        log.Fatal(err)
    }

    // 学生选课
    _, err = queries.EnrollStudent(ctx, db.EnrollStudentParams{StudentID: sql.NullInt32{1, true}, CourseID: sql.NullInt32{1, true}, Semester: "Fall", Year: 2024})
    if err != nil {
        log.Fatal(err)
    }
    _, err = queries.EnrollStudent(ctx, db.EnrollStudentParams{StudentID: sql.NullInt32{2, true}, CourseID: sql.NullInt32{2, true}, Semester: "Fall", Year: 2024})
    if err != nil {
        log.Fatal(err)
    }
}

func queryData(queries *db.Queries) {
    ctx := context.Background()

    // 查询所有学生
    students, err := queries.GetStudents(ctx)
    if err != nil {
        log.Fatal(err)
    }
    for _, student := range students {
        fmt.Printf("Student ID: %d, Name: %s, Department ID: %d\n", student.ID, student.Name, student.DeptID.Int32)
    }

    // 查询某个部门的课程
    courses, err := queries.GetCoursesByDept(ctx, sql.NullInt32{1, true})
    if err != nil {
        log.Fatal(err)
    }
    for _, course := range courses {
        fmt.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.ID, course.Title, course.DeptID.Int32)
    }

    // 查询某个学生的选课信息
    enrollments, err := queries.GetEnrollmentByStudent(ctx, sql.NullInt32{1, true})
    if err != nil {
        log.Fatal(err)
    }
    for _, enrollment := range enrollments {
        fmt.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.StudentID.Int32, enrollment.CourseID.Int32, enrollment.Semester, enrollment.Year)
    }
}

func updateData(queries *db.Queries) {
    ctx := context.Background()

    // 更新学生姓名
    err := queries.UpdateStudentName(ctx, db.UpdateStudentNameParams{ID: 1, Name: "Alice Johnson"})
    if err != nil {
        log.Fatal(err)
    }

    // 更新课程标题
    err = queries.UpdateCourseTitle(ctx, db.UpdateCourseTitleParams{ID: 1, Title: "Advanced Database Systems"})
    if err != nil {
        log.Fatal(err)
    }
}

func deleteData(queries *db.Queries) {
    ctx := context.Background()

    // 删除选课记录
    err := queries.DeleteEnrollmentByCourseID(ctx, sql.NullInt32{1, true})
    if err != nil {
        log.Fatal(err)
    }

    // 删除课程
    err = queries.DeleteCourse(ctx, 1)
    if err != nil {
        log.Fatal(err)
    }

    // 删除学生
    err = queries.DeleteStudent(ctx, 1)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>通过上述示例，我们可以看到sqlc在生成类型安全的Go代码方面非常高效，它结合了SQL查询的灵活性和Go语言的类型安全特性，使得数据库操作更加直观和可靠。不过，学习sqlc的DSL还是需要一点时间的，也有一个小小的门槛。</p>
<p>除了sqlc，Facebook开源的entgo是一个同时基于代码生成以及ORM进行数据库操作的方式。和sqlc一样，entgo在前期需要一定的额外学习成本。下面我们来看看使用entgo如何访问和操作数据库。</p>
<h3>5.6. 使用entgo操作数据库</h3>
<p>Ent是Facebook开源的一个实体框架，它使用Schema作为强类型的Go代码生成数据模型和查询方法。Ent提供了类型安全的API、自动化的迁移、GraphQL支持等特性。</p>
<h4>5.6.1 安装Ent</h4>
<p>和sqlc一样，由于需要代码生成，我们需要先安装ent的命令行工具：</p>
<pre><code>$go install  entgo.io/ent/cmd/ent@latest
</code></pre>
<p>使用下面命令可以验证ent安装是否ok：</p>
<pre><code>$ent -h
Usage:
  ent [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  describe    print a description of the graph schema
  generate    generate go code for the schema directory
  help        Help about any command
  new         initialize a new environment with zero or more schemas

Flags:
  -h, --help   help for ent

Use "ent [command] --help" for more information about a command.
</code></pre>
<p>接下来，和sqlc一样，我们需要使用ent的DSL来定义schema，和sqlc不同，ent使用Go语法来定义schema。</p>
<h4>5.6.2 定义Schema</h4>
<p>使用Ent需要先定义Schema，我们建立一个schema目录，将所有schema相关的Go定义文件都放入目录中：</p>
<pre><code>$tree schema
schema
├── course.go
├── department.go
├── enrollment.go
├── instructor.go
└── student.go
</code></pre>
<p>schema目录下的每个文件都是一个entity的定义，以department.go为例：</p>
<pre><code>// database-access/ent/schema/department.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// Department holds the schema definition for the Department entity.
type Department struct {
    ent.Schema
}

// Fields of the Department.
func (Department) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").NotEmpty(),
    }
}

// Edges of the Department.
func (Department) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("instructors", Instructor.Type),
        edge.To("courses", Course.Type),
        edge.To("students", Student.Type),
    }
}
</code></pre>
<p>我们看到结构体类型Department对应表department，department与其他表之间的关系使用ent.Edge表示，这样就建立了与其他表的关系。有了Schema定义后，我们就可以来生成代码了。</p>
<h4>5.6.3 生成代码</h4>
<p>我们在database-access/ent目录下执行下面命令：</p>
<pre><code>$ent generate demo/schema --target ent
</code></pre>
<p>ent会基于demo/schema包生成相应代码(这里go module为demo)，即在ent目录下生成包名为ent的代码：</p>
<pre><code>$tree -L 1 -F ./ent
./ent
├── client.go
├── course/
├── course.go
├── course_create.go
├── course_delete.go
├── course_query.go
├── course_update.go
├── department/
├── department.go
├── department_create.go
├── department_delete.go
├── department_query.go
├── department_update.go
├── enrollment/
├── enrollment.go
├── enrollment_create.go
├── enrollment_delete.go
├── enrollment_query.go
├── enrollment_update.go
├── ent.go
├── enttest/
├── hook/
├── instructor/
├── instructor.go
├── instructor_create.go
├── instructor_delete.go
├── instructor_query.go
├── instructor_update.go
├── migrate/
├── mutation.go
├── predicate/
├── runtime/
├── runtime.go
├── student/
├── student.go
├── student_create.go
├── student_delete.go
├── student_query.go
├── student_update.go
└── tx.go
</code></pre>
<p>我们看到，ent为每个entity，比如department都生成了一组文件，包括增删改查。接下来，我们就来使用ent生成的代码来操作数据库！</p>
<h4>5.6.4 使用生成的代码操作数据库</h4>
<p>下面是使用ent生成的代码操作数据库的示例代码：</p>
<pre><code>// database-access/ent/main.go
package main

import (
    "context"
    "log"

    "demo/ent"
    "demo/ent/course"
    "demo/ent/department"
    "demo/ent/enrollment"
    "demo/ent/student"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    client, err := ent.Open("mysql", "root:123456@tcp(127.0.0.1:4407)/example_db?parseTime=True")
    if err != nil {
        log.Fatalf("failed opening connection to mysql: %v", err)
    }
    defer client.Close()
    ctx := context.Background()

    // Run the automatic migration tool to create all schema resources.
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }

    // 执行CRUD操作
    createData(ctx, client)
    queryData(ctx, client)
    updateData(ctx, client)
    deleteData(ctx, client)
}

func createData(ctx context.Context, client *ent.Client) {
    // 创建部门
    cs, err := client.Department.Create().SetName("Computer Science").Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
    math, err := client.Department.Create().SetName("Mathematics").Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 创建教师
    _, err = client.Instructor.Create().SetName("John Doe").SetDepartment(cs).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
    _, err = client.Instructor.Create().SetName("Jane Smith").SetDepartment(math).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 创建课程
    dbCourse, err := client.Course.Create().SetTitle("Database Systems").SetDepartment(cs).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
    calcCourse, err := client.Course.Create().SetTitle("Calculus").SetDepartment(math).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 创建学生
    alice, err := client.Student.Create().SetName("Alice").SetDepartment(cs).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
    bob, err := client.Student.Create().SetName("Bob").SetDepartment(math).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 学生选课
    _, err = client.Enrollment.Create().SetStudent(alice).SetCourse(dbCourse).SetSemester("Fall").SetYear(2024).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
    _, err = client.Enrollment.Create().SetStudent(bob).SetCourse(calcCourse).SetSemester("Fall").SetYear(2024).Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
}

func queryData(ctx context.Context, client *ent.Client) {
    // 查询所有学生
    //students, err := client.Student.Query().All(ctx)
    students, err := client.Student.Query().WithDepartment().All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    for _, stu := range students {
        log.Printf("Student ID: %d, Name: %s, Department ID: %d\n", stu.ID, stu.Name, stu.Edges.Department.ID)
    }

    // 查询某个部门的课程
    courses, err := client.Course.Query().WithDepartment().Where(course.HasDepartmentWith(department.ID(1))).All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    for _, course := range courses {
        log.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.ID, course.Title, course.Edges.Department.ID)
    }

    // 查询某个学生的选课信息
    enrollments, err := client.Enrollment.Query().WithStudent().WithCourse().Where(enrollment.HasStudentWith(student.ID(1))).All(ctx)
    if err != nil {
        log.Fatal(err)
    }
    for _, enrollment := range enrollments {
        log.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.Edges.Student.ID,
            enrollment.Edges.Course.ID, enrollment.Semester, enrollment.Year)
    }
}

func updateData(ctx context.Context, client *ent.Client) {
    // 更新学生姓名
    _, err := client.Student.UpdateOneID(1).SetName("Alice Johnson").Save(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 更新课程标题
    _, err = client.Course.UpdateOneID(1).SetTitle("Advanced Database Systems").Save(ctx)
    if err != nil {
        log.Fatal(err)
    }
}

func deleteData(ctx context.Context, client *ent.Client) {
    // 删除选课记录
    _, err := client.Enrollment.Delete().Where(enrollment.HasCourseWith(course.ID(1))).Exec(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 删除课程
    err = client.Course.DeleteOneID(1).Exec(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 删除学生
    err = client.Student.DeleteOneID(1).Exec(ctx)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>通过以上示例可以看到，使用GORM和Ent都可以大大简化数据库操作，并提供了类型安全的API和自动化的迁移支持，使得开发更加高效和可靠。</p>
<p>到这里我们已经见识到了三类数据库访问和操作的方式，那么究竟那种适合我们呢？我们接下来做一个简单的对比。</p>
<h2>6. 不同数据库访问方式的对比</h2>
<p>在前面的小节中，我们介绍了三种主要的数据库访问方式：Go标准库、ORM库（GORM），以及代码生成工具（sqlc和ent）。在本节中，我们将基于前面示例中的表现，对这些方式进行一个简单的对比，以帮助开发者在实际项目中做出最佳选择。</p>
<p>以下是整理的关于Go不同数据库访问方式优缺点的表格：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-the-ways-to-access-databases-in-go-4.png" alt="" /></p>
<p>这张表格总结了不同数据库访问方式的优缺点，帮助读者选择最适合其项目需求的方式。</p>
<p>关于各种数据库访问方式的性能对比，做起来还是稍麻烦的，之前goland博客曾发表过一篇<a href="https://blog.jetbrains.com/zh-hans/go/2023/06/30/database-sql-gorm-sqlx-sqlc/">有关go标准库 vs. gorm vs. sqlx. vs. sqlc的压测的文章</a>，大家可以参考其中的结论，即Go标准库、sqlc由于是原生sql操作，所以性能最佳；sqlx略有扩展，性能次之；gorm在小数据量的情况下，性能是很快的，但性能会随着数据量的增加而下降很多。</p>
<p>综合，以上对比与性能情况，这里也给出一些选择建议：</p>
<ul>
<li>如果性能是首要考虑，且不介意手写SQL查询，推荐使用Go标准库 (database/sql)。</li>
<li>如果需要更多的功能和一些简化的开发体验，可以选择sqlx。</li>
<li>如果需要高级的ORM特性和简化开发过程，GORM和Ent都是不错的选择，但需要注意性能开销。</li>
<li>如果希望在保持性能的同时获得类型安全和编译时检查，sqlc是一个非常好的选择。</li>
</ul>
<h2>7. 小结</h2>
<p>本文详细介绍了在Go语言中访问和操作数据库的几种主流方式。</p>
<p>我们首先搭建了一个基于MySQL数据库的示例环境，并定义了一个简单的学校院系选课关系模型作为数据库模式。然后，我们分别使用以下三种方法实现了对该数据库的CRUD操作：</p>
<ul>
<li>
<p>使用Go标准库database/sql加上特定数据库的driver，并配合像sqlx这样的功能增强包。这种方式灵活性高，可完全控制SQL语句，但需要编写较多样板代码。</p>
</li>
<li>
<p>使用ORM工具GORM，这种方式可以将数据库操作抽象为对象关系映射，降低开发难度，但功能可能无法完全满足需求，性能也会在数据量增大的情况下有较大下降。</p>
</li>
<li>
<p>使用代码生成 + ORM 的方式，如sqlc和Ent。这种方式将SQL语句编译为Go代码或直接用Go代码表述schema，既可以获得类似ORM的便利，又可以自定义SQL语句。不过这种方式有相对高一些的学习门槛，比如要熟练掌握sqlc的DSL语法才能写出满足要求的数据库操作语句。</p>
</li>
</ul>
<p>最后，我们还简单对比了这三种方法的优劣，希望可以帮助大家选择出适合自身项目的数据库访问方式。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/blob/master/database-access">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/blob/master/database-access</p>
<p>本文中的部分源码由OpenAI的GPT-4o生成。</p>
<h2>8. 参考资料</h2>
<ul>
<li>比较database/sql、GORM、sqlx 和 sqlc &#8211; https://blog.jetbrains.com/zh-hans/go/2023/06/30/database-sql-gorm-sqlx-sqlc/</li>
<li>https://github.com/rexfordnyrk/go-db-comparison/</li>
<li>https://www.reddit.com/r/golang/comments/130kxaw/comparing_databasesql_gorm_sqlx_and_sqlc/</li>
<li>sqlc介绍 &#8211; https://conroy.org/introducing-sqlc</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; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>单测时尽量用fake object</title>
		<link>https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/</link>
		<comments>https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/#comments</comments>
		<pubDate>Thu, 20 Apr 2023 14:13:11 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[Dummy]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[FakeObject]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httptest]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Postgres]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[Stub]]></category>
		<category><![CDATA[SUT]]></category>
		<category><![CDATA[testcontainers]]></category>
		<category><![CDATA[testcontainers-go]]></category>
		<category><![CDATA[TestDouble]]></category>
		<category><![CDATA[testify]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[unit-testing]]></category>
		<category><![CDATA[xUnit]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[单测]]></category>
		<category><![CDATA[外部协作者]]></category>
		<category><![CDATA[框架]]></category>
		<category><![CDATA[测试替身]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3860</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators 1. 单元测试的难点：外部协作者(external collaborators)的存在 单元测试是软件开发的一个重要部分，它有助于在开发周期的早期发现错误，帮助开发人员增加对生产代码正常工作的信心，同时也有助于改善代码设计。Go语言从诞生那天起就内置Testing框架(以及测试覆盖率计算工具)，基于该框架，Gopher们可以非常方便地为自己设计实现的package编写测试代码。 注：《Go语言精进之路》vol2中的第40条到第44条有关于Go包内、包外测试区别、测试代码组织、表驱动测试、管理外部测试数据等内容的系统地讲解，感兴趣的童鞋可以读读。 不过即便如此，在实际开发工作中，大家发现单元测试的覆盖率依旧很低，究其原因，排除那些对测试代码不作要求的组织，剩下的无非就是代码设计不佳，使得代码不易测；或是代码有外部协作者（比如数据库、redis、其他服务等）。代码不易测可以通过重构来改善，但如果代码有外部协作者，我们该如何对代码进行测试呢，这也是各种编程语言实施单元测试的一大共同难点。 为此，《xUnit Test Patterns : Refactoring Test Code》一书中提供了Test Double(测试替身)的概念专为解决此难题。那么什么是Test Double呢？我们接下来就来简单介绍一下Test Double的概念以及常见的种类。 2. 什么是Test Double？ 测试替身是在测试阶段用来替代被测系统依赖的真实组件的对象或程序(如下图)，以方便测试，这些真实组件或程序即是外部协作者(external collaborators)。这些外部协作者在测试环境下通常很难获取或与之交互。测试替身可以使开发人员或QA专业人员专注于新的代码而不是代码与环境集成。 测试替身是通用术语，指的是不同类型的替换对象或程序。目前xUnit Patterns至少定义了五种类型的Test Doubles： Test stubs Mock objects Test spies Fake objects Dummy objects 这其中最为常用的是Fake objects、stub和mock objects。下面逐一说说这三种test double： 2.1 fake object fake object最容易理解，它是被测系统SUT(System Under Test)依赖的外部协作者的“替身”，和真实的外部协作者相比，fake object外部行为表现与真实组件几乎是一致的，但更简单也更易于使用，实现更轻量，仅用于满足测试需求即可。 fake object也是Go testing中最为常用的一类fake object。以Go的标准库为例，我们在src/database/sql下面就看到了Go标准库为进行sql包测试而实现的一个database driver： // [...]]]></description>
			<content:encoded><![CDATA[<p><a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators">本文永久链接</a> &#8211; https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators</p>
<p><img src="https://tonybai.com/wp-content/uploads/provide-fake-object-for-external-collaborators-1.png" alt="" /></p>
<h2>1. 单元测试的难点：外部协作者(external collaborators)的存在</h2>
<p>单元测试是软件开发的一个重要部分，它有助于在开发周期的早期发现错误，帮助开发人员增加对生产代码正常工作的信心，同时也有助于改善代码设计。<strong>Go语言从诞生那天起就内置Testing框架(以及测试覆盖率计算工具)</strong>，基于该框架，Gopher们可以非常方便地为自己设计实现的package编写测试代码。</p>
<blockquote>
<p>注：<a href="https://book.douban.com/subject/35720729/">《Go语言精进之路》vol2</a>中的第40条到第44条有关于Go包内、包外测试区别、测试代码组织、表驱动测试、管理外部测试数据等内容的系统地讲解，感兴趣的童鞋可以读读。</p>
</blockquote>
<p>不过即便如此，在实际开发工作中，大家发现单元测试的覆盖率依旧很低，究其原因，排除那些对测试代码不作要求的组织，剩下的无非就是代码设计不佳，使得代码不易测；或是代码有外部协作者（比如数据库、redis、其他服务等）。代码不易测可以通过重构来改善，但如果代码有外部协作者，我们该如何对代码进行测试呢，这也是<strong>各种编程语言实施单元测试的一大共同难点</strong>。</p>
<p>为此，<a href="https://book.douban.com/subject/1859393/">《xUnit Test Patterns : Refactoring Test Code》</a>一书中提供了<strong>Test Double(测试替身)</strong>的概念专为解决此难题。那么什么是Test Double呢？我们接下来就来简单介绍一下Test Double的概念以及常见的种类。</p>
<h2>2. 什么是Test Double？</h2>
<p>测试替身是在测试阶段用来替代被测系统依赖的真实组件的对象或程序(如下图)，以方便测试，这些真实组件或程序即是外部协作者(external collaborators)。这些外部协作者在测试环境下通常很难获取或与之交互。测试替身可以使开发人员或QA专业人员专注于新的代码而不是代码与环境集成。</p>
<p><img src="https://tonybai.com/wp-content/uploads/provide-fake-object-for-external-collaborators-2.png" alt="" /></p>
<p>测试替身是通用术语，指的是不同类型的替换对象或程序。目前<a href="http://xunitpatterns.com/Test%20Double%20Patterns.html">xUnit Patterns</a>至少定义了五种类型的Test Doubles：</p>
<ul>
<li>Test stubs</li>
<li>Mock objects</li>
<li>Test spies</li>
<li>Fake objects</li>
<li>Dummy objects</li>
</ul>
<p>这其中最为常用的是Fake objects、stub和mock objects。下面逐一说说这三种test double：</p>
<h3>2.1 fake object</h3>
<p>fake object最容易理解，它是被测系统SUT(System Under Test)依赖的外部协作者的“替身”，和真实的外部协作者相比，fake object外部行为表现与真实组件几乎是一致的，但更简单也更易于使用，实现更轻量，仅用于满足测试需求即可。</p>
<p>fake object也是Go testing中最为常用的一类fake object。以Go的标准库为例，我们在src/database/sql下面就看到了Go标准库为进行sql包测试而实现的一个database driver：</p>
<pre><code>// $GOROOT/src/database/fakedb_test.go

var fdriver driver.Driver = &amp;fakeDriver{}

func init() {
    Register("test", fdriver)
}
</code></pre>
<p>我们知道一个真实的sql数据库的代码量可是数以百万计的，这里不可能实现一个生产级的真实SQL数据库，从fakedb_test.go源文件的注释我们也可以看到，这个fakeDriver仅仅是用于testing，它是一个实现了driver.Driver接口的、支持少数几个DDL(create)、DML(insert)和DQL(selet)的toy版的纯内存数据库：</p>
<pre><code>// fakeDriver is a fake database that implements Go's driver.Driver
// interface, just for testing.
//
// It speaks a query language that's semantically similar to but
// syntactically different and simpler than SQL.  The syntax is as
// follows:
//
//  WIPE
//  CREATE|&lt;tablename&gt;|&lt;col&gt;=&lt;type&gt;,&lt;col&gt;=&lt;type&gt;,...
//    where types are: "string", [u]int{8,16,32,64}, "bool"
//  INSERT|&lt;tablename&gt;|col=val,col2=val2,col3=?
//  SELECT|&lt;tablename&gt;|projectcol1,projectcol2|filtercol=?,filtercol2=?
//  SELECT|&lt;tablename&gt;|projectcol1,projectcol2|filtercol=?param1,filtercol2=?param2
</code></pre>
<p>与此类似的，Go标准库中还有net/dnsclient_unix_test.go中的fakeDNSServer等。此外，Go标准库中一些以mock做前缀命名的变量、类型等其实质上是fake object。</p>
<p>我们再来看第二种test double: stub。</p>
<h3>2.2 stub</h3>
<p>stub显然也是一个在测试阶段专用的、用来替代真实外部协作者与SUT进行交互的对象。与fake object稍有不同的是，stub是一个内置了预期值/响应值且可以在多个测试间复用的替身object。</p>
<p>stub可以理解为一种fake object的特例。</p>
<blockquote>
<p>注：fakeDriver在sql_test.go中的不同测试场景中时而是fake object，时而是stub(见sql_test.go中的newTestDBConnector函数)。</p>
</blockquote>
<p>Go标准库中的net/http/httptest就是一个提供创建stub的典型的测试辅助包，十分适合对http.Handler进行测试，这样我们无需真正启动一个http server。下面就是基于httptest的一个测试例子：</p>
<pre><code>// 被测对象 client.go

package main

import (
    "bytes"
    "net/http"
)

// Function that uses the client to make a request and parse the response
func GetResponse(client *http.Client, url string) (string, error) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return "", err
    }

    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    buf := new(bytes.Buffer)
    _, err = buf.ReadFrom(resp.Body)
    if err != nil {
        return "", err
    }

    return buf.String(), nil
}

// 测试代码 client_test.go

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestClient(t *testing.T) {
    // Create a new test server with a handler that returns a specific response
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "Hello, world!"}`))
    }))
    defer server.Close()

    // Create a new client that uses the test server
    client := server.Client()

    // Call the function that uses the client
    message, err := GetResponse(client, server.URL)

    // Check that the response is correct
    expected := `{"message": "Hello, world!"}`
    if message != expected {
        t.Errorf("Expected response %q, but got %q", expected, message)
    }

    // Check that no errors were returned
    if err != nil {
        t.Errorf("Unexpected error: %v", err)
    }
}
</code></pre>
<p>在这个例子中，我们要测试一个名为GetResponse的函数，该函数通过client向url发送Get请求，并将收到的响应内容读取出来并返回。为了测试这个函数，我们需要“建立”一个与GetResponse进行协作的外部http server，这里我们使用的就是httptest包。我们通过httptest.NewServer建立这个server，该server预置了一个返回特定响应的HTTP handler。我们通过该server得到client和对应的url参数后，将其传给被测目标GetResponse，并将其返回的结果与预期作比较来完成这个测试。注意，我们在测试结束后使用defer server.Close()来关闭测试服务器，以确保该服务器不会在测试结束后继续运行。</p>
<p>httptest还常用来做http.Handler的测试，比如下面这个例子：</p>
<pre><code>// handler.go

package main

import (
    "bytes"
    "io"
    "net/http"
)

func AddHelloPrefix(w http.ResponseWriter, r *http.Request) {
    b, err := io.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    w.Write(bytes.Join([][]byte{[]byte("hello, "), b}, nil))
    w.WriteHeader(http.StatusOK)
}

// handler_test.go

package main

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestHandler(t *testing.T) {
    r := strings.NewReader("world!")
    req, err := http.NewRequest("GET", "/test", r)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(AddHelloPrefix)
    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    expected := "hello, world!"
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}
</code></pre>
<p>在这个例子中，我们创建一个新的http.Request对象，用于向/test路径发出GET请求。然后我们创建一个新的httptest.ResponseRecorder对象来捕获服务器的响应。 我们定义一个简单的HTTP Handler(被测函数): AddHelloPrefix，该Handler会在请求的内容之前加上”hello, “并返回200 OK状态代码作为响应体。之后，我们在handler上调用ServeHTTP方法，传入httptest.ResponseRecorder和http.Request对象，这会将请求“发送”到处理程序并捕获响应。最后，我们使用标准的Go测试包来检查响应是否具有预期的状态码和正文。</p>
<p>在这个例子中，我们利用net/http/httptest创建了一个测试服务器“替身”，并向其“发送”间接预置信息的请求以测试Go中的HTTP handler。这个过程中其实并没有任何网络通信，也没有http协议打包和解包的过程，我们也不关心http通信，那是Go net/http包的事情，我们只care我们的Handler是否能按逻辑运行。</p>
<p>fake object与stub的优缺点基本一样。多数情况下，<strong>大家也无需将这二者划分的很清晰</strong>。</p>
<h3>2.3 mock object</h3>
<p>和fake/stub一样，mock object也是一个测试替身。通过上面的例子我们看到fake建立困难(比如创建一个近2千行代码的fakeDriver)，但使用简单。而mock object则是一种建立简单，使用简单程度因被测目标与外部协作者交互复杂程度而异的test double，我们看一下下面这个例子：</p>
<pre><code>// db.go 被测目标

package main

// Define the `Database` interface
type Database interface {
    Save(data string) error
    Get(id int) (string, error)
}

// Example functions that use the `Database` interface
func saveData(db Database, data string) error {
    return db.Save(data)
}

func getData(db Database, id int) (string, error) {
    return db.Get(id)
}

// 测试代码

package main

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

// Define a mock struct that implements the `Database` interface
type MockDatabase struct {
    mock.Mock
}

func (m *MockDatabase) Save(data string) error {
    args := m.Called(data)
    return args.Error(0)
}

func (m *MockDatabase) Get(id int) (string, error) {
    args := m.Called(id)
    return args.String(0), args.Error(1)
}

func TestSaveData(t *testing.T) {
    // Create a new mock database
    db := new(MockDatabase)

    // Expect the `Save` method to be called with "test data"
    db.On("Save", "test data").Return(nil)

    // Call the code that uses the database
    err := saveData(db, "test data")

    // Assert that the `Save` method was called with the correct argument
    db.AssertCalled(t, "Save", "test data")

    // Assert that no errors were returned
    assert.NoError(t, err)
}

func TestGetData(t *testing.T) {
    // Create a new mock database
    db := new(MockDatabase)

    // Expect the `Get` method to be called with ID 123 and return "test data"
    db.On("Get", 123).Return("test data", nil)

    // Call the code that uses the database
    data, err := getData(db, 123)

    // Assert that the `Get` method was called with the correct argument
    db.AssertCalled(t, "Get", 123)

    // Assert that the correct data was returned
    assert.Equal(t, "test data", data)

    // Assert that no errors were returned
    assert.NoError(t, err)
}
</code></pre>
<p>在这个例子中，被测目标是两个接受Database接口类型参数的函数：saveData和getData。显然在单元测试阶段，我们不能真正为这两个函数传入真实的Database实例去测试。</p>
<p>这里，我们没有使用fake object，而是定义了一个mock object：MockDatabase，该类型实现了Database接口。然后我们定义了两个测试函数，TestSaveData和TestGetData，它们分别使用MockDatabase实例来测试saveData和getData函数。</p>
<p>在每个测试函数中，我们对MockDatabase实例进行设置，包括期待特定参数的方法调用，然后调用使用该数据库的代码(即被测目标函数saveData和getData)。然后我们使用github.com/stretchr/testify中的assert包，对代码的预期行为进行断言。</p>
<blockquote>
<p>注：除了上述测试中使用的AssertCalled方法外，MockDatabase结构还提供了其他方法来断言方法被调用的次数、方法被调用的顺序等。请查看github.com/stretchr/testify/mock包的文档，了解更多信息。</p>
</blockquote>
<h2>3. Test Double有多种，选哪个呢？</h2>
<p>从mock object的例子来看，测试代码的核心就是mock object的构建与mock object的方法的参数和返回结果的设置，相较于fake object的简单直接，mock object在使用上较为难于理解。而且对Go语言来说，mock object要与接口类型联合使用，如果被测目标的参数是非接口类型，mock object便“无从下嘴”了。此外，mock object使用难易程度与被测目标与外部协作者的交互复杂度相关。像上面这个例子，建立mock object就比较简单。但对于一些复杂的函数，当存在多个外部协作者且与每个协作者都有多次交互的情况下，建立和设置mock object就将变得困难并更加难于理解。</p>
<p>mock object仅是满足了被测目标对依赖的外部协作者的调用需求，比如设置不同参数传入下的不同返回值，但mock object并未真实处理被测目标传入的参数，这会降低测试的可信度以及开发人员对代码正确性的信心。</p>
<p>此外，如果被测函数的输入输出未发生变化，但内部逻辑发生了变化，比如调用的外部协作者的方法参数、调用次数等，使用mock object的测试代码也需要一并更新维护。</p>
<p>而通过上面的fakeDriver、fakeDNSSever以及httptest应用的例子，我们看到：作为test double，fake object/stub有如下优点：</p>
<ul>
<li>我们与fake object的交互方式与与真实外部协作者交互的方式相同，这让其显得更简单，更容易使用，也降低了测试的复杂性；</li>
<li>fake objet的行为更像真正的协作者，可以给开发人员更多的信心；</li>
<li>当真实协作者更新时，我们不需要更新使用fake object时设置的expection和结果验证条件，因此，使用fake object时，重构代码往往比使用其他test double更容易。</li>
</ul>
<p>不过fake object也有自己的不足之处，比如：</p>
<ul>
<li>fake object的创建和维护可能很费时，就像上面的fakeDriver，源码有近2k行；</li>
<li>fake object可能无法提供与真实组件相同的功能覆盖水平，这与fake object的提供方式有关。</li>
<li>fake object的实现需要维护，每当真正的协作者更新时，都必须更新fake object。</li>
</ul>
<p>综上，测试的主要意义是保证SUT代码的正确性，让开发人员对自己编写的代码更有信心，从这个角度来看，我们<strong>在单测时应首选为外部协作者提供fake object以满足测试需要</strong>。</p>
<h3>4. fake object的实现和获取方法</h3>
<p>随着技术的进步，fake object的实现和获取日益容易。</p>
<p>我们可以借助类似ChatGPT/copilot的工具快速构建出一个fake object，即便是几百行代码的fake object的实现也很容易。</p>
<p>如果要更高的可信度和更高的功能覆盖水平，我们还可以借助docker来构建“真实版/无阉割版”的fake object。</p>
<p>借助github上开源的<a href="https://golang.testcontainers.org/">testcontainers-go</a>可以更为简便的构建出一个fake object，并且testcontainer提供了常见的外部协作者的封装实现，比如：MySQL、Redis、Postgres等。</p>
<p>以测试redis client为例，我们使用testcontainer建立如下测试代码：</p>
<pre><code>// redis_test.go

package main

import (
    "context"
    "fmt"
    "testing"

    "github.com/go-redis/redis/v8"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

func TestRedisClient(t *testing.T) {
    // Create a Redis container with a random port and wait for it to start
    req := testcontainers.ContainerRequest{
        Image:        "redis:latest",
        ExposedPorts: []string{"6379/tcp"},
        WaitingFor:   wait.ForLog("Ready to accept connections"),
    }
    ctx := context.Background()
    redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Fatalf("Failed to start Redis container: %v", err)
    }
    defer redisC.Terminate(ctx)

    // Get the Redis container's host and port
    redisHost, err := redisC.Host(ctx)
    if err != nil {
        t.Fatalf("Failed to get Redis container's host: %v", err)
    }
    redisPort, err := redisC.MappedPort(ctx, "6379/tcp")
    if err != nil {
        t.Fatalf("Failed to get Redis container's port: %v", err)
    }

    // Create a Redis client and perform some operations
    client := redis.NewClient(&amp;redis.Options{
        Addr: fmt.Sprintf("%s:%s", redisHost, redisPort.Port()),
    })
    defer client.Close()

    err = client.Set(ctx, "key", "value", 0).Err()
    if err != nil {
        t.Fatalf("Failed to set key: %v", err)
    }

    val, err := client.Get(ctx, "key").Result()
    if err != nil {
        t.Fatalf("Failed to get key: %v", err)
    }

    if val != "value" {
        t.Errorf("Expected value %q, but got %q", "value", val)
    }
}
</code></pre>
<p>运行该测试将看到类似如下结果：</p>
<pre><code>$go test
2023/04/15 16:18:20 github.com/testcontainers/testcontainers-go - Connected to docker:
  Server Version: 20.10.8
  API Version: 1.41
  Operating System: Ubuntu 20.04.3 LTS
  Total Memory: 10632 MB
2023/04/15 16:18:21 Failed to get image auth for docker.io. Setting empty credentials for the image: docker.io/testcontainers/ryuk:0.3.4. Error is:credentials not found in native keychain

2023/04/15 16:19:06 Starting container id: 0d8341b2270e image: docker.io/testcontainers/ryuk:0.3.4
2023/04/15 16:19:10 Waiting for container id 0d8341b2270e image: docker.io/testcontainers/ryuk:0.3.4
2023/04/15 16:19:10 Container is ready id: 0d8341b2270e image: docker.io/testcontainers/ryuk:0.3.4
2023/04/15 16:19:28 Starting container id: 999cf02b5a82 image: redis:latest
2023/04/15 16:19:30 Waiting for container id 999cf02b5a82 image: redis:latest
2023/04/15 16:19:30 Container is ready id: 999cf02b5a82 image: redis:latest
PASS
ok      demo    73.262s
</code></pre>
<p>我们看到建立这种真实版的“fake object”的一大不足就是依赖网络下载container image且耗时过长，在单元测试阶段使用还是要谨慎一些。testcontainer更多也会被用在集成测试或冒烟测试上。</p>
<p>一些开源项目，比如etcd，也提供了<a href="https://github.com/etcd-io/etcd/blob/main/tests/integration/embed">用于测试的自身简化版的实现(embed)</a>。这一点也值得我们效仿，在团队内部每个服务的开发者如果都能提供一个服务的简化版实现，那么对于该服务调用者来说，它的单测就会变得十分容易。</p>
<h2>5. 参考资料</h2>
<ul>
<li>《xUnit Test Patterns : Refactoring Test Code》- https://book.douban.com/subject/1859393/</li>
<li>Test Double Patterns &#8211; http://xunitpatterns.com/Test%20Double%20Patterns.html</li>
<li>The Unit in Unit Testing &#8211; https://www.infoq.com/articles/unit-testing-approach/</li>
<li>Test Doubles — Fakes, Mocks and Stubs &#8211; https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da</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/04/20/provide-fake-object-for-external-collaborators/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Docker Compose构建一键启动的运行环境</title>
		<link>https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose/</link>
		<comments>https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose/#comments</comments>
		<pubDate>Fri, 26 Nov 2021 13:08:10 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-compose]]></category>
		<category><![CDATA[docker-compose.yml]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[jaeger]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kafka]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[nacos]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[profiling]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[pyroscope]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[swarm]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[yaml]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[微服务]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3345</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose 如今，不管你是否喜欢，不管你是否承认，微服务架构模式的流行就摆在那里。作为架构师的你，如果再将系统设计成个大单体结构，那么即便不懂技术的领导，都会给你送上几次白眼。好吧，妥协了！开拆！“没吃过猪肉，还没见过猪跑吗！”。拆不出40-50个服务，我就不信还拆不出4-5个服务^_^。 终于拆出了几个服务，但又犯难了：以前单体程序，搭建一个运行环境十分easy，程序往一个主机上一扔，配置配置，启动就ok了；但自从拆成服务后，开发人员的调试环境、集成环境、测试环境等搭建就变得异常困难。 有人会说，现在都云原生了？你不知道云原生操作系统k8s的存在么？让运维帮你在k8s上整环境啊。 一般小厂，运维人员不多且很忙，开发人员只能“自力更生，丰衣足食”。开发人员自己整k8s？别扯了！没看到这两年k8s变得越来越复杂了吗！如果有一年不紧跟k8s的演进，新版本中的概念你就可能很陌生，不知源自何方。一般开发人员根本搞不定(如果你想搞定，可以看看我的k8s实战课程哦，包教包会^_^)。 那怎么办呢？角落里曾经的没落云原生贵族docker发话了：要不让我兄弟试试！ 1. docker compose docker虽然成了“过气网红”，但docker依然是容器界的主流。至少对于非docker界的开发人员来说，一提到容器，大家首先想到的还是docker。 docker公司的产品推出不少，开发人员对多数都不买账也是现实，但我们也不能一棒子打死，毕竟docker是可用的，还有一个可用的，那就是docker的兄弟：docker compose。 Compose是一个用于定义和运行多容器Docker应用程序的工具。使用Compose，我们可以使用一个YAML文件来配置应用程序的所有服务组件。然后，只需一条命令，我们就可以创建并启动配置中的所有服务。 这不正是我们想要的工具么! Compose与k8s很像，都算是容器编排工具，最大的不同：Compose更适合在单节点上的调试或集成环境中（虽然也支持跨主机，基于被淘汰的docker swarm)。Compose可以大幅提升开发人员以及测试人员搭建应用运行环境的效率。 2. 选版本 使用docker compose搭建运行环境，我们仅需一个yml文件。但docker compose工具也经历了多年演化，这个文件的语法规范也有多个版本，截至目前，docker compose的配置文件的语法版本就有2、2.x和3.x三种。并且不同规范版本支持的docker引擎版本还不同，这个对应关系如下图。图来自docker compose文件规范页面： 选版本是最闹心的。选哪个呢？设定两个条件： docker引擎版本怎么也得是17.xx 规范版本怎么也得是3.x吧 这样一来，版本3.2是最低要求的了。我们就选3.2： // docker-compose.yml version: "3.2" 3. 选网络 docker compose默认会为docker-compose.yml中的各个service创建一个bridge网络，所有service在这个网络里可以相互访问。以下面docker-compose.yml为例： // demo1/docker-compose.yml version: "3.2" services: srv1: image: nginx:latest container_name: srv1 srv2: image: nginx:latest container_name: srv2 启动这个yml中的服务： # docker-compose [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/build-all-in-one-runtime-environment-with-docker-compose-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose">本文永久链接</a> &#8211; https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose</p>
<p>如今，不管你是否喜欢，不管你是否承认，微服务架构模式的流行就摆在那里。作为架构师的你，如果再将系统设计成个大单体结构，那么即便不懂技术的领导，都会给你送上几次白眼。好吧，妥协了！开拆！“没吃过猪肉，还没见过猪跑吗！”。拆不出40-50个服务，我就不信还拆不出4-5个服务^_^。</p>
<p>终于拆出了几个服务，但又犯难了：以前单体程序，搭建一个运行环境十分easy，程序往一个主机上一扔，配置配置，启动就ok了；但自从拆成服务后，开发人员的调试环境、集成环境、测试环境等搭建就变得异常困难。</p>
<p>有人会说，现在都云原生了？你不知道<a href="https://kubernetes.io">云原生操作系统k8s</a>的存在么？让运维帮你在k8s上整环境啊。 一般小厂，运维人员不多且很忙，开发人员只能“自力更生，丰衣足食”。开发人员自己整k8s？别扯了！没看到这两年k8s变得越来越复杂了吗！如果有一年不紧跟k8s的演进，新版本中的概念你就可能很陌生，不知源自何方。一般开发人员根本搞不定(如果你想搞定，可以看看<a href="https://coding.imooc.com/class/284.html">我的k8s实战课程</a>哦，包教包会^_^)。</p>
<p>那怎么办呢？角落里<strong>曾经的没落云原生贵族docker</strong>发话了：要不让我兄弟试试！</p>
<h3>1. docker compose</h3>
<p><a href="https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building">docker</a>虽然成了“过气网红”，但docker依然是容器界的主流。至少对于非docker界的开发人员来说，一提到容器，大家首先想到的还是docker。</p>
<p>docker公司的产品推出不少，开发人员对多数都不买账也是现实，但我们也不能一棒子打死，毕竟docker是可用的，还有一个可用的，那就是docker的兄弟：<a href="https://docs.docker.com/compose/">docker compose</a>。</p>
<p>Compose是一个用于定义和运行多容器Docker应用程序的工具。使用Compose，我们可以使用一个<a href="https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/">YAML文件</a>来配置应用程序的所有服务组件。然后，只需一条命令，我们就可以创建并启动配置中的所有服务。</p>
<p><strong>这不正是我们想要的工具么</strong>! Compose与k8s很像，都算是容器编排工具，最大的不同：Compose更适合在单节点上的调试或集成环境中（虽然也支持跨主机，基于被淘汰的docker swarm)。Compose可以大幅提升开发人员以及测试人员搭建应用运行环境的效率。</p>
<h3>2. 选版本</h3>
<p>使用docker compose搭建运行环境，我们仅需一个yml文件。但docker compose工具也经历了多年演化，这个文件的语法规范也有多个版本，截至目前，docker compose的配置文件的语法版本就有2、2.x和3.x三种。并且不同规范版本支持的docker引擎版本还不同，这个对应关系如下图。图来自<a href="https://docs.docker.com/compose/compose-file/">docker compose文件规范页面</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/build-all-in-one-runtime-environment-with-docker-compose-2.png" alt="" /></p>
<p>选版本是最闹心的。选哪个呢？设定两个条件：</p>
<ul>
<li>docker引擎版本怎么也得是17.xx</li>
<li>规范版本怎么也得是3.x吧</li>
</ul>
<p>这样一来，版本3.2是最低要求的了。我们就选3.2：</p>
<pre><code>// docker-compose.yml
version: "3.2"
</code></pre>
<h3>3. 选网络</h3>
<p>docker compose默认会为docker-compose.yml中的各个service创建一个bridge网络，所有service在这个网络里可以相互访问。以下面docker-compose.yml为例：</p>
<pre><code>// demo1/docker-compose.yml
version: "3.2"
services:
  srv1:
    image: nginx:latest
    container_name: srv1
  srv2:
    image: nginx:latest
    container_name: srv2
</code></pre>
<p>启动这个yml中的服务：</p>
<pre><code># docker-compose -f docker-compose.yml up -d
Creating network "demo1_default" with the default driver
... ...
</code></pre>
<p>docker compose会为这组容器创建一个名为demo1_default的桥接网络:</p>
<pre><code># docker network ls
NETWORK ID          NAME                     DRIVER              SCOPE
f9a6ac1af020        bridge                   bridge              local
7099c68b39ec        demo1_default            bridge              local
... ...
</code></pre>
<p>关于demo1_default网络的细节，可以通过docker network inspect 7099c68b39ec获得。</p>
<p>对于这样的网络中的服务，我们在外部是无法访问的。如果要访问其中服务，我们需要对其中的服务做端口映射，比如如果我们要将srv1暴露到外部，我们可以将srv1监听的服务端口80映射到主机上的某个端口，这里用8080，修改后的docker-compose.yml如下：</p>
<pre><code>version: "3.2"
services:
  srv1:
    image: nginx:latest
    container_name: srv1
    ports:
    - "8080:80"
  srv2:
    image: nginx:latest
    container_name: srv2
</code></pre>
<p>这样启动该组容器后，我们通过curl localhost:8080就可以访问到容器中的srv1服务。不过这种情况下，服务间的相互发现比较麻烦，要么借助于外部的发现服务，要么通过容器间的link来做。</p>
<p>开发人员大多只有一个环境，不同服务的服务端口亦不相同，让容器使用host网络要比单独创建一个bridge网络来的更加方便。通过network_mode我们可以指定服务使用host网络，就像下面这样：</p>
<pre><code>version: "3.2"
services:
  srv1:
    image: bigwhite/srv1:1.0.0
    container_name: srv1
    network_mode: "host"
</code></pre>
<p>在host网络下，容器监听的端口就是主机上的端口，各个服务间通过端口区别各个服务实例(前提是端口各不相同)，ip使用localhost即可。</p>
<p>使用host网络还有一个好处，那就是我们在该环境之外的主机上访问环境中的服务也十分方便，比如查看prometheus的面板等。</p>
<h3>4. 依赖的中间件先启动，预置配置次之</h3>
<p>如今的微服务架构系统，除了自身实现的服务外，外围还有大量其依赖的中间件，比如：redis、kafka(mq)、nacos/etcd(服务发现与注册）、prometheus(时序度量数据服务)、mysql(关系型数据库)、jaeger server(trace服务器)、elastic(日志中心)、pyroscope-server(持续profiling服务)等。</p>
<p>这些中间件若没有启动成功，我们自己的服务多半启动都要失败，因此我们要保证这些中间件服务都启动成功后，再来启动我们自己的服务。</p>
<p>如何做呢？compose规范中有一个<a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#depends_on">迷惑人的“depends_on”</a>，比如下面配置文件中srv1依赖redis和nacos两个service：</p>
<pre><code>version: "3.2"
services:
  srv1:
    image: bigwhite/srv1:1.0.0
    container_name: srv1
    network_mode: "host"
    depends_on:
      - "redis"
      - "nacos"
    environment:
      - NACOS_SERVICE_ADDR=127.0.0.1:8848
      - REDIS_SERVICE_ADDR=127.0.0.1:6379
    restart: on-failure
</code></pre>
<p>不深入了解，很多人会认为depends_on可以保证先启动依赖项redis和nacos，并等依赖项ready后再启动我们自己的服务srv1。但实际上，depends_on仅能保证先启动依赖项，后启动我们的服务。但它不会探测依赖项redis或nacos是否ready，也不会等依赖项ready后，才启动我们的服务。于是你会看到srv1启动后依旧出现各种的报错，包括无法与redis、nacos建立连接等。</p>
<p>要想真正实现依赖项ready后才启动我们自己的服务，我们需要借助外部工具了，<a href="https://docs.docker.com/compose/startup-order/">docker compose文档对此有说明</a>。其中一个方法是使用<a href="https://github.com/vishnubob/wait-for-it">wait-for-it脚本</a>。</p>
<p>我们可以改变一下自由服务的容器镜像，将其entrypoint从执行服务的可执行文件变为执行一个start.sh的脚本：</p>
<pre><code>// Dockerfile
... ...
ENTRYPOINT ["/bin/bash", "./start.sh"]

</code></pre>
<p>这样我们就可以在start.sh脚本中“定制”我们的启动逻辑了。下面是一个start.sh脚本的示例：</p>
<pre><code>#! /bin/sh

./wait_for_it.sh $NACOS_SERVICE_ADDR -t 60 --strict -- echo "nacos is up" &amp;&amp; \
./wait_for_it.sh $REDIS_SERVICE_ADDR -- echo "redis is up" &amp;&amp; \
exec ./srv1
</code></pre>
<p>我们看到，在start.sh脚本中，我们使用<a href="https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh">wait_for_it.sh脚本</a>等待nacos和redis启动，如果在限定时间内等待失败，根据restart策略，我们的服务还会被docker compose重新拉起，直到nacos与redis都ready，我们的服务才会真正开始执行启动过程。</p>
<p>在exec ./srv1之前，很多时候我们还需要进行一些配置初始化操作，比如向nacos中写入预置的srv1服务的配置文件内容以保证srv1启动后能从nacos中读取到自己的配置文件，下面是加了配置初始化的start.sh：</p>
<pre><code>#! /bin/sh

./wait_for_it.sh $NACOS_SERVICE_ADDR -t 60 --strict -- echo "nacos is up" &amp;&amp; \
./wait_for_it.sh $REDIS_SERVICE_ADDR -- echo "redis is up" &amp;&amp; \
curl -X POST --header 'Content-Type: application/x-www-form-urlencoded' -d dataId=srv1.yml --data-urlencode content@./conf/srv1.yml "http://127.0.0.1:8848/nacos/v1/cs/configs?group=MY_GROUP" &amp;&amp; \
exec ./srv1
</code></pre>
<p>我们通过curl将打入镜像的./conf/srv1.yml配置写入已经启动了的nacos中供后续srv1启动时读取。</p>
<h3>5. 全家桶，一应俱全</h3>
<p>就像前面提到的，如今的系统对外部的中间件“依存度”很高，好在主流中间件都提供了基于docker启动的官方支持。这样我们的开发环境也可以是一个一应俱全的“全家桶”。不过要有一个很容易满足的前提：你的机器配置足够高，才能把这些中间件全部运行起来。</p>
<p>有了这些全家桶，我们无论是诊断问题(看log、看trace、看度量数据），还是作性能优化（看持续profiling的数据），都方便的不要不要的。</p>
<h3>6. 结合Makefile，简化命令行输入</h3>
<p>docker-compose这个工具有一个“严重缺陷”，那就是名字太长^_^。这导致我们每次操作都要敲入很多命令字符，当你使用的compose配置文件名字不为docker-compose.yml时，更是如此，我们还需要通过-f选项指定配置文件路径。</p>
<p>为了简化命令行输入，减少键盘敲击次数，我们可以将复杂的docker-compose命令与Makefile相结合，通过定制命令行命令并将其赋予简单的make target名字来实现这一简化目标，比如：</p>
<pre><code>// Makefile

pull:
    docker-compose -f my-docker-compose.yml pull

pull-my-system:
    docker-compose -f my-docker-compose.yml pull srv1 srv2 srv3

up: pull-my-system
    docker-compose -f my-docker-compose.yml up

upd: pull-my-system
    docker-compose -f my-docker-compose.yml up -d

up2log: pull-my-system
    docker-compose -f my-docker-compose.yml up &gt; up.log 2&gt;&amp;1

down:
    docker-compose -f my-docker-compose.yml down

ps:
    docker-compose -f my-docker-compose.yml ps -a

log:
    docker-compose -f my-docker-compose.yml logs -f

# usage example: make upsrv service=srv1
service=
upsrv:
    docker-compose -f my-docker-compose.yml up -d ${service}

config:
    docker-compose -f my-docker-compose.yml config
</code></pre>
<p>另外服务依赖的中间件一般都时启动与运行开销较大的系统，每次和我们的服务一起启停十分浪费时间，我们可以将这些依赖与我们的服务分别放在不同的compose配置文件中管理，这样我们每次重启自己的服务时，没有必要重新启动这些依赖，这样可以节省大量“等待”时间。</p>
<h3>7. .env文件</h3>
<p>有些时候，我们需要在compose的配置文件中放置一些“变量”，我们通常使用环境变量来实现“变量”的功能，比如：我们将srv1的镜像版本改为一个环境变量：</p>
<pre><code>version: "3.2"
services:
  srv1:
    image: bigwhite/srv1:${SRV1_VER}
    container_name: srv1
    network_mode: "host"
  ... ...
</code></pre>
<p>docker compose支持通过同路径下的.env文件的方式docker-compose.yml中环境变量的值，比如：</p>
<pre><code>// .env
SRV1_VER=dev
</code></pre>
<p>这样docker compose在启动srv1时会将.env中SRV1_VER的值读取出来并替换掉compose配置文件中的相应环境变量。通过这种方式，我们可以灵活的修改我们使用的镜像版本。</p>
<h3>8. 优点与不足</h3>
<p>使用docker compose工具，我们可以轻松拥有并快速启动一个all-in-one的运行环境，大幅度加速了部署、调试与测试的效率，在特定的工程环节，它可以给予开发与测试人员很大帮助。</p>
<p>不过这样的运行环境也有一些不足，比如：</p>
<ul>
<li>对部署的机器/虚拟机配置要求较高；</li>
<li>这样的运行环境有局限，用在功能测试、持续集成、验收测试的场景下可以，但不能用来执行压测或者说即便压测也只是摸底，数据不算数的，因为所有服务放在一起，相互干扰；</li>
<li>服务或中间件多了以后，完全启动一次也要耐心等待一段时间。</li>
</ul>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.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>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
