<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; 内存</title>
	<atom:link href="http://tonybai.com/tag/%e5%86%85%e5%ad%98/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 的“简单”幻象：易于上手，难于精通</title>
		<link>https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master/</link>
		<comments>https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master/#comments</comments>
		<pubDate>Fri, 07 Nov 2025 06:28:23 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[append]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Channelmisuse]]></category>
		<category><![CDATA[cloudnativeapplications]]></category>
		<category><![CDATA[Concurrency]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[coreconcepts]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[do-while]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[fanin]]></category>
		<category><![CDATA[fanout]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[foreach]]></category>
		<category><![CDATA[GeekTime]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[Goexpert]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoLanguageAdvancedCourse]]></category>
		<category><![CDATA[GoLanguageFirstCourse]]></category>
		<category><![CDATA[Gophilosophy]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[GoroutineLeak]]></category>
		<category><![CDATA[GoSkilledWorker]]></category>
		<category><![CDATA[httpserver]]></category>
		<category><![CDATA[iferrnil]]></category>
		<category><![CDATA[implicitdependency]]></category>
		<category><![CDATA[Interfaces]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[keywords]]></category>
		<category><![CDATA[LLMassistedcoding]]></category>
		<category><![CDATA[memorymodel]]></category>
		<category><![CDATA[monkeypatching]]></category>
		<category><![CDATA[net/http]]></category>
		<category><![CDATA[newbook]]></category>
		<category><![CDATA[nilinterface]]></category>
		<category><![CDATA[operationalcomplexity]]></category>
		<category><![CDATA[Orchestrator]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[pathtomastery]]></category>
		<category><![CDATA[productionsystem]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[race]]></category>
		<category><![CDATA[RaceConditions]]></category>
		<category><![CDATA[racedetector]]></category>
		<category><![CDATA[redditgolangforum]]></category>
		<category><![CDATA[rollyourown]]></category>
		<category><![CDATA[slices]]></category>
		<category><![CDATA[SoftwareEngineering]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[standardlibrarysourcecode]]></category>
		<category><![CDATA[sync.Mutex]]></category>
		<category><![CDATA[synchronizationprimitives]]></category>
		<category><![CDATA[TonyBai]]></category>
		<category><![CDATA[unbufferedchannel]]></category>
		<category><![CDATA[underlyingbehavior]]></category>
		<category><![CDATA[while]]></category>
		<category><![CDATA[互斥锁]]></category>
		<category><![CDATA[共享底层数组]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[减法哲学]]></category>
		<category><![CDATA[可靠]]></category>
		<category><![CDATA[团队]]></category>
		<category><![CDATA[基础设施]]></category>
		<category><![CDATA[外部系统]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[密码学工具]]></category>
		<category><![CDATA[并发编程]]></category>
		<category><![CDATA[幻象破灭]]></category>
		<category><![CDATA[异常]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[性能下降]]></category>
		<category><![CDATA[所见即所得]]></category>
		<category><![CDATA[易于上手]]></category>
		<category><![CDATA[易于维护]]></category>
		<category><![CDATA[易于阅读]]></category>
		<category><![CDATA[极简语法]]></category>
		<category><![CDATA[死锁]]></category>
		<category><![CDATA[画图]]></category>
		<category><![CDATA[第三方库]]></category>
		<category><![CDATA[简单幻象]]></category>
		<category><![CDATA[简单性]]></category>
		<category><![CDATA[精通]]></category>
		<category><![CDATA[线程]]></category>
		<category><![CDATA[继承体系]]></category>
		<category><![CDATA[设计]]></category>
		<category><![CDATA[调试]]></category>
		<category><![CDATA[运行时]]></category>
		<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=5362</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master 大家好，我是Tony Bai。 “Go 语言看起来如此简单，我的这种假设是错的吗？” 近日，一位刚接触 Go 几个月的新手在reddit golang论坛发出了这样一个真诚的提问。他感觉 Go “超级简单”，并好奇自己是否因为初学者的身份，而忽略了语言中那些“疯狂的复杂性”。 这个问题，立刻引发了社区关注。数百条评论从四面八方涌来，汇成了一场关于 Go 语言简单性本质的深度辩论。最终，社区的集体智慧凝聚成一个经典而又充满辩证性的共识：Go 的简单，是刻意为之的设计；而通往精通之路，则隐藏在简约表象之下的深邃之处。 本文将带你深入探索这座“简单”的冰山，从其光彩照人的水上部分，一直潜入其复杂深邃的水下世界。 “蜜月期”——为什么 Go 语言感觉如此简单？ 对于初学者而言，Go 带来的“简单”感受是真实且强烈的。这并非巧合，而是源于 Go 设计者们一系列深思熟虑的“减法”哲学。 极简的语法与关键字 “25 个关键字，宝贝！” 一位评论者这样感叹道。Go 有意地限制了语言的表面积，仅保留了构建大型系统所必需的核心元素。它只有一个循环结构 for，没有 while、do-while 或 foreach 的变体。这种极简主义，让学习者可以快速掌握语言的全貌，而不必记忆大量特殊语法。 “所见即所得”的代码 一位来自 Java/Python 背景的开发者分享道：“Go 给你的玩具可能更少，但至少你可以相信，它们不会在调试时反咬你一口。” Go 缺乏猴子补丁 (monkey patching)、复杂的继承体系和隐式的魔法，这意味着代码的行为更加可预测。“代码读起来就像它实际运行的样子，即便这意味着多写几行。” “电池自带”的强大标准库 “标准库太棒了，” 社区普遍赞同，“你需要花些时间才能理解，在不引入单个依赖的情况下，你能做多少事情。” 从 HTTP 服务器到密码学工具，Go 的标准库提供了构建现代网络服务所需 90% 的功能，让初学者可以立即开始构建有价值的应用，而无需在茫茫的第三方库中选择和配置。 幻象的破灭——“简单”背后的隐藏复杂性 当“蜜月期”结束，开发者开始构建更复杂的真实世界系统时，Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-simple-illusion-easy-to-learn-hard-to-master-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master">本文永久链接</a> &#8211; https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master</p>
<p>大家好，我是Tony Bai。</p>
<p>“Go 语言看起来如此简单，我的这种假设是错的吗？”</p>
<p>近日，一位刚接触 Go 几个月的新手在reddit golang论坛发出了这样<a href="https://www.reddit.com/r/golang/comments/1oj9jb6/golang_seems_so_simple_am_i_wrong_to_assume_that/">一个真诚的提问</a>。他感觉 Go “超级简单”，并好奇自己是否因为初学者的身份，而忽略了语言中那些“疯狂的复杂性”。</p>
<p>这个问题，立刻引发了社区关注。数百条评论从四面八方涌来，汇成了一场关于 Go 语言简单性本质的深度辩论。最终，社区的集体智慧凝聚成一个经典而又充满辩证性的共识：<strong>Go 的简单，是刻意为之的设计；而通往精通之路，则隐藏在简约表象之下的深邃之处。</strong></p>
<p>本文将带你深入探索这座“简单”的冰山，从其光彩照人的水上部分，一直潜入其复杂深邃的水下世界。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>“蜜月期”——为什么 Go 语言感觉如此简单？</h2>
<p>对于初学者而言，Go 带来的“简单”感受是真实且强烈的。这并非巧合，而是源于 Go 设计者们一系列深思熟虑的“减法”哲学。</p>
<h3>极简的语法与关键字</h3>
<p>“25 个关键字，宝贝！” 一位评论者这样感叹道。Go 有意地限制了语言的表面积，仅保留了构建大型系统所必需的核心元素。它只有一个循环结构 for，没有 while、do-while 或 foreach 的变体。这种极简主义，让学习者可以快速掌握语言的全貌，而不必记忆大量特殊语法。</p>
<h3>“所见即所得”的代码</h3>
<p>一位来自 Java/Python 背景的开发者分享道：“Go 给你的玩具可能更少，但至少你可以相信，它们不会在调试时反咬你一口。” Go 缺乏猴子补丁 (monkey patching)、复杂的继承体系和隐式的魔法，这意味着代码的行为更加可预测。“代码读起来就像它实际运行的样子，即便这意味着多写几行。”</p>
<h3>“电池自带”的强大标准库</h3>
<p>“标准库太棒了，” 社区普遍赞同，“你需要花些时间才能理解，在不引入单个依赖的情况下，你能做多少事情。” 从 HTTP 服务器到密码学工具，Go 的标准库提供了构建现代网络服务所需 90% 的功能，让初学者可以立即开始构建有价值的应用，而无需在茫茫的第三方库中选择和配置。</p>
<h2>幻象的破灭——“简单”背后的隐藏复杂性</h2>
<p>当“蜜月期”结束，开发者开始构建更复杂的真实世界系统时，Go 的另一面便会逐渐显现。这份复杂性，并非来自语言本身，而是源于 Go 为了维持简单性，而将复杂性“转移”到的地方。</p>
<h3>并发：Go 的“光荣与荆棘”</h3>
<p>这是社区中被提及次数最多的“深水区”。Go 通过 goroutine 和 channel，将并发编程的门槛降到了前所未有的低度。然而，这种易用性也隐藏着巨大的风险。</p>
<blockquote>
<p>“理解并发作为一个概念可能会很复杂，但 Go 让实现它变得简单。”</p>
</blockquote>
<p>但“实现简单”不等于“用对简单”。</p>
<ul>
<li><strong>Goroutine 泄露</strong>：新手很容易创建出无人“负责”的 goroutine，导致其在后台永久运行，悄无声息地消耗内存和 CPU。</li>
<li><strong>竞态条件 (Race Conditions)</strong>：尽管 Go 提供了强大的竞态检测器 (-race)，但理解和避免数据竞争，需要对内存模型和同步原语（如 sync.Mutex）有深刻的理解。</li>
<li><strong>Channel 的滥用</strong>：“我数不清有多少次，人们到处使用 goroutine 和 channel，然后好奇为什么他们的项目变得如此之慢。” Channel 是强大的工具，但错误地使用无缓冲 channel、忘记关闭 channel、或用它来解决本该用互斥锁解决的问题，都会导致死锁、性能下降和难以调试的 bug。</li>
</ul>
<p><strong>精通并发，是区分 Go 新手与专家的第一道分水岭。</strong></p>
<h3>运维复杂性</h3>
<p>Go 的设计哲学，在某些方面将应用程序的韧性责任，从语言运行时“推”给了基础设施。这为 Go 程序带来了一种独特的<strong>运维复杂性</strong>。</p>
<p>最典型的例子就是 <strong>panic 的处理</strong>。</p>
<ul>
<li>在某些语言中（如 Java），一个未捕获的异常通常只会导致单个线程死亡，而整个应用程序进程会默认继续运行。</li>
<li>但在 Go 中，一个未被 recover 的 panic 会导致<strong>整个程序（进程）立即崩溃退出</strong>。Go 语言本身不提供自动重启或进程守护的能力，它将这种“灾难恢复”的职责，明确地交给了程序的运行环境。</li>
</ul>
<p>这意味着，构建一个高可用的 Go 服务，你<strong>必须</strong>依赖外部系统。正如一位资深开发者在讨论中指出的那样：</p>
<blockquote>
<p>“像 panic 这样的东西，要求你在一个编排器（如 K8s/ECS 等）下运行你的生产系统。”</p>
</blockquote>
<p>这种设计选择，对于新手来说可能是一个认知上的巨大跳跃。他们必须明白，Go 程序的健壮性，并不仅仅是代码层面的 if err != nil，更是在<strong>基础设施层面</strong>，通过配置进程管理器（如 systemd）或容器编排器（如 Kubernetes）的健康检查和自动重启策略来共同保证的。</p>
<p>Go 将自己定位为一个用于构建云原生应用的“零件”，而非一个大包大揽的“一体机”。这种对运维环境的<strong>隐性依赖</strong>，正是其简单性背后的一种深刻权衡。</p>
<h3>“魔鬼在细节中”：切片、接口与错误处理</h3>
<p>Go 的一些核心特性，虽然表面简单，但其底层机制却充满了需要深入理解的“微妙之处”。</p>
<ul>
<li><strong>切片 (Slices)</strong>：新手常常会对其“共享底层数组”的行为感到困惑，不经意间写出因 append 操作导致意外数据修改的 bug。</li>
<li><strong>接口 (Interfaces)</strong>：nil 接口与“值为 nil 的接口”之间的区别，是无数 Gopher 都曾踩过的经典“坑”。</li>
<li><strong>错误处理的冗长</strong>：if err != nil 虽然明确，但在 LLM 辅助编码时代到来之前，这种冗长曾是许多开发者的抱怨之源。现在，新的挑战变成了如何确保依赖 AI 的新手，能真正理解他们生成的每一行错误处理代码。</li>
</ul>
<h2>精通之路——从“知道”到“理解”</h2>
<p>那么，如何跨越从“简单”到“精通”的鸿沟？社区的智慧为我们指明了方向。</p>
<h3>接受 Go 的哲学</h3>
<p>Go 是一门<strong>“刻意设计的简单语言”</strong>。它的目标，是让大型团队能够编写出风格统一、易于阅读和维护的代码。这意味着，你需要接受它的“冗长”，理解它为何抵制某些“高级”特性，并学会在其提供的“约束”下优雅地解决问题。</p>
<h3>刻意练习核心概念</h3>
<p>不要满足于 API 的表面用法。花时间去：</p>
<ul>
<li><strong>画图理解并发模式</strong>：亲自绘制 goroutine 如何通过 channel 通信，理解扇入 (fan-in)、扇出 (fan-out) 等模式。</li>
<li><strong>实验切片的底层行为</strong>：编写小程序来观察 append 何时会触发底层数组的重新分配。</li>
<li><strong>深入标准库源码</strong>：阅读 net/http 或 context 包的源码，是理解 Go 设计哲学的最佳途径。</li>
</ul>
<h3>拥抱“造轮子”</h3>
<p>“你经常需要‘自己动手造轮子’(roll your own)”，一位开发者评论道。这在 Go 的世界里并非贬义。Go 强大的标准库为你提供了高质量的“零件”，鼓励你根据自己的具体需求，组合出最适合的“轮子”，而不是像其他生态那样，总是先去寻找一个庞大、臃肿的“现成汽车”。</p>
<h2>小结：“简单”是起点，而非终点</h2>
<p>回到最初的问题：Go 语言真的简单吗？</p>
<p><strong>是的，Go 的入口极其简单。</strong> 它拥有平缓的学习曲线，让有经验的程序员可以在一周内上手，让新手也能在短时间内构建出有用的程序。</p>
<p><strong>但精通 Go 绝不简单。</strong> 它的真正深度，不在于复杂的语法，而在于理解其并发模型背后的权衡、标准库设计的精妙、以及在简约哲学约束下构建复杂系统的工程智慧。</p>
<p>正如一位评论者所引用的那句古老格言：“<strong>一分钟学会，一辈子精通。</strong>” 虽说“一辈子”有些夸张，但这或许是对 Go 语言简单性与复杂性辩证关系的最佳诠释。Go 的“简单”，为你打开了一扇通往高效、可靠软件工程的大门，但门后的风景，需要你用持续的学习和深刻的思考，去亲自探索和领悟。</p>
<p>资料链接：https://www.reddit.com/r/golang/comments/1oj9jb6/golang_seems_so_simple_am_i_wrong_to_assume_that/</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/11/07/go-simple-illusion-easy-to-learn-hard-to-master/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 官方详解“Green Tea”垃圾回收器：从对象到页，一场应对现代硬件挑战的架构演进</title>
		<link>https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/</link>
		<comments>https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/#comments</comments>
		<pubDate>Thu, 30 Oct 2025 23:08:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AVX512]]></category>
		<category><![CDATA[CherryMui]]></category>
		<category><![CDATA[CPUProfile]]></category>
		<category><![CDATA[CPU成本]]></category>
		<category><![CDATA[CPU时间]]></category>
		<category><![CDATA[CPU核心]]></category>
		<category><![CDATA[DavidChase]]></category>
		<category><![CDATA[GaliosField]]></category>
		<category><![CDATA[GitHubissue]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[go1.26]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GopherCon2025]]></category>
		<category><![CDATA[Go团队]]></category>
		<category><![CDATA[GraphFlood]]></category>
		<category><![CDATA[greentea]]></category>
		<category><![CDATA[GreenTeaGC]]></category>
		<category><![CDATA[Intel]]></category>
		<category><![CDATA[issue]]></category>
		<category><![CDATA[jsonv2]]></category>
		<category><![CDATA[KeithRandall]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[MarkSweepAlgorithm]]></category>
		<category><![CDATA[MichaelKnyszek]]></category>
		<category><![CDATA[MichaelPratt]]></category>
		<category><![CDATA[MicroarchitecturalDisaster]]></category>
		<category><![CDATA[nogreenteagc]]></category>
		<category><![CDATA[NUMA]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[SIMDAccelerationPackage]]></category>
		<category><![CDATA[swisstable]]></category>
		<category><![CDATA[tip版本]]></category>
		<category><![CDATA[TracingGarbageCollection]]></category>
		<category><![CDATA[VGF2P8AFFINEQB]]></category>
		<category><![CDATA[WorkList]]></category>
		<category><![CDATA[x86硬件]]></category>
		<category><![CDATA[YvesVandriessche]]></category>
		<category><![CDATA[不可达]]></category>
		<category><![CDATA[主内存]]></category>
		<category><![CDATA[代码生成器]]></category>
		<category><![CDATA[仿射变换]]></category>
		<category><![CDATA[位]]></category>
		<category><![CDATA[位运算]]></category>
		<category><![CDATA[元数据]]></category>
		<category><![CDATA[先进先出]]></category>
		<category><![CDATA[全局变量]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[内存带宽]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[原型]]></category>
		<category><![CDATA[反馈]]></category>
		<category><![CDATA[后进先出]]></category>
		<category><![CDATA[向量加速]]></category>
		<category><![CDATA[向量增强]]></category>
		<category><![CDATA[向量指令]]></category>
		<category><![CDATA[向量硬件]]></category>
		<category><![CDATA[图]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[垃圾回收器]]></category>
		<category><![CDATA[垃圾回收成本]]></category>
		<category><![CDATA[基准测试]]></category>
		<category><![CDATA[堆]]></category>
		<category><![CDATA[堆内存]]></category>
		<category><![CDATA[堆结构]]></category>
		<category><![CDATA[实验性]]></category>
		<category><![CDATA[寄存器]]></category>
		<category><![CDATA[对象]]></category>
		<category><![CDATA[局部变量]]></category>
		<category><![CDATA[工作负载]]></category>
		<category><![CDATA[已扫描位]]></category>
		<category><![CDATA[已见位]]></category>
		<category><![CDATA[已访问]]></category>
		<category><![CDATA[广度优先]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[扫描]]></category>
		<category><![CDATA[技术原理]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[架构演进]]></category>
		<category><![CDATA[标记]]></category>
		<category><![CDATA[标记阶段]]></category>
		<category><![CDATA[根]]></category>
		<category><![CDATA[汇编代码]]></category>
		<category><![CDATA[深度优先]]></category>
		<category><![CDATA[清除]]></category>
		<category><![CDATA[清除阶段]]></category>
		<category><![CDATA[现代硬件]]></category>
		<category><![CDATA[生产环境]]></category>
		<category><![CDATA[空闲槽位]]></category>
		<category><![CDATA[绿茶算法]]></category>
		<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=5335</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc 大家好，我是Tony Bai。 关注 Go 语言演进的 Gopher 们可能已经注意到，Go 团队更换技术负责人以来，对运行时 (runtime) 和编译器 (compiler) 核心组件的打磨正日益成为团队的工作重心。从备受期待的“绿茶”GC (Green Tea GC)，到 标准库simd 加速包的探索，再到 基于swisstable的 map 的实现，以及 json/v2 的设计实现，一系列动作都预示着 Go 正在其性能核心地带进行着深刻的自我革新。 而就在最近，Go 运行时和编译器团队的一项决议，更是将这一趋势推向了高潮：他们计划在 Go 1.26 版本中，将实验性的“绿茶”GC 作为默认的垃圾回收器正式落地。 为了帮助大家深入理解这一重大变更背后的技术原理与深层思考，我翻译了 Go 官方博客10月29日的最新文章《The Green Tea Garbage Collector》。该文是基于 Go 团队核心成员 Michael Knyszek 在 GopherCon 2025 大会上的演讲整理而成。在这篇极具技术深度的原理文章中，没有人能比官方团队的讲解更为专业和权威。因此，为了最大程度地保留其“原汁原味”，我选择以全文翻译的形式，将其最真实、最精确的面貌呈现给大家。 以下是译文全文，供大家参考。 Go 1.25 包含一个名为“绿茶”（Green Tea）的全新实验性垃圾回收器，在构建时通过设置 GOEXPERIMENT=greenteagc 即可启用。使用该垃圾回收器后，许多工作负载在垃圾回收上花费的时间减少了约 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/deep-into-go-green-tea-gc-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc">本文永久链接</a> &#8211; https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc</p>
<p>大家好，我是Tony Bai。</p>
<p>关注 Go 语言演进的 Gopher 们可能已经注意到，<a href="https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/">Go 团队更换技术负责人</a>以来，对运行时 (runtime) 和编译器 (compiler) 核心组件的打磨正日益成为团队的工作重心。从备受期待的<a href="https://tonybai.com/2025/05/03/go-green-tea-garbage-collector/">“绿茶”GC (Green Tea GC)</a>，到 <a href="https://tonybai.com/2025/08/22/go-simd-package-preview">标准库simd 加速包的探索</a>，再到 <a href="https://tonybai.com/2024/11/14/go-map-use-swiss-table/">基于swisstable的 map 的实现</a>，以及 <a href="https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2">json/v2</a> 的设计实现，一系列动作都预示着 Go 正在其性能核心地带进行着深刻的自我革新。</p>
<p>而就在最近，Go 运行时和编译器团队的一项决议，更是将这一趋势推向了高潮：<strong>他们计划在 Go 1.26 版本中，<a href="https://mp.weixin.qq.com/s/pjrnZQym724T5EGuL0a2UQ">将实验性的“绿茶”GC 作为默认的垃圾回收器正式落地</a>。</strong></p>
<p>为了帮助大家深入理解这一重大变更背后的技术原理与深层思考，我翻译了 <a href="https://tonybai.com/wp-content/uploads/2025">Go 官方博客10月29日的最新文章《The Green Tea Garbage Collector》</a>。该文是基于 Go 团队核心成员 Michael Knyszek 在 GopherCon 2025 大会上的演讲整理而成。在这篇极具技术深度的原理文章中，没有人能比官方团队的讲解更为专业和权威。因此，为了最大程度地保留其“原汁原味”，我选择以全文翻译的形式，将其最真实、最精确的面貌呈现给大家。</p>
<p>以下是译文全文，供大家参考。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/the-ultimate-guide-to-go-module-qr.png" alt="" /></p>
<hr />
<p><a href="https://tonybai.com/2025/08/15/some-changes-in-go-1-25">Go 1.25</a> 包含一个名为“绿茶”（Green Tea）的全新实验性垃圾回收器，在构建时通过设置 GOEXPERIMENT=greenteagc 即可启用。使用该垃圾回收器后，许多工作负载在垃圾回收上花费的时间减少了约 10%，而有些工作负载的降幅甚至高达 40%！</p>
<p>它已为生产环境准备就绪，并在 Google 内部投入使用，因此我们鼓励你进行尝试。我们知道某些工作负载的收益不大，<a href="https://www.dolthub.com/blog/2025-09-26-greentea-gc-with-dolt/">甚至完全没有</a>，所以你的反馈对于我们向前推进至关重要。根据我们目前掌握的数据，我们计划<a href="https://mp.weixin.qq.com/s/pjrnZQym724T5EGuL0a2UQ">在 Go 1.26 中将其设为默认GC</a>。</p>
<p>如需报告任何问题，请<a href="https://go.dev/issue/new">提交一个新 issue</a>。</p>
<p>如需分享任何成功经验，请回复至<a href="https://go.dev/issue/73581">现有的 Green Tea issue</a>。</p>
<p>下文是基于 Michael Knyszek 在 GopherCon 2025 上的演讲整理的博文。一旦演讲视频上线，我们将会更新此博文并附上链接。</p>
<h2>追踪垃圾回收过程</h2>
<p>在讨论“绿茶”之前，让我们先就垃圾收集问题达成共识。</p>
<h3>对象和指针</h3>
<p>垃圾回收的目的是自动回收并重用程序不再使用的内存。</p>
<p>为此，Go 垃圾回收器关注的是对象(Object)和指针(Pointer)。</p>
<p>在 Go 运行时的上下文中，对象是Go值(Value)，其底层内存分配自堆。当 Go 编译器无法找到其他方式为某个值分配内存时，就会创建堆对象。例如，以下代码片段会分配一个堆对象：一个指针切片的底层存储空间。</p>
<pre><code>var x = make([]*int, 10) // 全局变量
</code></pre>
<p>Go 编译器只能在堆上分配切片后备存储，因为它很难（甚至可能不可能）知道 x 将引用该对象多长时间。</p>
<p>指针只是一些数字，用于指示 Go 值在内存中的位置，Go 程序通过它们来引用对象。例如，要获取上一个代码片段中分配的对象的起始指针，我们可以这样写：</p>
<pre><code>&amp;x[0] // 0xc000104000
</code></pre>
<h3>标记-清除算法</h3>
<p>Go 的垃圾回收器遵循一种广义上称为“追踪式垃圾回收”的策略，这意味着垃圾回收器会跟随或追踪程序中的指针，以识别程序仍在使用的对象。</p>
<p>更具体地说，Go 垃圾回收器实现了标记-清除(mark-sweep)算法。这比听起来要简单得多。 可以把对象和指针想象成计算机科学意义上的图：<strong>对象是节点，指针是边</strong>。</p>
<p>标记-清除算法就在这个图上运行的，顾名思义，它分两个阶段进行。</p>
<p>在第一阶段，即标记阶段，它从一组明确定义的、称为“根(root)”的源边开始遍历对象图。可以将其理解为<strong>全局变量</strong>和<strong>局部变量</strong>。然后，它将沿途找到的所有东西标记为<strong>已访问(visited)</strong>，以避免循环。这类似于典型的图遍历算法，如深度优先或广度优先搜索。</p>
<p>接下来是清除阶段。在我们的图遍历中未被访问到的任何对象，都是程序<strong>未使用</strong>或<strong>不可达(unreachable)</strong>的。我们称这种状态为<strong>不可达</strong>，因为通过语言的语义，正常的安全 Go 代码已无法再访问那块内存。为完成清除阶段，算法只需遍历所有未访问的节点，并将其内存标记为空闲，以便内存分配器可以重用它们。</p>
<h3>就是这样？</h3>
<p>你可能觉得我在这里把事情想得有点过于简单了。垃圾回收器经常被比作魔法和黑盒子 。你的说法也对了一部分，实际情况要复杂得多。</p>
<p>例如，实际上，这个算法会与你的常规 Go 代码并行执行。遍历一个不断变化的图会带来挑战。我们还对这个算法进行了并行化，这一点稍后会再次提及。</p>
<p>但请相信我，这些细节大多与核心算法无关。核心算法实际上只是一个简单的图泛洪(graph flood)操作。</p>
<h3>图泛洪示例</h3>
<p>我们来看一个例子。请浏览下面的幻灯片图片，跟随步骤操作。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-007.png" alt="" /></p>
<p>这里我们有一个包含一些全局变量和 Go 堆的图示。让我们一步步来分析。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-008.png" alt="" /></p>
<p>左边是我们的根。它们是全局变量 x 和 y。这将是我们图遍历的起点。根据左下角的图例，它们被标记为蓝色，表示它们当前在我们的工作列表上。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-009.png" alt="" /></p>
<p>右边是我们的堆。目前，堆中的所有东西都是灰色的，因为我们还没有访问过任何部分。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-010.png" alt="" /></p>
<p>每个矩形中代表一个对象。每个对象都标有其类型。这个特殊的对象是 T 类型的对象，其类型定义在左上角。它有一个指向子节点数组的指针和一些值。我们可以推断这是一种递归的树形数据结构。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-011.png" alt="" /></p>
<p>除了 T 类型的对象，你还会注意到我们有包含 *T 的数组对象。这些数组对象由 T 类型对象的 “children” 字段指向。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-012.png" alt="" /></p>
<p>矩形内的每个方块代表 8 字节的内存。带有点的方块是一个指针。如果它有箭头，那么它是一个指向某个其他对象的非空指针。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-013.png" alt="" /></p>
<p>如果它没有对应的箭头，那么它就是一个空指针。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-014.png" alt="" /></p>
<p>接下来，这些虚线矩形代表空闲空间，我称之为空闲“槽位(slot)”。我们可以在那里放置一个对象，但目前还没有。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-015.png" alt="" /></p>
<p>你还会注意到对象被这些带标签的、虚线圆角矩形组合在一起。每一个都代表一个页(page)：一块连续的内存块。这些页被标记为 A、B、C 和 D，我将以此来称呼它们。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-015.png" alt="" /></p>
<p>在这个图中，每个对象都被分配到某个页面中。就像实际实现一样，这里的每个页面只包含特定大小的对象。这正是 Go 堆的组织方式。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-016.png" alt="" /></p>
<p>页也是我们组织每个对象元数据的方式。这里你可以看到七个框，每个对应页 A 中的七个对象槽位之一。</p>
<p>每个框代表一位(bit)信息：我们之前是否见过这个对象。实际上，Go运行时就是通过这种方式来管理对象是否已被访问过的，这一点稍后会很重要。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-017.png" alt="" /></p>
<p>细节讲了很多，感谢你跟读。这些稍后都会派上用场。现在，让我们看看图泛洪如何应用于这幅图。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-018.png" alt="" /></p>
<p>我们首先从工作列表中取出一个根。我们将其标记为红色，表示它现在是活跃的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-019.png" alt="" /></p>
<p>沿着根指针，我们找到了一个 T 类型的对象，并将其添加到我们的工作列表。根据图例，我们将该对象绘制成蓝色，以表明它已在工作列表中。请注意，我们同时在右上角的元数据中设置了与此对象对应的“已见”位。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-020.png" alt="" /></p>
<p>下一个根也同样处理。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-021.png" alt="" /></p>
<p>现在我们处理完了所有的根，工作列表上还剩下两个对象。让我们从工作列表中取出一个对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-022.png" alt="" /></p>
<p>我们现在要做的是遍历该对象的指针，以找到更多的对象。顺便说一下，我们称遍历一个对象的指针为“扫描”该对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-023.png" alt="" /></p>
<p>我们找到了这个有效的数组对象…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-024.png" alt="" /></p>
<p>… 并将其添加到我们的工作列表中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-025.png" alt="" /></p>
<p>从这里开始，我们递归地进行。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-026.png" alt="" /></p>
<p>我们遍历数组的指针。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-027.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-028.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-029.png" alt="" /></p>
<p>找到更多对象…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-030.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-031.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-032.png" alt="" /></p>
<p>然后我们遍历数组对象引用的那些对象！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-033.png" alt="" /></p>
<p>请注意，我们仍然需要遍历所有指针，即使它们是 nil。我们事先并不知道它们是否为空。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-034.png" alt="" /></p>
<p>这个分支下还有一个对象…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-035.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-036.png" alt="" /></p>
<p>现在我们到达了另一个分支，从我们早先从某个根找到的页 A 中的那个对象开始。</p>
<p>你可能注意到了我们工作列表的“后进先出”规则，这表明我们的工作列表是一个栈，因此我们的图遍历近似于深度优先。这是有意为之的，并反映了 Go 运行时中实际的图遍历算法。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-037.png" alt="" /></p>
<p>让我们继续…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-038.png" alt="" /></p>
<p>接下来我们找到了另一个数组对象…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-039.png" alt="" /></p>
<p>并遍历它…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-040.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-041.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-042.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-043.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-044.png" alt="" /></p>
<p>我们的工作列表上只剩最后一个对象了…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-045.png" alt="" /></p>
<p>让我们扫描它…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-046.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-047.png" alt="" /></p>
<p>标记阶段完成了！我们没有任何正在处理的工作，工作列表也空了。所有用黑色绘制的对象都是可达的，所有用灰色绘制的对象都是不可达的。让我们一次性清除所有不可达的对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/marksweep-048.png" alt="" /></p>
<p>我们已将那些对象转换为空闲槽位，准备好容纳新的对象。</p>
<h2>问题所在</h2>
<p>经过上面一番摸索，我认为我们已经掌握了 Go 垃圾回收器的实际工作原理。目前看来，这个过程运行良好，那么问题出在哪里呢？</p>
<p>事实证明，在某些程序中，执行这个特定算法会花费大量时间，而且几乎会给所有 Go 程序带来显著的开销。Go 程序将 20% 甚至更多的 CPU 时间用于垃圾回收的情况并不少见。</p>
<p>让我们来分析一下这些时间都花在了哪里。</p>
<h3>垃圾回收成本</h3>
<p>在宏观层面上，垃圾回收器的成本由两部分组成。一是运行频率，二是每次运行所做的工作量。将这两者相乘，就得到了垃圾回收的总成本。</p>
<pre><code>Total GC cost = Number of GC cycles × Average cost per GC cycle

即 总 GC 成本 = GC 周期数 × 每个 GC 周期的平均成本
</code></pre>
<p>多年来，我们一直在研究这个等式中的这两个术语。要了解更多关于垃圾回收器运行频率的信息，请参阅 <a href="https://www.youtube.com/watch?v=07wduWyWx8M">Michael 在 2022 年 GopherCon EU 大会上的关于内存限制的演讲</a>。 <a href="https://go.dev/doc/gc-guide">Go 垃圾回收器的指南</a>也对此主题进行了很多阐述，如果你想深入了解，值得一看。</p>
<p>但现在，我们只关注第二部分，即每个周期的成本。</p>
<p>多年来，我们不断研究 CPU Profile分析结果，试图提高性能，从中我们了解到 Go 的垃圾回收器有两大特点。</p>
<p>第一，大约 90% 的垃圾回收器成本都花在了标记上，只有大约 10% 是在清除。事实证明，清除比标记更容易优化，多年来 Go 已经拥有了一个非常高效的清除器。</p>
<p>第二，在那段用于标记的时间里，有相当大一部分(通常至少有 35%)，都浪费在了访问堆内存上。这本身已经够糟糕了，更糟糕的是，它完全阻碍了现代 CPU 真正高速运行的关键机制。</p>
<h3>“微架构灾难”</h3>
<p>在这种情况下，“堵塞工作机制(gump up the works)”意味着什么？现代 CPU 的具体构造相当复杂，所以我们用一个类比来说明。</p>
<p>想象 CPU 在一条路上行驶，这条路就是你的程序。CPU 想要加速到很高的速度，为此它需要能看清前方的路，并且道路必须畅通。但图遍历算法对 CPU 来说，就像在城市街道里开车。CPU 看不到拐角后的情况，也无法预测接下来会发生什么。为了前进，它必须不断地减速、转弯、在红绿灯前停下、避开行人。你的引擎有多快几乎无关紧要，因为你根本没有机会真正跑起来。</p>
<p>让我们通过再次审视我们的例子来使这一点更具体。我在这里的堆上叠加了我们所走的路径。每个从左到右的箭头代表我们做的一段扫描工作，虚线箭头则显示了我们在不同扫描工作之间是如何跳转的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/graphflood-path.png" alt="" /></p>
<p>上图展示了我们的图泛洪示例中，垃圾回收器在堆中执行的路径。</p>
<p>请注意，我们正在内存中到处跳转，在每个地方只做一点点工作。特别是，我们频繁地在页之间，以及页的不同部分之间跳转。</p>
<p>现代 CPU 做了大量的缓存。访问主内存可能比访问缓存中的内存慢上 100 倍。CPU 缓存中填充的是最近访问过的内存，以及与最近访问过的内存相邻的内存。但是，并不能保证两个相互指向的对象在内存中也彼此靠近。图泛洪算法并没有考虑到这一点。</p>
<p>补充一点：如果我们只是在等待从主内存中获取数据，情况可能还没那么糟。CPU 会异步地发出内存请求，所以即使是慢的请求也可以重叠，只要 CPU 能看得足够远。但在图遍历中，每一小段工作都是不可预测的，并且高度依赖于上一段工作，所以 CPU 被迫几乎在每一次独立的内存获取后都进行等待。</p>
<p>不幸的是，对我们来说，这个问题只会越来越严重。业界有句格言：“等两年，你的代码会变得更快。”</p>
<p>但 Go，作为一个依赖于标记-清除算法的垃圾回收语言，却面临着相反的风险。“等两年，你的代码会变得更慢。” 现代 CPU 硬件的趋势正在给垃圾回收器的性能带来新的挑战：</p>
<ul>
<li>
<p><strong>非一致性内存访问 (Non-uniform memory access)。</strong> 首先，内存现在往往与 CPU 核心的子集相关联。其他 CPU 核心访问该内存的速度比前者慢。换句话说，主内存访问的成本<a href="https://jprahman.substack.com/p/sapphire-rapids-core-to-core-latency">取决于哪个 CPU 核心正在访问它</a> 。这种成本是不一致的，因此我们称之为非一致内存访问，简称 NUMA。</p>
</li>
<li>
<p><strong>内存带宽减少 (Reduced memory bandwidth)。</strong> 每个 CPU 的可用内存带宽随着时间推移呈下降趋势。这意味着虽然我们拥有更多的 CPU 核心，但每个核心能够提交的数据量相对较少。 对主内存的请求导致未缓存的请求等待时间比以前更长。</p>
</li>
<li>
<p><strong>越来越多的 CPU 核心 (Ever more CPU cores)。</strong> 上面，我们看的是一个顺序的标记算法，但真正的垃圾回收器是并行执行此算法的。这在核心数量有限的情况下扩展得很好，但即使经过精心设计，用于扫描的共享对象队列也会成为一个瓶颈。</p>
</li>
<li>
<p><strong>现代硬件特性 (Modern hardware features)。</strong> 新硬件拥有像向量指令这样的酷炫功能，让我们能一次性操作大量数据。虽然这有可能大幅提升速度，但目前还不清楚如何才能实现这一点。因为标记工作包含很多不规则且通常是小块的工作。</p>
</li>
</ul>
<h2>绿茶(Green Tea)</h2>
<p>最后，我们来看看绿茶算法，这是我们对标记扫描算法的一个新的尝试。绿茶算法的核心思想非常简单：</p>
<p><strong>操作页面，而不是对象。</strong></p>
<p>听起来很简单，对吧？然而，为了弄清楚如何安排对象图遍历的顺序以及我们需要跟踪哪些内容才能使其在实践中有效运作，我们做了大量的工作。</p>
<p>更具体地说，这意味着：</p>
<ul>
<li>我们不再扫描对象，而是扫描整个页。</li>
<li>我们不再在工作列表上跟踪对象，而是跟踪整个页。</li>
<li>我们最终(在一个扫描周期结束时)仍然需要标记对象，但我们会跟踪每个页面本地标记的对象，而不是跟踪整个堆中的标记对象。</li>
</ul>
<h3>绿茶示例</h3>
<p>让我们通过再次审视我们的示例堆，来看看这在实践中意味着什么，但这次运行的是“绿茶”而不是直接的图泛洪。</p>
<p>和之前一样，请跟随带注释的幻灯片进行浏览。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-060.png" alt="" /></p>
<p>这和之前的堆是一样的，但现在每个对象有两个比特的元数据而不是一个。同样，每个比特或框，对应于页中的一个对象槽位。总的来说，我们现在有 14 个比特对应于页 A 中的七个槽位。</p>
<p>顶部的比特代表和以前一样的东西：我们是否见过一个指向该对象的指针。我称之为“已见” (seen) 位。底部的比特集是新的。这些“已扫描” (scanned) 位跟踪我们是否已经扫描了该对象。</p>
<p>这块新的元数据是必需的，因为在“绿茶”中，<strong>工作列表跟踪的是页，而不是对象</strong>。我们仍然需要在某种程度上跟踪对象，这就是这些比特的目的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-062.png" alt="" /></p>
<p>我们和以前一样开始，从根开始遍历对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-063.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-064.png" alt="" /></p>
<p>但这一次，我们不是把一个对象放到工作列表上，而是把整个页——在这里是页 A——放到工作列表上，通过将整个页用蓝色阴影表示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-066.png" alt="" /></p>
<p>我们找到的对象也是蓝色的，表示当我们从工作列表中取出这个页时，我们将需要查看那个对象。请注意，对象的蓝色调直接反映了页 A 中的元数据。其对应的“已见”位被设置，但其“已扫描”位没有。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-069.png" alt="" /></p>
<p>我们跟随下一个根，找到另一个对象，再次将整个页——页 C——放到工作列表上，并设置该对象的“已见”位。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-071.png" alt="" /></p>
<p>我们处理完根了，所以我们转向工作列表，并从工作列表中取出页 A。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-072.png" alt="" /></p>
<p>通过“已见”和“已扫描”位，我们可以知道页 A 上有一个对象需要扫描。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-074.png" alt="" /></p>
<p>我们扫描那个对象，跟随它的指针。结果，我们将页 B 添加到工作列表，因为页 A 中的第一个对象指向了页 B 中的一个对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-075.png" alt="" /></p>
<p>我们处理完页 A 了。接下来我们从工作列表中取出页 C。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-076.png" alt="" /></p>
<p>与页 A 类似，页 C 上有一个单独的对象需要扫描。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-078.png" alt="" /></p>
<p>我们在页 B 中找到了一个指向另一个对象的指针。页 B 已经在工作列表上了，所以我们不需要向工作列表添加任何东西。我们只需为目标对象设置“已见”位。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-079.png" alt="" /></p>
<p>现在轮到页 B 了。我们在页 B 上累积了两个待扫描的对象，我们可以按内存顺序，连续处理这两个对象！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-081.png" alt="" /></p>
<p>我们遍历第一个对象的指针…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-082.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-083.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-084.png" alt="" /></p>
<p>我们在页 A 中找到了一个指向一个对象的指针。页 A 之前在工作列表上，但此时不在了，所以我们把它放回工作列表。与原始的标记-清除算法不同，在原始算法中，任何给定的对象在整个标记阶段最多只会被添加到工作列表一次；而在“绿茶”中，一个给定的页在标记阶段可能会多次出现在工作列表上。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-085.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-086.png" alt="" /></p>
<p>我们在扫描完第一个之后，立即扫描页中的第二个“已见”对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-087.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-088.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-089.png" alt="" /></p>
<p>我们在页 A 中又找到了几个对象…</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-090.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-091.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-092.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-093.png" alt="" /></p>
<p>我们扫描完页 B 了，所以我们从工作列表中取出页 A。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-094.png" alt="" /></p>
<p>这次我们只需要扫描三个对象，而不是四个，因为我们已经扫描过第一个对象了。我们通过查看“已见”和“已扫描”位之间的差异，来知道要扫描哪些对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-095.png" alt="" /></p>
<p>我们将按顺序扫描这些对象。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-096.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-097.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-098.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-099.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-100.png" alt="" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-101.png" alt="" /></p>
<p>我们完成了！工作列表上没有更多的页了，我们也没有正在处理的东西。请注意，现在元数据都很好地对齐了，因为所有可达的对象都既被“已见”又被“已扫描”。</p>
<p>你可能在我们的遍历过程中也注意到了，工作列表的顺序与图遍历有点不同。图遍历是“后进先出”或类似栈的顺序，而这里我们对工作列表上的页使用的是“先进先出”或类似队列的顺序。</p>
<p>这是有意为之的。当页在队列中等待时，我们让“已见”对象在每个页上累积，这样我们就可以一次性处理尽可能多的对象。这就是我们能一次性处理页 A 上那么多对象的原因。有时候，懒惰是一种美德。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-102.png" alt="" /></p>
<p>最后，我们可以像以前一样，清除掉未访问的对象。</p>
<h3>驶上高速公路</h3>
<p>让我们回到我们开车的比喻。我们终于要上高速公路了吗？</p>
<p>让我们回顾一下之前的图泛洪图片。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/graphflood-path2.png" alt="" /></p>
<p>原始图遍历在堆中穿行的路径需要 7 次独立的扫描。</p>
<p>我们到处跳跃，在不同的地方做着零碎的工作。“绿茶”所走的路径看起来非常不同。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/greentea-path.png" alt="" /></p>
<p>“绿茶”所走的路径仅需要 4 次扫描。</p>
<p>相比之下，绿茶在 A 和 B 页面上从左到右的移动次数较少，但每次移动时间更长。 这些箭头越长越好，箭头堆积越多，这种效果就越强。这就是绿茶的魅力所在。</p>
<p>这也是我们驰骋高速公路的机会。</p>
<p>这一切都使得它与微架构更加契合。现在，我们可以更精确地扫描彼此靠近的对象，从而更有可能利用缓存并避免使用主内存。同样，每页的元数据也更有可能被缓存。跟踪页面而非对象意味着工作列表更小，而工作列表压力的降低意味着争用更少，CPU 停顿也更少。</p>
<p>说到高速公路，我们可以把我们比喻意义上的引擎开到以前从未开过的档位，因为现在我们可以使用向量硬件了！</p>
<h3>向量加速</h3>
<p>如果你对向量硬件只有粗浅的了解，可能会不明白我们在这里如何使用它。但除了常见的算术和三角运算之外，最新的向量硬件还支持两项对绿茶算法非常有用的功能：超宽寄存器和复杂的位运算。</p>
<p>大多数现代 x86 CPU 都支持 AVX-512 指令集，它拥有 512 位宽的向量寄存器。如此宽的寄存器足以在 CPU 上仅使用两个寄存器来存储整个页面的所有元数据，从而使 Green Tea 能够仅用几条直线指令就完成整个页面的扫描。向量硬件长期以来一直支持对整个向量寄存器进行基本的位运算，但从 AMD Zen 4 和 Intel Ice Lake 开始，它还支持一种新的位向量“瑞士军刀”指令，使得 Green Tea 扫描过程中的关键步骤能够在几个 CPU 周期内完成。这些改进共同作用，使我们能够大幅提升 Green Tea 的扫描循环速度。</p>
<p>对于之前的图泛洪来说，这根本不可能，因为我们需要在各种大小的对象之间来回扫描。有时只需要两条元数据，有时却需要一万条。向量硬件根本无法满足这种可预测性和规律性要求。</p>
<p>如果你想深入了解一些细节，请继续阅读！否则，请随时跳到下面的【评估】小节。</p>
<h4>AVX-512 扫描内核</h4>
<p>要了解 AVX-512 GC 扫描是什么样子，请看下面的图。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/avx512.png" alt="" /><br />
<center>用于扫描的 AVX-512 矢量内核</center></p>
<p>这里面涉及的内容很多，我们可能光是解释它的运作原理就能写一整篇博客文章。现在，我们先从宏观层面来概括一下：</p>
<ol>
<li>首先，我们获取页面的“已查看”和“已扫描”位。请记住，页面中的每个对象对应一位，并且页面中的所有对象大小相同。 </li>
<li>接下来，我们比较这两个位集。它们的并集成为新的“扫描”位，而它们的差集则是“活动对象”位图，它告诉我们在本次页面扫描过程中（与之前的扫描相比）需要扫描哪些对象。</li>
<li>我们计算两个位图的差值并进行“扩展”，这样就不是每个对象占用一位，而是页面中的每个字（8 字节）占用一位。我们称之为“活动字”位图。例如，如果页面存储 6 个字（48 字节）的对象，则活动对象位图中的每位将被复制到活动字位图中的 6 位。如下所示：</li>
</ol>
<pre><code>0 0 1 1 ...  → 000000 000000 111111 111111 ...
</code></pre>
<ol>
<li>接下来，我们获取页面的指针/标量位图。同样，这里的每一位都对应页面的一个字（8 字节），并告诉我们该字是否存储指针。这些数据由内存分配器管理。</li>
<li>
<p>现在，我们取指针/标量位图和活动字位图的交集。结果就是“活动指针位图”：该位图告诉我们尚未扫描的任何活动对象中包含的整个页面中每个指针的位置。</p>
</li>
<li>
<p>最后，我们可以遍历页面内存并收集所有指针。逻辑上，我们遍历活动指针位图中的每个置位，加载该字处的指针值，并将其写回缓冲区。该缓冲区稍后将用于标记已访问的对象并将页面添加到工作列表中。利用向量指令，我们只需几条指令即可一次处理 64 字节。</p>
</li>
</ol>
<p>让这一切变快的部分原因是 VGF2P8AFFINEQB 指令，它是“Galios Field新指令” x86 扩展的一部分，也是我们上面提到的位操作“瑞士军刀”。它是真正的明星，因为它让我们能够非常高效地完成扫描内核中的第 (3) 步。它执行逐位的<a href="https://en.wikipedia.org/wiki/Affine_transformation">仿射变换</a>，将向量中的每个字节本身视为一个 8 位的数学向量，并将其与一个 8&#215;8 的比特矩阵相乘。这一切都是在<a href="https://en.wikipedia.org/wiki/Finite_field">Galios Field</a> GF(2) 上完成的，这意味着乘法是AND，加法是XOR。这样做的好处是，我们可以为每个对象大小定义几个 8&#215;8 的比特矩阵，来精确地执行我们需要的 1:n 比特扩展。</p>
<p>完整的汇编代码，请看<a href="https://cs.opensource.google/go/go/+/master:src/internal/runtime/gc/scan/scan_amd64.s;l=23;drc=041f564b3e6fa3f4af13a01b94db14c1ee8a42e0">这个文件</a>。“扩展器”为每个大小类别使用不同的矩阵和不同的排列，所以它们在一个由<a href="https://cs.opensource.google/go/go/+/master:src/internal/runtime/gc/scan/mkasm.go;drc=041f564b3e6fa3f4af13a01b94db14c1ee8a42e0">代码生成器</a>编写的<a href="https://cs.opensource.google/go/go/+/master:src/internal/runtime/gc/scan/expand_amd64.s;drc=041f564b3e6fa3f4af13a01b94db14c1ee8a42e0">单独文件</a>中。除了扩展函数，代码量其实不多。大部分代码都被极大地简化了，因为我们可以在纯粹位于寄存器中的数据上执行大部分上述操作。而且，希望很快这段汇编代码<a href="https://go.dev/issue/73787">将被 Go 代码所取代</a>！</p>
<p>感谢 Austin Clements 设计了这个过程。它非常酷，而且非常快！</p>
<h3>评估</h3>
<p>那么，这就是Green Tea的工作原理。它到底有多大帮助呢？</p>
<p>效果可能相当显著。即使不考虑向量增强，我们的基准测试套件也显示垃圾回收的 CPU 成本降低了 10% 到 40%。例如，如果应用程序 10% 的时间都花在了垃圾回收器上，那么根据工作负载的具体情况，整体 CPU 消耗将降低 1% 到 4%。垃圾回收 CPU 时间降低 10% 大致是典型的改进幅度。<br />
（有关这些细节，请参阅 <a href="https://go.dev/issue/73581">GitHub issue</a>。）</p>
<p>我们在谷歌内部推广了绿茶，并且大规模推广后也看到了类似的效果。</p>
<p>我们仍在推出向量增强功能，但基准测试和早期结果表明，这将额外带来 10%的 GC CPU 降低。</p>
<p>虽然大多数工作负载都能在一定程度上受益，但也有一些工作负载不会受益。</p>
<p>Green Tea 算法基于这样的假设：我们可以一次性在单页上累积足够多的对象进行扫描，从而抵消累积过程的成本。如果堆结构非常规则（对象大小相同，且在对象图中的深度也相近），那么这个假设显然成立。但是，有些工作负载通常要求我们每次只能扫描一个对象。这可能比图泛洪更糟糕，因为我们可能在尝试累积对象到页面上的过程中，反而做了更多工作，最终却失败了。</p>
<p>Green Tea 算法针对仅包含单个待扫描对象的页面进行了特殊处理。这有助于减少性能回退，但并不能完全消除它们。</p>
<p>然而，要超越图泛洪算法，所需的单页累积数据量远比你想象的要少。这项研究的一个意外发现是，每次仅扫描页面 2% 的数据就能取得比图泛洪算法更好的性能。</p>
<h3>可用性</h3>
<p>“绿茶”已经在最近的 Go 1.25 版本中作为实验性功能提供，并且可以通过在构建时将环境变量 GOEXPERIMENT 设置为 greenteagc 来启用。这不包括前述的向量加速。</p>
<p>我们预计在 Go 1.26 中将“绿茶”作为默认的垃圾回收器，但你仍然可以通过 GOEXPERIMENT=nogreenteagc 在构建时选择退出。Go 1.26 还将在较新的 x86 硬件上增加向量加速，并根据我们收集的反馈包含一系列的调整和改进。</p>
<p>如果可以，我们鼓励你尝试<a href="https://tonybai.com/2024/11/15/install-gotip-using-go-repo-mirror/">使用 Go 的最新tip版本</a>！如果你更喜欢使用 Go 1.25，我们也同样欢迎您的反馈。请参阅<a href="https://go.dev/issue/73581#issuecomment-2847696497">这个 GitHub 评论</a>，其中包含一些关于我们感兴趣的诊断信息、如果你可以分享的话，以及首选的反馈渠道的细节。</p>
<h2>旅程</h2>
<p>在结束这篇博文之前，让我们花点时间谈谈我们走到今天的历程，以及这项技术背后的人的因素。</p>
<p>绿茶的核心理念看似简单，就像某个人灵光一闪的灵感火花。</p>
<p>但事实并非如此。“绿茶”是许多人多年来共同努力和构思的成果。Go 团队的多位成员都参与了构思，包括 Michael Pratt、Cherry Mui、David Chase 和 Keith Randall。当时在英特尔工作的 Yves Vandriessche 的微架构见解也对设计探索起到了至关重要的作用。为了使这个看似简单的理念得以实现，我们尝试了许多方法，也处理了许多细节问题。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/timeline.png" alt="" /><br />
<center>时间线描绘了我们在达到今天这种状态之前，尝试过的一些类似想法</center></p>
<p>这个想法的萌芽可以追溯到2018年。有趣的是，团队里的每个人都认为最初的想法是别人提出的。</p>
<p>绿茶这个名字是在2024年得来的。当时，奥斯汀在日本四处寻觅咖啡馆，喝了无数抹茶，并由此构思出了早期版本的原型！这个原型证明了绿茶的核心理念是可行的。从此，我们便开始了绿茶的研发之路。</p>
<p>在 2025 年，随着 Michael 将绿茶项目实施并投入生产，其理念进一步发展和变化。</p>
<p>这需要大量的协作探索，因为绿茶算法不仅仅是一个算法，而是一个完整的设计空间。我们认为，单凭我们中的任何一个人都无法独自驾驭它。仅仅有想法是不够的，你还需要弄清楚细节并加以验证。现在我们已经做到了，终于可以开始迭代了。</p>
<p>“绿茶”的未来是光明的。</p>
<p>再次，请通过设置 GOEXPERIMENT=greenteagc 来尝试它，并让我们知道它的效果如何！我们对这项工作感到非常兴奋，并希望听到你的声音！</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/31/deep-into-go-green-tea-gc/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>7 个常见的 Kubernetes 陷阱（以及我是如何学会避免它们的）</title>
		<link>https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls/</link>
		<comments>https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls/#comments</comments>
		<pubDate>Wed, 22 Oct 2025 13:21:36 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[CNI插件]]></category>
		<category><![CDATA[ConfigMaps]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[Deployments]]></category>
		<category><![CDATA[DNS解析]]></category>
		<category><![CDATA[FluentBit]]></category>
		<category><![CDATA[Fluentd]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HorizontalPodAutoscaler]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[ingress-nginx]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[jaeger]]></category>
		<category><![CDATA[kubectllogs]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[kustomize]]></category>
		<category><![CDATA[Kyverno]]></category>
		<category><![CDATA[OOMKilled]]></category>
		<category><![CDATA[OPAGatekeeper]]></category>
		<category><![CDATA[opentelemetry]]></category>
		<category><![CDATA[PersistentVolumeClaims]]></category>
		<category><![CDATA[Pod安全准入]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[RBAC]]></category>
		<category><![CDATA[SealedSecrets]]></category>
		<category><![CDATA[Secrets]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[SRE]]></category>
		<category><![CDATA[TonyBai]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[分布式追踪]]></category>
		<category><![CDATA[启动探针]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[存活探针]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[容器日志]]></category>
		<category><![CDATA[就绪探针]]></category>
		<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=5289</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls 大家好，我是Tony Bai。 本文翻译自Kubernetes官方博客《7 Common Kubernetes Pitfalls (and How I Learned to Avoid Them)》一文。 这篇文章的作者Abdelkoddous Lhajouji 以第一人称视角，系统性地梳理了从资源管理、健康检查到安全配置等多个方面，新手乃至资深工程师都极易忽视的关键点。文中的每个“陷阱”都源于真实的生产经验，其规避建议更是极具实践指导意义。无论你是 K8s 初学者还是经验丰富的 SRE，相信都能从中获得启发，审视并改善自己的日常实践。 以下是译文全文，供大家参考。 Kubernetes 有时既强大又令人沮丧，这已经不是什么秘密了。当我刚开始涉足容器编排时，我犯的错误足以整理出一整份陷阱清单。在这篇文章中，我想详细介绍我遇到（或看到别人遇到）的七个大坑，并分享一些如何避免它们的技巧。无论你是刚开始接触 Kubernetes，还是已经在管理生产集群，我都希望这些见解能帮助你避开一些额外的压力。 忽略资源请求（requests）和限制（limits） 陷阱：在 Pod 规范中不指定 CPU 和内存需求。这通常是因为 Kubernetes 并不强制要求这些字段，而且工作负载通常可以在没有它们的情况下启动和运行——这使得在早期配置或快速部署周期中很容易忽略这个疏漏。 背景：在 Kubernetes 中，资源请求和限制对于高效的集群管理至关重要。资源请求确保调度器为每个 Pod 预留适当数量的 CPU 和内存，保证其拥有运行所需的必要资源。资源限制则为 Pod 可以使用的 CPU 和内存设置了上限，防止任何单个 Pod 消耗过多资源，从而可能导致其他 Pod 资源匮乏。当未设置资源请求和限制时： 资源匮乏：Pod 可能会获得不足的资源，导致性能下降或失败。这是因为 Kubernetes 会根据这些请求来调度 Pod。如果没有它们，调度器可能会在单个节点上放置过多的 Pod，从而导致资源争用和性能瓶颈。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/seven-kubernetes-pitfalls-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls">本文永久链接</a> &#8211; https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls</p>
<p>大家好，我是Tony Bai。</p>
<p>本文翻译自Kubernetes官方博客《<a href="https://kubernetes.io/blog/2025/10/20/seven-kubernetes-pitfalls-and-how-to-avoid/">7 Common Kubernetes Pitfalls (and How I Learned to Avoid Them)</a>》一文。</p>
<p>这篇文章的作者Abdelkoddous Lhajouji 以第一人称视角，系统性地梳理了从资源管理、健康检查到安全配置等多个方面，新手乃至资深工程师都极易忽视的关键点。文中的每个“陷阱”都源于真实的生产经验，其规避建议更是极具实践指导意义。无论你是 K8s 初学者还是经验丰富的 SRE，相信都能从中获得启发，审视并改善自己的日常实践。</p>
<p>以下是译文全文，供大家参考。</p>
<hr />
<p>Kubernetes 有时既强大又令人沮丧，这已经不是什么秘密了。当我刚开始涉足容器编排时，我犯的错误足以整理出一整份陷阱清单。在这篇文章中，我想详细介绍我遇到（或看到别人遇到）的七个大坑，并分享一些如何避免它们的技巧。无论你是刚开始接触 Kubernetes，还是已经在管理生产集群，我都希望这些见解能帮助你避开一些额外的压力。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-network-programming-complete-guide-pr.png" alt="" /></p>
<h2>忽略资源请求（requests）和限制（limits）</h2>
<p><strong>陷阱</strong>：在 Pod 规范中不指定 CPU 和内存需求。这通常是因为 Kubernetes 并不强制要求这些字段，而且工作负载通常可以在没有它们的情况下启动和运行——这使得在早期配置或快速部署周期中很容易忽略这个疏漏。</p>
<p><strong>背景</strong>：在 Kubernetes 中，资源请求和限制对于高效的集群管理至关重要。资源请求确保调度器为每个 Pod 预留适当数量的 CPU 和内存，保证其拥有运行所需的必要资源。资源限制则为 Pod 可以使用的 CPU 和内存设置了上限，防止任何单个 Pod 消耗过多资源，从而可能导致其他 Pod 资源匮乏。当未设置资源请求和限制时：</p>
<ol>
<li><strong>资源匮乏</strong>：Pod 可能会获得不足的资源，导致性能下降或失败。这是因为 Kubernetes 会根据这些请求来调度 Pod。如果没有它们，调度器可能会在单个节点上放置过多的 Pod，从而导致资源争用和性能瓶颈。</li>
<li><strong>资源囤积</strong>：相反，如果没有限制，一个 Pod 可能会消耗超过其应有份额的资源，影响同一节点上其他 Pod 的性能和稳定性。这可能导致其他 Pod 因内存不足而被驱逐或被内存溢出（OOM）杀手终止等问题。</li>
</ol>
<h3>如何避免</h3>
<ul>
<li>从适度的 requests 开始（例如 100m CPU，128Mi 内存），然后观察你的应用表现如何。</li>
<li>监控实际使用情况并优化你的设置；<a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">HorizontalPodAutoscaler</a> 可以帮助根据指标自动进行扩缩容。</li>
<li>留意 kubectl top pods 或你的日志/监控工具，以确认你没有过度或不足地配置资源。</li>
</ul>
<p><strong>我的惨痛教训</strong>：早期，我从未考虑过内存限制。在我的本地集群上，一切似乎都很好。然后，在一个更大的环境中，Pod 们接二连三地被 OOMKilled。教训惨痛。有关为你的容器配置资源请求和限制的详细说明，请参阅官方 Kubernetes 文档的<a href="https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/">为容器和 Pod 分配内存资源</a>。</p>
<h2>低估存活探针（liveness）和就绪探针（readiness）</h2>
<p><strong>陷阱</strong>：部署容器时不明确定义 Kubernetes 应如何检查其健康或就绪状态。这往往是因为只要容器内的进程没有退出，Kubernetes 就会认为该容器处于“运行中”状态。在没有额外信号的情况下，Kubernetes 会假设工作负载正在正常运行——即使内部的应用程序没有响应、正在初始化或卡住了。</p>
<p><strong>背景</strong>：<br />
存活、就绪和启动探针是 Kubernetes 用来监控容器健康和可用性的机制。</p>
<ul>
<li><strong>存活探针</strong> 决定应用程序是否仍然存活。如果存活检查失败，容器将被重启。</li>
<li><strong>就绪探针</strong> 控制容器是否准备好为流量提供服务。在就绪探针通过之前，该容器会从 Service 的端点中移除。</li>
<li><strong>启动探针</strong> 帮助区分长时间的启动过程和实际的故障。</li>
</ul>
<h3>如何避免</h3>
<ul>
<li>添加一个简单的 HTTP livenessProbe 来检查一个健康端点（例如 /healthz），以便 Kubernetes 可以重启卡住的容器。</li>
<li>使用一个 readinessProbe 来确保流量在你的应用预热完成前不会到达它。</li>
<li>保持探针简单。过于复杂的检查可能会产生误报和不必要的重启。</li>
</ul>
<p><strong>我的惨痛教训</strong>：我曾有一次忘记为一个需要一些时间来加载的 Web 服务设置就绪探针。用户过早地访问了它，遇到了奇怪的超时，而我花了几个小时挠头苦思。一个 3 行的就绪探针本可以拯救那一天。</p>
<p>有关为容器配置存活、就绪和启动探针的全面说明，请参阅官方 Kubernetes 文档中的<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">配置存活、就绪和启动探针</a>。</p>
<h2>“我们就看看容器日志好了”（著名遗言）</h2>
<p><strong>陷阱</strong>：仅仅依赖通过 kubectl logs 获取的容器日志。这通常是因为该命令快速方便，并且在许多设置中，日志在开发或早期故障排查期间似乎是可访问的。然而，kubectl logs 仅检索当前运行或最近终止的容器的日志，而这些日志存储在节点的本地磁盘上。一旦容器被删除、驱逐或节点重新启动，日志文件可能会被轮替掉或永久丢失。</p>
<h3>如何避免</h3>
<ul>
<li>使用 CNCF 工具如 <a href="https://kubernetes.io/docs/concepts/cluster-administration/logging/#sidecar-container-with-a-logging-agent">Fluentd</a> 或 <a href="https://fluentbit.io/">Fluent Bit</a> 来<strong>集中化日志</strong>，聚合所有 Pod 的输出。</li>
<li><strong>采用 OpenTelemetry</strong> 以获得日志、指标和（如果需要）追踪的统一视图。这使你能够发现基础设施事件与应用级行为之间的关联。</li>
<li><strong>将日志与 Prometheus 指标配对</strong>，以跟踪集群级别的数据以及应用程序日志。如果你需要分布式追踪，可以考虑 CNCF 项目如 <a href="https://www.jaegertracing.io/">Jaeger</a>。</li>
</ul>
<p><strong>我的惨痛教训</strong>：第一次因为一次快速重启而丢失 Pod 日志时，我才意识到 kubectl logs 本身是多么不可靠。从那时起，我为每个集群都设置了一个合适的管道，以避免丢失重要线索。</p>
<h2>将开发和生产环境完全等同对待</h2>
<p><strong>陷阱</strong>：在开发、预发布和生产环境中使用完全相同的设置部署相同的 Kubernetes 清单（manifests）。这通常发生在团队追求一致性和重用时，但忽略了特定于环境的因素——如流量模式、资源可用性、扩缩容需求或访问控制——可能会有显著不同。如果不进行定制，为一个环境优化的配置可能会在另一个环境中导致不稳定、性能不佳或安全漏洞。</p>
<h3>如何避免</h3>
<ul>
<li>使用overlays环境 或 <a href="https://kustomize.io/">kustomize</a> 来维护一个共享的基础配置，同时为每个环境定制资源请求、副本数或配置。</li>
<li>将特定于环境的配置提取到 ConfigMaps 和/或 Secrets 中。你可以使用专门的工具，如 <a href="https://github.com/bitnami-labs/sealed-secrets">Sealed Secrets</a> 来管理机密数据。</li>
<li>为生产环境的规模做好规划。你的开发集群可能用最少的 CPU/内存就能应付，但生产环境可能需要多得多。</li>
</ul>
<p><strong>我的惨痛教训</strong>：有一次，我为了“测试”，在一个小小的开发环境中将 replicaCount 从 2 扩展到 10。我立刻耗尽了资源，并花了半天时间清理残局。哎。</p>
<h2>让旧东西到处漂浮</h2>
<p><strong>陷阱</strong>：让未使用的或过时的资源——如 Deployments、Services、ConfigMaps 或 PersistentVolumeClaims——在集群中持续运行。这通常是因为 Kubernetes 不会自动移除资源，除非得到明确指示，而且没有内置机制来跟踪所有权或过期时间。随着时间的推移，这些被遗忘的对象会累积起来，消耗集群资源，增加云成本，并造成操作上的混乱，尤其是当过时的 Services 或 LoadBalancers 仍在继续路由流量时。</p>
<h3>如何避免</h3>
<ul>
<li>为<strong>所有东西打上标签</strong>，附上用途或所有者标签。这样，你就可以轻松查询不再需要的资源。</li>
<li><strong>定期审计</strong>你的集群：运行 kubectl get all -n <namespace> 来查看实际在运行什么，并确认它们都是合法的。</li>
<li><strong>采用 Kubernetes 的垃圾回收</strong>：<a href="https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/">K8s 文档</a>展示了如何自动移除依赖对象。</li>
<li><strong>利用策略自动化</strong>：像 <a href="https://kyverno.io/">Kyverno</a> 这样的工具可以在一定时期后自动删除或阻止过时的资源，或强制执行生命周期策略，这样你就不必记住每一个清理步骤。</li>
</ul>
<p><strong>我的惨痛教训</strong>：一次hackathon之后，我忘记拆除一个关联到外部负载均衡器的“test-svc”。三周后，我才意识到我一直在为那个负载均衡器付费。捂脸。</p>
<h2>过早地深入研究网络</h2>
<p><strong>陷阱</strong>：在完全理解 Kubernetes 的原生网络原语之前，就引入了高级的网络解决方案——如服务网格（service meshes）、自定义 CNI 插件或多集群通信。这通常发生在团队使用外部工具实现流量路由、可观测性或 mTLS 等功能，而没有首先掌握核心 Kubernetes 网络的工作原理时：包括 Pod 到 Pod 的通信、ClusterIP Services、DNS 解析和基本的 ingress 流量处理。结果，与网络相关的问题变得更难排查，尤其是当overlays网络引入了额外的抽象和故障点时。</p>
<h3>如何避免</h3>
<ul>
<li>从小处着手：一个 Deployment、一个 Service 和一个基本的 ingress 控制器，例如基于 NGINX 的控制器（如 Ingress-NGINX）。</li>
<li>确保你理解集群内的流量如何流动、服务发现如何工作以及 DNS 是如何配置的。</li>
<li>只有在你真正需要时，才转向功能完备的网格或高级 CNI 功能，复杂的网络会增加开销。</li>
</ul>
<p><strong>我的惨痛教训</strong>：我曾在一个小型的内部应用上尝试过 Istio，结果花在调试 Istio 本身的时间比调试实际应用还多。最终，我退后一步，移除了 Istio，一切都正常工作了。</p>
<h2>对安全和 RBAC 太掉以轻心</h2>
<p><strong>陷阱</strong>：使用不安全的配置部署工作负载，例如以 root 用户身份运行容器、使用 latest 镜像标签、禁用安全上下文（security contexts），或分配过于宽泛的 RBAC 角色（如 cluster-admin）。这些做法之所以持续存在，是因为 Kubernetes 开箱即用时并不强制执行严格的安全默认设置，而且该平台的设计初衷是灵活而非固执己见。在没有明确的安全策略的情况下，集群可能会持续暴露于容器逃逸、未经授权的权限提升或因未固定的镜像导致的意外生产变更等风险中。</p>
<h3>如何避免</h3>
<ul>
<li>使用 <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC</a> 来定义 Kubernetes 内部的角色和权限。虽然 RBAC 是默认且最广泛支持的授权机制，但 Kubernetes 也允许使用替代的授权方。对于更高级或外部的策略需求，可以考虑像 <a href="https://open-policy-agent.github.io/gatekeeper/">OPA Gatekeeper</a>（基于 Rego）、<a href="https://kyverno.io/">Kyverno</a> 或使用 CEL 或 <a href="https://cedarpolicy.com/">Cedar</a> 等策略语言的自定义 webhook 等解决方案。</li>
<li>将镜像固定到特定的版本（不要再用 :latest！）。这能帮助你确切地知道实际部署的是什么。</li>
<li>研究一下 <a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">Pod 安全准入</a>（或其他解决方案，如 Kyverno），以强制执行非 root 容器、只读文件系统等。</li>
</ul>
<p><strong>我的惨痛教训</strong>：我从未遇到过重大的安全漏洞，但我听过足够多的警示故事。如果你不把事情收紧，出问题只是时间问题。</p>
<h2>小结：最后的想法</h2>
<p>Kubernetes 很神奇，但它不会读心术，如果你不告诉它你需要什么，它不会神奇地做出正确的事。通过牢记这些陷阱，你将避免大量的头痛和时间浪费。错误会发生（相信我，我犯过不少），但每一次都是一个机会，让你更深入地了解 Kubernetes 在底层是如何真正工作的。如果你有兴趣深入研究，<a href="https://kubernetes.io/docs/home/">官方文档</a>和<a href="http://slack.kubernetes.io/">社区 Slack</a> 是绝佳的下一步。当然，也欢迎分享你自己的恐怖故事或成功技巧，因为归根结底，我们都在这场云原生的冒险中并肩作战。</p>
<p><strong>祝你交付愉快！</strong></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p><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/22/seven-kubernetes-pitfalls/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 作为第一门编程语言：天才之选还是糟糕开端？</title>
		<link>https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language/</link>
		<comments>https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language/#comments</comments>
		<pubDate>Sat, 11 Oct 2025 00:14:22 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[iferrnil]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[malloc/free]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[reference]]></category>
		<category><![CDATA[TypeScript]]></category>
		<category><![CDATA[专业开发流程]]></category>
		<category><![CDATA[两大阵营]]></category>
		<category><![CDATA[中间地带]]></category>
		<category><![CDATA[交互式体验]]></category>
		<category><![CDATA[伪代码]]></category>
		<category><![CDATA[健壮性]]></category>
		<category><![CDATA[兴趣培养]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[内存管理]]></category>
		<category><![CDATA[初学者]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[堆]]></category>
		<category><![CDATA[天才之选]]></category>
		<category><![CDATA[失败]]></category>
		<category><![CDATA[契约]]></category>
		<category><![CDATA[安全的指针哲学]]></category>
		<category><![CDATA[安全网]]></category>
		<category><![CDATA[实战派]]></category>
		<category><![CDATA[工程思想]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[底层]]></category>
		<category><![CDATA[引用vs值]]></category>
		<category><![CDATA[快速入门]]></category>
		<category><![CDATA[快速反馈]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[指针算术]]></category>
		<category><![CDATA[捷径]]></category>
		<category><![CDATA[摩擦力]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[显式错误处理]]></category>
		<category><![CDATA[构建时]]></category>
		<category><![CDATA[栈]]></category>
		<category><![CDATA[核心分歧]]></category>
		<category><![CDATA[现代计算机科学]]></category>
		<category><![CDATA[甜蜜点]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[糟糕开端]]></category>
		<category><![CDATA[纪律训练]]></category>
		<category><![CDATA[经验丰富的开发者]]></category>
		<category><![CDATA[编程教育]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[编译周期]]></category>
		<category><![CDATA[解释型语言]]></category>
		<category><![CDATA[计算机科学基础教育]]></category>
		<category><![CDATA[计算机科学家]]></category>
		<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=5240</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language 大家好，我是Tony Bai。 近日，在 r/golang 社区，一个初学者的真诚提问，再次点燃了一场关于 Go 是否适合作为入门语言的激烈辩论。他很困惑：“为什么很多经验丰富的开发者说 Go 不适合作为第一门编程语言，而很多大学却用与之相似的 C 语言作为第一门编程语言呢？” 这个问题，如同一块探针，深入到了编程教育的核心分歧之中，并迅速将社区观点分裂为两大阵营。一方认为，Go 能从第一天起就培养严谨的工程思维，堪称“天才之选”。另一方则认为，它的定位不上不下，对初学者而言是一个“糟糕的开端”。 那么，真相究竟为何？为了厘清思路，让我们深入这场辩论，分别听取两大阵营的观点，并审视其背后的根本分歧：我们学习编程，到底是为了什么？ 观点一：Go 是一个“糟糕的开端” 这一方的核心论点是：Go 语言陷入了一个尴尬的“中间地带”，对于编程教育的两个主要目标，它都未能完美胜任。 论据一：Go 不够底层，无法胜任“计算机科学基础教育” 这一方的支持者指出，大学 CS 教育的首要目标，是培养学生对计算机工作原理的深刻理解。在这个目标下，C 语言之所以是“黄金标准”，恰恰在于它的“不友好”： 直面内存：手动 malloc/free 和危险的指针算术，迫使学生直面内存布局、栈与堆等核心概念。 最小化抽象：学生必须从零开始构建数据结构，这个过程能让他们对算法的理解建立在物理实现之上。 而Go 的垃圾回收 (GC) 机制，虽然是工程上的巨大进步，但在教育上却成了一个“黑盒”，完全隐藏了内存管理的复杂性。它让学生“知其然”，却无法“知其所以然”，因此无法胜任传授底层原理的重任。 论据二：Go 不够“温柔”，无法胜任“快速入门与兴趣培养” 接着，这一方展示了另一个极端——以 Python 为代表的“实战派”入门语言。这类语言的目标是让初学者尽快体验到编程的乐趣和效用。 语法“温柔”：Python 的语法接近伪代码，极大地降低了入门的认知门槛。 快速反馈：作为解释型语言，其“编写即运行”的交互式体验，对维持初学者的学习热情至关重要。 尽管 Go 也以简单著称，但其静态类型、编译周期、以及对项目结构的规范要求，都为纯粹的初学者制造了不必要的“摩擦力”。与 Python 相比，它不够“温柔”，可能会在入门阶段就劝退一部分学习者。 由此来看，Go 既不像 C 那样能让你深入底层，又不像 Python 那样能让你轻松起步。它是一个尴尬的“中间派”，对于任何一个明确的教学目标来说，都有比它更好的选择。因此，它是一个“糟糕的开端”。 观点二：Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-a-good-first-programming-language-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language">本文永久链接</a> &#8211; https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language</p>
<p>大家好，我是Tony Bai。</p>
<p>近日，在 r/golang 社区，<a href="https://www.reddit.com/r/golang/comments/1nvbrv8/im_confused_as_to_why_experienced_devs_say_go_is/">一个初学者的真诚提问</a>，再次点燃了一场关于 Go 是否适合作为入门语言的激烈辩论。他很困惑：“为什么很多经验丰富的开发者说 Go 不适合作为第一门编程语言，而很多大学却用与之相似的 C 语言作为第一门编程语言呢？”</p>
<p>这个问题，如同一块探针，深入到了编程教育的核心分歧之中，并迅速将社区观点分裂为两大阵营。一方认为，Go 能从第一天起就培养严谨的工程思维，堪称<strong>“天才之选”</strong>。另一方则认为，它的定位不上不下，对初学者而言是一个<strong>“糟糕的开端”</strong>。</p>
<p>那么，真相究竟为何？为了厘清思路，让我们深入这场辩论，分别听取两大阵营的观点，并审视其背后的根本分歧：<strong>我们学习编程，到底是为了什么？</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-network-programming-complete-guide-pr.png" alt="" /></p>
<h2>观点一：Go 是一个“糟糕的开端”</h2>
<p>这一方的核心论点是：Go 语言陷入了一个尴尬的“中间地带”，对于编程教育的两个主要目标，它都未能完美胜任。</p>
<h3>论据一：Go 不够底层，无法胜任“计算机科学基础教育”</h3>
<p>这一方的支持者指出，大学 CS 教育的首要目标，是培养学生对计算机工作原理的深刻理解。在这个目标下，C 语言之所以是“黄金标准”，恰恰在于它的“不友好”：</p>
<ul>
<li><strong>直面内存</strong>：手动 malloc/free 和危险的指针算术，迫使学生直面内存布局、栈与堆等核心概念。</li>
<li><strong>最小化抽象</strong>：学生必须从零开始构建数据结构，这个过程能让他们对算法的理解建立在物理实现之上。</li>
</ul>
<p>而Go 的<strong>垃圾回收 (GC)</strong> 机制，虽然是工程上的巨大进步，但在教育上却成了一个“黑盒”，<strong>完全隐藏了内存管理的复杂性</strong>。它让学生“知其然”，却无法“知其所以然”，因此无法胜任传授底层原理的重任。</p>
<h3>论据二：Go 不够“温柔”，无法胜任“快速入门与兴趣培养”</h3>
<p>接着，这一方展示了另一个极端——以 Python 为代表的“实战派”入门语言。这类语言的目标是让初学者尽快体验到编程的乐趣和效用。</p>
<ul>
<li><strong>语法“温柔”</strong>：Python 的语法接近伪代码，极大地降低了入门的认知门槛。</li>
<li><strong>快速反馈</strong>：作为解释型语言，其“编写即运行”的交互式体验，对维持初学者的学习热情至关重要。</li>
</ul>
<p>尽管 Go 也以简单著称，但其<strong>静态类型、编译周期、以及对项目结构的规范要求</strong>，都为纯粹的初学者制造了不必要的“摩擦力”。与 Python 相比，它不够“温柔”，可能会在入门阶段就劝退一部分学习者。</p>
<p>由此来看，Go 既不像 C 那样能让你深入底层，又不像 Python 那样能让你轻松起步。它是一个尴尬的“中间派”，对于任何一个明确的教学目标来说，都有比它更好的选择。因此，它是一个“糟糕的开端”。</p>
<h2>观点二：Go 是一个“天才之选”</h2>
<p>另一方的核心论点是：观点一中所说的“中间地带”并非尴尬，而是一个<strong>经过深思熟虑、精心设计的“甜蜜点” (sweet spot)</strong>。Go 的目标，不是培养纯粹的理论家或业余爱好者，而是从第一天起，就<strong>为培养专业的“软件工程师”奠定基础</strong>。</p>
<h3>论据一：Go 教授的是“更重要”的底层原理</h3>
<p>观点二的支持者承认 Go 隐藏了手动内存管理的细节，但他们认为，在 2025 年的今天，这部分细节的教学价值正在下降。相反，Go 教授了更现代、更重要的底层概念：</p>
<ul>
<li><strong>安全的指针哲学</strong>：Go 保留了指针，让学生能够深刻理解<strong>“引用 vs. 值”</strong>这一核心概念，这是理解程序性能和行为的关键。同时，它通过移除指针算术，杜绝了 C 语言中最常见的一类安全漏洞。</li>
<li><strong>并发是第一性原理</strong>：他们强调，现代计算的核心是并发。Go 将 goroutine 和 channel 作为内建特性，让学生能够以一种前所未有的简洁方式，去接触和理解并发这一现代计算机科学的基石。</li>
</ul>
<p>Go 并非不教底层，而是有选择地教授那些<strong>在现代软件工程中依然至关重要的底层概念</strong>，同时将那些日益自动化、易出错的细节（如手动内存管理）抽象掉。</p>
<h3>论据二：Go 的“摩擦力”恰恰是良好工程习惯的开端</h3>
<p>观点二的支持者认为，观点一所说的“摩擦力”，实际上是宝贵的“纪律训练”：</p>
<ul>
<li><strong>静态类型</strong>：不是负担，而是一张安全网，它教会学生思考数据的结构和契约。TypeScript逐步超越JavaScript就是一个静态类型取得胜利的明证。</li>
<li><strong>显式错误处理</strong>：if err != nil 不是样板代码，而是对健壮性最深刻的、日复一日的训练。它让学生明白，<strong>失败是程序中正常的一部分，必须被认真对待</strong>。</li>
<li><strong>编译周期</strong>：不是障碍，而是专业开发流程的预演，教会学生区分构建时和运行时。</li>
</ul>
<p>Go 的设计，完美地平衡了抽象与细节。它既能让学生快速构建出实际的应用（比如一个简单的 Web 服务器），又在整个过程中不断地、潜移默化地向他们灌输专业的工程思想。它不是在教“编程”，而是在教“软件工程”。因此，对于立志成为专业工程师的学习者来说，它是一个<strong>“天才之选”</strong>。</p>
<h2>小结：目标决定了最佳路径</h2>
<p>至此，辩论的脉络已经清晰。这场争论没有绝对的赢家，因为双方的论点都建立在各自合理的目标之上。</p>
<p><strong>最终的结论是：这取决于你的目标。</strong></p>
<ul>
<li>如果你的目标是<strong>成为一名计算机科学家</strong>，深入理解机器的每一个齿轮如何运转，那么从 C 开始的“苦修”或许无法绕开。</li>
<li>如果你的目标是<strong>快速体验编程的乐趣、尽快构建应用</strong>，那么 Python 或 JavaScript 可能会为你提供一条更平坦、更愉悦的道路。</li>
<li>而 Go，则为那些从一开始就立志于<strong>成为一名专业、高效、能构建并发系统的现代软件工程师</strong>的学习者，提供了一条无与伦比的捷径。</li>
</ul>
<p>它或许不是最完美的“第一站”，但对于目标明确的人来说，它是一个能让你赢在起跑线上的<strong>“天才之选”</strong>。它将“学习编程”与“成为一名软件工程师”这两个阶段，以前所未有的方式紧密地结合在了一起。</p>
<p>资料链接：https://www.reddit.com/r/golang/comments/1nvbrv8/im_confused_as_to_why_experienced_devs_say_go_is/</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/11/go-is-a-good-first-programming-language/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>context：Go 语言的“天问”，你真的懂了吗？</title>
		<link>https://tonybai.com/2025/09/15/go-context-column/</link>
		<comments>https://tonybai.com/2025/09/15/go-context-column/#comments</comments>
		<pubDate>Mon, 15 Sep 2025 00:35:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GoContext]]></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[Go语言进阶课]]></category>
		<category><![CDATA[OOM]]></category>
		<category><![CDATA[TonyBai]]></category>
		<category><![CDATA[WithValue]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[军规]]></category>
		<category><![CDATA[函数签名]]></category>
		<category><![CDATA[取消]]></category>
		<category><![CDATA[学习体会]]></category>
		<category><![CDATA[技术深度]]></category>
		<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=5165</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/09/15/go-context-column 大家好，我是Tony Bai。 作为一个 Gopher，如果说 Go 语言里哪个标准库最能引发“灵魂拷问”，我想 context 说第二，没人敢说第一。 我们每天都在和它打交道，不是吗？ 打开任何一个 Go 项目，从 Gin 的 c.Request.Context()，到 gRPC 的方法签名，再到数据库的 QueryContext，context.Context 这个参数就像一个“幽灵”，无处不在，却又常常让人捉摸不透。 它总是雷打不动地占据着函数签名的第一个位置，仿佛在宣告自己的“正宫”地位。我们依葫芦画瓢地将它一层层往下传，似乎只要照做，程序就能安然无恙。 但你是否也曾在某个深夜，对着一段因为 context deadline exceeded 而崩溃的代码，陷入沉思： 这个 ctx 到底是个什么“东西”？为什么它能“凭空”知道超时了？ context.Background() 和 context.TODO()，我到底该用哪个？感觉好像都能跑&#8230; 那个 WithValue，用起来真方便！我是不是可以把所有参数都塞进去，告别冗长的函数签名？（危险的想法！） 为什么我的 goroutine 明明收到了取消信号，却还在后台疯狂吃内存，最后 OOM 了？ 这些问题，就像一个个幽灵，盘旋在许多 Gopher 的脑海里。我们似乎懂 context，但又好像只懂它的皮毛。这种“半懂不懂”的状态，在平时或许相安无事，但在复杂的生产环境中，往往就是那个导致服务雪崩的“致命稻草”。 说实话，我曾经也为此挣扎了很久。 我读过官方文档，写过零散的学习体会博客，但总感觉知识是碎片化的。直到我下定决心，从 context 诞生的“前世”开始，一路追溯到它的源码“心脏”，再回到真实世界的“最佳实践”和“天坑”现场，我才终于将这些碎片拼成了一幅完整的、清晰的地图。 那一刻，我豁然开朗。 原来 context 的设计如此精妙，它用最简单的接口，解决的是 Go 并发编程中最核心的两个难题：生命周期控制和数据传递。它就是 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-context-column-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/09/15/go-context-column">本文永久链接</a> &#8211; https://tonybai.com/2025/09/15/go-context-column</p>
<p>大家好，我是Tony Bai。</p>
<p>作为一个 Gopher，如果说 Go 语言里哪个标准库最能引发“灵魂拷问”，我想 context 说第二，没人敢说第一。</p>
<p>我们每天都在和它打交道，不是吗？</p>
<p>打开任何一个 Go 项目，从 Gin 的 c.Request.Context()，到 gRPC 的方法签名，再到数据库的 QueryContext，context.Context 这个参数就像一个“幽灵”，无处不在，却又常常让人捉摸不透。</p>
<p>它总是雷打不动地占据着函数签名的第一个位置，仿佛在宣告自己的“正宫”地位。我们依葫芦画瓢地将它一层层往下传，似乎只要照做，程序就能安然无恙。</p>
<p>但你是否也曾在某个深夜，对着一段因为 context deadline exceeded 而崩溃的代码，陷入沉思：</p>
<ul>
<li>这个 ctx 到底是个什么“东西”？为什么它能“凭空”知道超时了？</li>
<li>context.Background() 和 context.TODO()，我到底该用哪个？感觉好像都能跑&#8230;</li>
<li>那个 WithValue，用起来真方便！我是不是可以把所有参数都塞进去，告别冗长的函数签名？（危险的想法！）</li>
<li>为什么我的 goroutine 明明收到了取消信号，却还在后台疯狂吃内存，最后 OOM 了？</li>
</ul>
<p>这些问题，就像一个个幽灵，盘旋在许多 Gopher 的脑海里。我们似乎懂 context，但又好像只懂它的皮毛。这种“半懂不懂”的状态，在平时或许相安无事，但在复杂的生产环境中，往往就是那个导致服务雪崩的“致命稻草”。</p>
<p><strong>说实话，我曾经也为此挣扎了很久。</strong></p>
<p>我读过官方文档，写过零散的学习体会博客，但总感觉知识是碎片化的。直到我下定决心，从 context 诞生的“前世”开始，一路追溯到它的源码“心脏”，再回到真实世界的“最佳实践”和“天坑”现场，我才终于将这些碎片拼成了一幅完整的、清晰的地图。</p>
<p>那一刻，我豁然开朗。</p>
<p>原来 context 的设计如此精妙，它用最简单的接口，解决的是 Go 并发编程中最核心的两个难题：<strong>生命周期控制</strong>和<strong>数据传递</strong>。它就是 Go 并发世界的“指挥官”和“情报员”。</p>
<p><strong>为了让更多像我一样曾经困惑的 Gopher 能够彻底征服 context，我决定将我的所有思考、踩坑经验和源码洞察，浓缩成一个全新的微专栏——《<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4163317975908614168#wechat_redirect">Go Context 解惑：从原理到最佳实践</a>》。</strong></p>
<p>这是一个<strong>反教条</strong>的专栏。我们不会一上来就罗列 API，而是：</p>
<ol>
<li><strong>回到原点：</strong> 在第一讲，我们会坐上时光机，回到那个没有 context 的“史前时代”，亲身体会一下当年的 Gopher 们是如何在资源泄漏和丑陋代码中“挣扎”的。只有理解了“痛苦”，你才能真正 appreciate context 的价值。</li>
<li><strong>系统学习：</strong> 我们会用最直观的方式，为你系统讲解 context 的核心 API 和最关键的<strong>超时与取消</strong>用法。</li>
<li><strong>深入源码：</strong> 我会带你一起潜入源码，用清晰的示意图，为你揭开 context 内部那棵“树”和那条“链表”的神秘面纱，让你彻底告别“黑盒”。</li>
<li><strong>实战为王：</strong> 最后，我会将所有知识沉淀为一套你可以直接打印出来贴在显示器上的<strong>“军规”和“避坑指南”</strong>，覆盖你在工作中 99% 的场景。</li>
</ol>
<p>整个专栏共 4 篇精心打磨的文章，每一篇都致力于解决一个核心问题，层层递进，帮你构建一个完整、牢固的 context 知识体系。</p>
<p>如果你也曾对 context 感到迷茫；</p>
<p>如果你渴望提升自己编写健壮并发程序的能力；</p>
<p>如果你想在技术深度上，与身边的同事拉开差距&#8230;</p>
<p>那么，这个专栏就是为你量身打造的。</p>
<p>现在就订阅吧。一次投资，让你彻底告别对 context 的恐惧。</p>
<p><strong>点击<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4163317975908614168#wechat_redirect">此链接</a>或扫描二维码，立即加入我们，一起征服 Go Context！</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-context-explained-pr.png" alt="" /></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="" /></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/15/go-context-column/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案</title>
		<link>https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal/</link>
		<comments>https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal/#comments</comments>
		<pubDate>Mon, 19 May 2025 00:09:03 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AddInt64]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[atomic-operation]]></category>
		<category><![CDATA[AustinClements]]></category>
		<category><![CDATA[CacheLine]]></category>
		<category><![CDATA[core]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[fasthttp]]></category>
		<category><![CDATA[G#]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOMAXPROCS]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[M]]></category>
		<category><![CDATA[Machine]]></category>
		<category><![CDATA[MESI]]></category>
		<category><![CDATA[NUMA]]></category>
		<category><![CDATA[P]]></category>
		<category><![CDATA[pool]]></category>
		<category><![CDATA[processor]]></category>
		<category><![CDATA[RWMutex]]></category>
		<category><![CDATA[SharedValue]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[TrueSharing]]></category>
		<category><![CDATA[VictoriaMetrics]]></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=4726</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal 大家好，我是Tony Bai。 在追求极致性能的道路上，Go 语言凭借其简洁的并发模型和高效的调度器，赢得了众多开发者的青睐。然而，随着现代服务器 CPU核心数量的不断攀升，一些我们曾经习以为常的“快速”操作，在高并发、多核环境下，也逐渐显露出其性能瓶颈。其中，原子操作 (atomic operations) 的扩展性问题，以及标准库中一些依赖原子操作的并发原语（如 sync.RWMutex）的性能表现，成为了社区热议的焦点。 最近，fasthttp 的作者及 VictoriaMetrics 数据库的联合创始人 Aliaksandr Valiakin (valyala) 在 X.com 上的一番“叹息”，更是将原子计数器的扩展性问题推向了前台： Valyala 指出：“基于原子操作的计数器更新性能在多 CPU 核心上无法扩展，因为每个 CPU 核心在增量操作期间都需要从慢速内存中原子加载实际的计数器值。因此，实际性能受限于内存延迟（约 15ns，即每秒 6 千万次增量）。通过使用可缓存于 CPU L1 缓存的 per-CPU 计数器，可以将单 CPU 核心性能提升至每秒数十亿次增量。遗憾的是，Go 语言本身并未提供高效处理 per-CPU 数据的函数。” 这番话点出了一个残酷的现实：即使是看似轻量级的原子操作，在多核“混战”中也可能成为性能的阿喀琉斯之踵。那么，这背后的深层原因是什么？Go 社区又在如何探索解决之道呢？今天，我们就来深入剖析这个问题，并解读 Go 项目 issue 中几个重要的相关提案，同时看看社区是如何先行一步尝试解决这类问题的。 原子操作为何在高并发多核下“失速”？sync.RWMutex 的痛点 要理解原子操作的瓶颈，我们需要潜入到 CPU 缓存的微观世界。现代多核 CPU 为了加速内存访问，都配备了多级缓存（L1, L2, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/shardedvalue-per-cpu-proposal-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal">本文永久链接</a> &#8211; https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal</p>
<p>大家好，我是Tony Bai。</p>
<p>在追求极致性能的道路上，Go 语言凭借其简洁的并发模型和高效的调度器，赢得了众多开发者的青睐。然而，随着现代服务器 CPU核心数量的不断攀升，一些我们曾经习以为常的“快速”操作，在高并发、多核环境下，也逐渐显露出其性能瓶颈。其中，<strong>原子操作 (atomic operations)</strong> 的扩展性问题，以及标准库中一些依赖原子操作的并发原语（如 sync.RWMutex）的性能表现，成为了社区热议的焦点。</p>
<p>最近，fasthttp 的作者及 VictoriaMetrics 数据库的联合创始人 Aliaksandr Valiakin (valyala) 在 X.com 上的一番“叹息”，更是将原子计数器的扩展性问题推向了前台：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/shardedvalue-per-cpu-proposal-2.png" alt="" /></p>
<p>Valyala 指出：“基于原子操作的计数器更新性能在多 CPU 核心上无法扩展，因为每个 CPU 核心在增量操作期间都需要从慢速内存中原子加载实际的计数器值。因此，实际性能受限于内存延迟（约 15ns，即每秒 6 千万次增量）。通过使用可缓存于 CPU L1 缓存的 per-CPU 计数器，可以将单 CPU 核心性能提升至每秒数十亿次增量。遗憾的是，Go 语言本身并未提供高效处理 per-CPU 数据的函数。”</p>
<p>这番话点出了一个残酷的现实：即使是看似轻量级的原子操作，在多核“混战”中也可能成为性能的阿喀琉斯之踵。那么，这背后的深层原因是什么？Go 社区又在如何探索解决之道呢？今天，我们就来深入剖析这个问题，并解读 Go 项目 issue 中几个重要的相关提案，同时看看社区是如何先行一步尝试解决这类问题的。</p>
<h2>原子操作为何在高并发多核下“失速”？sync.RWMutex 的痛点</h2>
<p>要理解原子操作的瓶颈，我们需要潜入到 CPU 缓存的微观世界。现代多核 CPU 为了加速内存访问，都配备了多级缓存（L1, L2, L3）。当多个核心同时读写同一块内存区域时，就需要<strong>缓存一致性协议 (Cache Coherence Protocols)</strong>（如 MESI，Modify-Exclusive-Shared-Invalid）来确保数据的一致性。</p>
<p>当我们对一个共享变量（即使是原子变量）进行写操作时，例如 atomic.AddInt64，会发生什么？</p>
<ol>
<li>执行该操作的 CPU 核心需要获得对该变量所在<strong>缓存行 (Cache Line)</strong> 的独占访问权 (Exclusive state)。</li>
<li>如果其他核心的缓存中也存在这份缓存行的副本（即使是共享状态 Shared state），它们会被标记为无效 (Invalidate)。</li>
<li>当其他核心再次需要访问这个变量时，就会发生缓存未命中 (Cache Miss)，需要从更高级别的缓存或主内存中重新加载数据，并可能再次引发缓存行在不同核心间的同步。</li>
</ol>
<p>在高并发场景下，如果多个核心<strong>频繁地对同一个缓存行中的原子变量进行写操作</strong>，就会导致：</p>
<ul>
<li><strong>缓存行在不同核心的 L1/L2 缓存之间频繁失效和同步</strong>，这个过程被称为“缓存行乒乓 (Cache Line Ping-Ponging)”。</li>
<li>产生大量的<strong>总线流量和内存访问延迟</strong>。</li>
</ul>
<p>这就是所谓的<strong>真共享 (True Sharing)</strong> 争用。即使原子操作本身在单个核心上执行得非常快，这种跨核心的缓存同步开销也会让其整体性能急剧下降。</p>
<p>这个问题的典型体现之一，便是 Go 标准库中的 sync.RWMutex。正如 github.com/jonhoo/drwmutex 项目在其 README 中指出的：“Go 默认的 sync.RWMutex 在多核下扩展性不佳，因为所有读操作者在尝试原子性地增加同一个内存位置（用于读者计数）时会产生争用。” 对于读多写少的场景，本应高效的读锁操作，却因为内部共享计数器的原子更新而受到了性能限制。</p>
<h2>社区的先行者：jonhoo/drwmutex 的分片读写锁实践</h2>
<p>面对标准库 sync.RWMutex 在多核环境下的扩展性瓶颈，社区早已开始了积极的探索。一个显著的例子便是 jonhoo/drwmutex，一个 n 路分片读写锁（Distributed Read-Write Mutex）的实现，也被称为“大读者”锁。</p>
<p>其核心思想非常直观：<strong>为每个 CPU 核心提供其自己的 RWMutex 实例。读者只需要获取其核心本地的读锁，而写者则必须按顺序获取所有核心上的锁。</strong> 这种设计通过将读操作的争用分散到各个核心，从而显著提升了读多写少场景下的并发性能。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/shardedvalue-per-cpu-proposal-3.png" alt="" /></p>
<p>jonhoo/drwmutex 的实现也揭示了构建这类 per-CPU 优化方案的一些关键技术点和挑战：</p>
<ul>
<li><strong>获取当前 CPU ID：</strong> 为了将操作路由到正确的本地锁，需要一种方法来确定当前 goroutine 正在哪个 CPU 核心上运行。drwmutex 在 Linux x86 平台上使用了 CPUID 汇编指令来获取 APICID，并在程序启动时构建 APICID 到 CPU 索引的映射。这突显了获取可靠且高效的 CPU/P 标识是实现此类优化的一个难点。</li>
<li><strong>CPU 信息可能过时：</strong> README 中也坦诚地指出，goroutine 获取到的 CPU 信息可能是过时的（因为 goroutine 可能已被调度到其他核心），但这主要影响性能而非正确性（只要读者记住它获取的是哪个锁）。OS 内核通常会尽量将线程保持在同一核心以提高缓存命中率，这在一定程度上缓解了这个问题。</li>
<li><strong>性能表现与 NUMA 效应：</strong> jonhoo/drwmutex 的性能测试表明，在核心数较多，特别是写操作比例低于 1% 时，其性能远超 sync.RWMutex。有趣的是，其性能图表还揭示了 NUMA (Non-Uniform Memory Access) 效应的影响——在测试机器上每增加一个包含 10 个核心的 NUMA 节点，跨核心流量的成本就会增加，导致性能曲线出现波动。</li>
</ul>
<p>jonhoo/drwmutex 的实践不仅提供了一个解决 sync.RWMutex 性能问题的有效方案，也为后续 Go 官方和社区在 per-CPU 数据结构方面的探索提供了宝贵的经验和参照。</p>
<h2>官方的早期探索：sync.ShardedValue 的初心与挑战 (#18802)</h2>
<p>在社区积极探索的同时，Go 核心团队也早已关注到这类问题。一个重要的早期官方提案便是由 Austin Clements 在 2017 年提出的 <strong>sync.ShardedValue (issue #18802)</strong>。</p>
<p>sync.ShardedValue 的核心思想与 jonhoo/drwmutex 有异曲同工之妙：<strong>提供一种机制来创建和使用分片值，将一个逻辑上的共享值分散到多个独立的“分片”中，每个分片与一个 CPU 核心或更准确地说是 Go 调度器中的 P (Processor) 相关联。</strong> 这样，每个 P 上的 goroutine 优先访问其本地分片，从而大大减少对单一共享内存位置的争用。</p>
<p>该提案围绕 Get()、Put() 和 Do() 等核心 API 进行了深入讨论，涉及了诸多设计维度，例如 Get/Put 的阻塞性、溢出处理、Do 操作的一致性等。尽管因难以就“最重要的问题达成共识”而被搁置，但 sync.ShardedValue 提案为后续的探索奠定了重要的基础，并清晰地指明了通过“分片”来提升多核扩展性的方向。</p>
<h2>新的尝试：valyala 的 sync.PLocalCache (#69229) 与 sync.MLocal (#73667)</h2>
<p>近期，valyala 基于其在 fasthttp 和 VictoriaMetrics 等高性能项目中的实践经验，提出了两个更聚焦、API 更简洁的提案，试图从特定场景切入，解决 per-CPU/per-P/per-M 数据的高效访问问题。</p>
<p><strong>1. sync.PLocalCache (issue #69229): Per-P 对象缓存</strong></p>
<ul>
<li><strong>设计目标：</strong> 为 CPU 密集型的算法提供一个高效且可随 CPU 核心数线性扩展的<strong>状态缓存机制</strong>。</li>
<li><strong>API 设计：</strong> 核心是 Get() (返回 P 本地对象，若无则返回 nil) 和 Put() (将对象放回 P 本地存储)，保证 Get() 返回的对象只能被当前 goroutine 访问，无需额外同步。</li>
<li><strong>解决痛点：</strong> 旨在解决 sync.Pool 在作为严格 per-P 缓存时存在的问题，如跨 P 窃取、内存浪费和 GC 清理等。</li>
</ul>
<p><strong>2. sync.MLocal[T any] (issue #73667): Per-M (OS 线程) 泛型存储</strong></p>
<ul>
<li><strong>设计目标：</strong> 为需要在 OS 线程层面实现数据隔离以达到线性扩展性的并发代码，提供 M 本地存储。</li>
<li><strong>API 设计 (泛型)：</strong> 提供 Get() (返回当前 M 的 *T 项) 和 All() (返回所有 M 上的项)。</li>
<li><strong>解决痛点：</strong> 直接应对 valyala 在 VictoriaMetrics 中遇到的共享缓冲区互斥锁争用导致的扩展性瓶颈。</li>
</ul>
<h2>这些提案的共性、差异与启示</h2>
<p>无论是社区的 jonhoo/drwmutex 实践，还是官方及 valyala 的提案，它们的核心目标都是一致的：<strong>通过数据的分片或本地化，最大限度地减少多核间的共享内存争用，从而提升高并发应用在多核处理器上的性能和可伸缩性。</strong></p>
<p>然而，它们在具体实现、API 设计的通用性、易用性以及针对的场景上有所不同：</p>
<ul>
<li>jonhoo/drwmutex 是一个针对特定问题（读写锁）的具体解决方案，它依赖平台相关的 CPUID 指令，并自己处理了核心映射和数据同步。</li>
<li>sync.ShardedValue 试图提供一个更通用的分片值抽象，但也因此面临更大的设计复杂性和社区共识挑战。Austin Clements 后续也反思了早期设计，并提出了更优的“检出/检入”模型。</li>
<li>sync.PLocalCache 和 sync.MLocal 则更为聚焦，API 更简洁，分别针对 per-P 缓存和 per-M 存储这两个具体场景。</li>
</ul>
<p>这些探索过程也充满了 Go 社区对技术细节的极致追求和严谨思辨，例如关于命名（”sharding” vs “perCPU” vs “SplitValue”）、GOMAXPROCS 动态变化的影响、与 GC 的交互、API 语义的精确性（如 mknyszek 提出的包含 Merge 方法的 ShardedValue API 及其多种语义可能）以及泛型的应用等。</p>
<h2>展望未来：Go 如何更好地拥抱多核时代？</h2>
<p>原子操作的瓶颈、标准库并发原语的局限，以及社区和官方对 per-CPU/P/M 存储方案的持续探索，清晰地表明了 Go 语言在追求极致多核扩展性方面仍有提升空间。解决这类底层并发原语的性能问题，对于 Go 在高性能服务器、大规模分布式系统、数据库、监控系统等领域的持续领先至关重要。</p>
<p>未来，我们或许会看到：</p>
<ul>
<li><strong>更底层的运行时支持：</strong> Go 运行时可能会暴露更底层的、与调度器（P、M）相关的亲和性原语，或提供高效获取当前 P/核心 ID 的标准方法，正如 jonhoo/drwmutex 所尝试的那样。</li>
<li><strong>标准库中出现新的同步原语：</strong> 借鉴这些提案和社区实践的精华，可能会有新的、经过精心设计的同步原语加入到 sync 或 sync/atomic 包中。</li>
<li><strong>社区持续贡献优秀的解决方案：</strong> 像 jonhoo/drwmutex 这样的项目，即使官方没有立即提供标准方案，社区也会基于现有技术孵化出优秀的第三方库。</li>
</ul>
<h2>小结</h2>
<p>从 valyala 对原子操作性能的“叹息”，到 jonhoo/drwmutex 的巧妙实践，再到 Go 社区围绕 sync.ShardedValue、sync.PLocalCache、sync.MLocal 等提案的深入探讨，我们看到了 Go 语言在追求极致性能道路上永不停歇的脚步。这不仅仅是关于几个新的 API，更是关于 Go 如何在多核时代继续保持其并发优势和工程效率的战略思考。</p>
<p>作为 Gopher，关注这些讨论和提案的进展，理解其背后的设计哲学和技术挑战，不仅能让我们更深刻地认识 Go 语言，也能启发我们在自己的高性能项目中进行类似的性能优化思考和实践。</p>
<p>让我们共同期待 Go 在多核扩展性方面能迈出更坚实的步伐，为构建更高性能的未来系统提供更强大的动力！</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://github.com/jonhoo/drwmutex">Distributed Read-Write Mutex in Go</a> &#8211; https://github.com/jonhoo/drwmutex</li>
<li><a href="https://github.com/golang/go/issues/18802">proposal: sync: support for sharded values #18802</a> &#8211; https://github.com/golang/go/issues/18802</li>
<li><a href="https://github.com/golang/go/issues/73667">proposal: sync: add M-local storage #73667</a> &#8211; https://github.com/golang/go/issues/73667</li>
<li><a href="https://github.com/golang/go/issues/69229">proposal: sync: add PLocalCache #69229</a> &#8211; https://github.com/golang/go/issues/69229</li>
</ul>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你的 Go 项目中，是否也曾遇到过原子操作或 sync.RWMutex 在高并发多核下的性能瓶颈？你是如何解决的？是否尝试过类似 jonhoo/drwmutex 的分片锁方案？</strong></li>
<li><strong>对于 Go 社区提出的这些 per-CPU/P/M 存储提案，你认为哪种设计思路更具潜力？或者你有什么更好的建议？</strong></li>
<li><strong>你认为 Go 语言在提升多核扩展性方面，未来最应该关注哪些方向？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的经验、思考和问题。如果你觉得这篇文章对你有所启发，也请<strong>转发给你身边的 Gopher 朋友们</strong>，让更多人参与到这场关于 Go 性能未来的讨论中来！</p>
<p><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/05/19/shardedvalue-per-cpu-proposal/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>惊！Go在十亿次循环和百万任务中表现不如Java，究竟为何？</title>
		<link>https://tonybai.com/2024/12/02/why-go-sucks/</link>
		<comments>https://tonybai.com/2024/12/02/why-go-sucks/#comments</comments>
		<pubDate>Sun, 01 Dec 2024 22:08:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Atoi]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[gnet]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Intn]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[lensm]]></category>
		<category><![CDATA[LoopUnrolling]]></category>
		<category><![CDATA[optimize]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[random]]></category>
		<category><![CDATA[real]]></category>
		<category><![CDATA[RES]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[strconv]]></category>
		<category><![CDATA[sys]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[top]]></category>
		<category><![CDATA[Uint32]]></category>
		<category><![CDATA[User]]></category>
		<category><![CDATA[waitgroup]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[寄存器]]></category>
		<category><![CDATA[循环]]></category>
		<category><![CDATA[循环展开]]></category>
		<category><![CDATA[指令]]></category>
		<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=4418</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/02/why-go-sucks 编程语言比较的话题总是能吸引程序员的眼球！ 近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由Ben Dicken (@BenjDicken) 做的语言性能测试，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的内存开销测试，对比了多种语言在处理百万任务时的内存开销。 下面是这两项测试的结果示意图： 10亿循环测试结果 百万任务内存开销测试结果 我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。 那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。 我们先从十亿次循环测试开始。 1. 循环测试跑的慢，都因编译器优化还不够 下面是作者给出的Go测试程序： // why-go-sucks/billion-loops/go/code.go package main import ( "fmt" "math/rand" "os" "strconv" ) func main() { input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line if e != nil { panic(e) } u [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/02/why-go-sucks">本文永久链接</a> &#8211; https://tonybai.com/2024/12/02/why-go-sucks</p>
<p>编程语言比较的话题总是能吸引程序员的眼球！</p>
<p>近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由<a href="https://benjdd.com">Ben Dicken (@BenjDicken)</a> 做的<a href="https://benjdd.com/languages/">语言性能测试</a>，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的<a href="https://hez2010.github.io/async-runtimes-benchmarks-2024/">内存开销测试</a>，对比了多种语言在处理百万任务时的内存开销。</p>
<p>下面是这两项测试的结果示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-2.png" alt="" /><br />
<center>10亿循环测试结果</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-3.png" alt="" /><br />
<center>百万任务内存开销测试结果</center></p>
<p>我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。</p>
<p>那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。</p>
<p>我们先从<strong>十亿次循环测试</strong>开始。</p>
<h2>1. 循环测试跑的慢，都因编译器优化还不够</h2>
<p>下面是作者给出的<a href="https://github.com/bddicken/languages/blob/main/loops/go/code.go">Go测试程序</a>：</p>
<pre><code>// why-go-sucks/billion-loops/go/code.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            a[i] = a[i] + j%u // Simple sum
        }
        a[i] += r // Add a random value to each element in array
    }
    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>这段代码通过命令行参数获取一个整数，然后生成一个随机数，接着通过两层循环对一个数组的每个元素进行累加，最终输出该数组中以随机数为下标对应的数组元素的值。</p>
<p>我们再来看一下”竞争对手”的测试代码。C测试代码如下：</p>
<pre><code>// why-go-sucks/billion-loops/c/code.c

#include "stdio.h"
#include "stdlib.h"
#include "stdint.h"

int main (int argc, char** argv) {
  int u = atoi(argv[1]);               // Get an input number from the command line
  int r = rand() % 10000;              // Get a random integer 0 &lt;= r &lt; 10k
  int32_t a[10000] = {0};              // Array of 10k elements initialized to 0
  for (int i = 0; i &lt; 10000; i++) {    // 10k outer loop iterations
    for (int j = 0; j &lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration
      a[i] = a[i] + j%u;               // Simple sum
    }
    a[i] += r;                         // Add a random value to each element in array
  }
  printf("%d\n", a[r]);                // Print out a single element from the array
}
</code></pre>
<p>下面是Java的测试代码：</p>
<pre><code>// why-go-sucks/billion-loops/java/code.java

package jvm;

import java.util.Random;

public class code {

    public static void main(String[] args) {
        var u = Integer.parseInt(args[0]); // Get an input number from the command line
        var r = new Random().nextInt(10000); // Get a random number 0 &lt;= r &lt; 10k
        var a = new int[10000]; // Array of 10k elements initialized to 0
        for (var i = 0; i &lt; 10000; i++) { // 10k outer loop iterations
            for (var j = 0; j &lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration
                a[i] = a[i] + j % u; // Simple sum
            }
            a[i] += r; // Add a random value to each element in array
        }
        System.out.println(a[r]); // Print out a single element from the array
    }
}
</code></pre>
<p>你可能不熟悉C或Java，但从代码的形式上来看，C、Java与Go的代码确实处于“同等条件”。这不仅意味着它们在相同的硬件和软件环境中运行，更包括它们采用了相同的计算逻辑和算法，以及一致的输入参数处理等方面的相似性。</p>
<p>为了确认一下原作者的测试结果，我在一台阿里云ECS上(amd64，8c32g，CentOS 7.9)对上面三个程序进行了测试(使用time命令测量计算耗时)，得到一个基线结果。我的环境下，C、Java和Go的编译器版本如下：</p>
<pre><code>$go version
go version go1.23.0 linux/amd64

$java -version
openjdk version "17.0.9" 2023-10-17 LTS
OpenJDK Runtime Environment Zulu17.46+19-CA (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Zulu17.46+19-CA (build 17.0.9+8-LTS, mixed mode, sharing)

$gcc -v
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
目标：x86_64-redhat-linux
配置为：../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
线程模型：posix
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
</code></pre>
<p>测试步骤与结果如下：</p>
<pre><code>Go代码测试：

$cd why-go-sucks/billion-loops/go
$go build -o code code.go
$time ./code 10
456953

real    0m3.766s
user    0m3.767s
sys 0m0.007s

C代码测试：

$cd why-go-sucks/billion-loops/c
$gcc -O3 -std=c99 -o code code.c
$time ./code 10
459383

real    0m3.005s
user    0m3.005s
sys 0m0.000s

Java代码测试：

$javac -d . code.java
$time java -cp . jvm.code 10
456181

real    0m3.105s
user    0m3.092s
sys 0m0.027s
</code></pre>
<p>从测试结果看到(基于real时间)：采用-O3优化的C代码最快，Java落后一个身位，而<strong>Go则比C慢了25%，比Java慢了21%</strong>。</p>
<blockquote>
<p>注：time命令的输出结果通常包含三个主要部分：real、user和sys。real是从命令开始执行到结束所经过的实际时间（墙钟时间），我们依次指标为准。user是程序在<strong>用户模式下执行所消耗的CPU时间</strong>。sys则是程序<strong>在内核模式下执行所消耗的CPU时间（系统调用）</strong>。如果总时间（real）略低于用户时间（user），这表明程序可能在某些时刻被调度或等待，而不是持续占用CPU。这种情况可能是由于输入输出操作、等待资源等原因。如果real时间显著小于user时间，这种情况通常发生在并发程序中，其中多个线程或进程在不同的时间段执行，导致总的用户CPU时间远大于实际的墙钟时间。sys时间保持较低，说明系统调用的频率较低，程序主要是执行计算而非进行大量的系统交互。</p>
</blockquote>
<p>这时作为Gopher的你可能会说：<strong>原作者编写的Go测试代码不够优化，我们能优化到比C还快</strong>！</p>
<p>大家都知道原代码是不够优化的，随意改改计算逻辑就能带来大幅提升。但我们不能忘了“同等条件”这个前提。你采用的优化方法，其他语言（C、Java）也可以采用。</p>
<p>那么，在不改变“同等条件”的前提下，我们还能优化点啥呢？本着能提升一点是一点的思路，我们尝试从下面几个点优化一下，看看效果：</p>
<ul>
<li>去除不必要的if判断</li>
<li>使用更快的rand实现</li>
<li>关闭边界检查</li>
<li>避免逃逸</li>
</ul>
<p>下面是修改之后的代码：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_optimize.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, _ := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    u := int32(input)
    r := int32(rand.Uint32() % 10000)   // Use Uint32 for faster random number generation
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            a[i] = a[i] + j%u // Simple sum
        }
        a[i] += r // Add a random value to each element in array
    }
    z := a[r]
    fmt.Println(z) // Print out a single element from the array
}
</code></pre>
<p>我们编译并运行一下测试：</p>
<pre><code>$cd why-go-sucks/billion-loops/go
$go build -o code_optimize -gcflags '-B' code_optimize.go
$time ./code_optimize 10
459443

real    0m3.761s
user    0m3.759s
sys 0m0.011s
</code></pre>
<p>对比一下最初的测试结果，这些“所谓的优化”没有什么卵用，优化前你估计也能猜测到这个结果，因为除了关闭边界检查，其他优化都<strong>没有处于循环执行的热路径之上</strong>。</p>
<blockquote>
<p>注：rand.Uint32() % 10000的确要比rand.Intn(10000)快，我自己的benchmark结果是快约1倍。</p>
</blockquote>
<p>那Go程序究竟慢在哪里呢？在“同等条件”下，我能想到的只能是<strong>Go编译器后端在代码优化方面优化做的还不够</strong>，相较于GCC、Java等老牌编译器还有明显差距。</p>
<p>比如说，原先的代码中在内层循环中频繁访问a&#91;i&#93;，导致数组访问的读写操作较多（从内存加载a&#91;i&#93;，更新值后写回）。GCC和Java编译器在后端很可能做了这样的优化：将数组元素累积到一个临时变量中，并在外层循环结束后写回数组，这样做可以<strong>减少内层循环中的内存读写操作，充分利用CPU缓存和寄存器，加速数据处理</strong>。</p>
<blockquote>
<p>注：数组从内存或缓存读，而一个临时变量很大可能是从寄存器读，那读取速度相差还是很大的。</p>
</blockquote>
<p>如果我们手工在Go中实施这一优化，看看能达到什么效果呢？我们改一下最初版本的Go代码(code.go)，新代码如下：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_local_var.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        temp := a[i]
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            temp += j % u // Simple sum
        }
        temp += r // Add a random value to each element in array
        a[i] = temp
    }
    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>编译并运行测试：</p>
<pre><code>$go build -o code_local_var code_local_var.go
$time ./code_local_var 10
459169

real    0m3.017s
user    0m3.017s
sys 0m0.007s
</code></pre>
<p>我们看到，测试结果直接就比Java略好一些了。显然Go编译器没有做这种优化，从code.go的汇编也大致可以看出来：</p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-4.png" alt="" /><br />
<center>使用<a href="https://github.com/loov/lensm">lensm</a>生成的汇编与go源码对应关系</center></p>
<p>而Java显然做了这类优化，我们在原Java代码版本上按上述优化逻辑修改了一下：</p>
<pre><code>// why-go-sucks/billion-loops/java/code_local_var.java

package jvm;

import java.util.Random;

public class code {

    public static void main(String[] args) {
        var u = Integer.parseInt(args[0]); // 获取命令行输入的整数
        var r = new Random().nextInt(10000); // 生成随机数 0 &lt;= r &lt; 10000
        var a = new int[10000]; // 定义长度为10000的数组a

        for (var i = 0; i &lt; 10000; i++) { // 10k外层循环迭代
            var temp = a[i]; // 使用临时变量存储 a[i] 的值
            for (var j = 0; j &lt; 100000; j++) { // 100k内层循环迭代，每次外层循环迭代
                temp += j % u; // 更新临时变量的值
            }
            a[i] = temp + r; // 将临时变量的值加上 r 并写回数组
        }
        System.out.println(a[r]); // 输出 a[r] 的值
    }
}
</code></pre>
<p>但从运行这个“优化”后的程序的结果来看，其对java代码的提升幅度几乎可以忽略不计：</p>
<pre><code>$time java -cp . jvm.code 10
450375

real    0m3.043s
user    0m3.028s
sys 0m0.027s
</code></pre>
<p>这也直接证明了即便采用的是原版java代码，java编译器也会生成带有抽取局部变量这种优化的可执行代码，java程序员无需手工进行此类优化。</p>
<p>像这种编译器优化，还有不少，比如大家比较熟悉的循环展开(Loop Unrolling)也可以提升Go程序的性能：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_loop_unrolling.go

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        var sum int32
        // Unroll inner loop in chunks of 4 for optimization
        for j := int32(0); j &lt; 100000; j += 4 {
            sum += j % u
            sum += (j + 1) % u
            sum += (j + 2) % u
            sum += (j + 3) % u
        }
        a[i] = sum + r // Add the accumulated sum and random value
    }

    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>运行这个Go测试程序，性能如下：</p>
<pre><code>$go build -o code_loop_unrolling code_loop_unrolling.go
$time ./code_loop_unrolling 10
458908

real    0m2.937s
user    0m2.940s
sys 0m0.002s
</code></pre>
<p>循环展开可以增加指令级并行性，因为展开后的代码块中可以有更多的独立指令，比如示例中的计算j % u、(j+1) % u、(j+2) % u和(j+3) % u，这些计算操作是独立的，可以并行执行，打破了依赖链，从而更好地利用处理器的并行流水线。而原版Go代码中，每次迭代都会根据前一次迭代的结果更新a&#91;i&#93;，形成一个依赖链，这种顺序依赖性迫使处理器只能按顺序执行这些指令，导致流水线停顿。</p>
<p>不过其他语言也可以做同样的手工优化，比如我们对C代码做同样的优化(why-go-sucks/billion-loops/c/code_loop_unrolling.c)，c测试程序的性能可以提升至2.7s水平，这也证明了初版C程序即便在-O3的情况下编译器也没有自动为其做这个优化：</p>
<pre><code>$time ./code_loop_unrolling 10
459383

real    0m2.723s
user    0m2.722s
sys 0m0.001s
</code></pre>
<p>到这里我们就不再针对这个10亿次循环的性能问题做进一步展开了，从上面的探索得到的初步结论就是<strong>Go编译器优化做的还不到位所致</strong>，期待后续Go团队能在编译器优化方面投入更多精力，争取早日追上GCC/Clang、Java这些成熟的编译器优化水平。</p>
<p>下面我们再来看Go在百万任务场景下内存开销大的“问题”。</p>
<h2>2. 内存占用高，问题出在Goroutine实现原理</h2>
<p>我们先来看第二个问题的测试代码：</p>
<pre><code>package main

import (
    "fmt"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    numRoutines := 100000
    if len(os.Args) &gt; 1 {
        n, err := strconv.Atoi(os.Args[1])
        if err == nil {
            numRoutines = n
        }
    }

    var wg sync.WaitGroup
    for i := 0; i &lt; numRoutines; i++ {
        wg.Add(1)
        go func() {
            time.Sleep(10 * time.Second)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>这个代码其实就是根据传入的task数量启动等同数量的goroutine，然后每个goroutine模拟工作负载sleep 10s，这等效于百万长连接的场景，只有连接，但没有收发消息。</p>
<p>相对于上一个问题，这个问题更好解释一些。</p>
<p>Go使用的groutine是一种有栈协程，文章中使用的是每个task一个goroutine的模型，且维护百万任务一段时间，这会真实创建百万个goroutine（G数据结构），并为其分配栈空间(2k起步)，这样你可以算一算，不考虑其他结构的占用，仅每个goroutine的栈空间所需的内存都是极其可观的：</p>
<pre><code>mem = 1000000 * 2000 Bytes = 2000000000 Bytes = 2G Bytes
</code></pre>
<p>所以启动100w goroutine，保底就2GB内存出去了，这与原作者测试的结果十分契合(原文是2.5GB多)。并且，内存还会随着goroutine数量增长而线性增加。</p>
<p>那么如何能减少内存使用呢？如果采用每个task一个goroutine的模型，这个内存占用很难省去，除非将来Go团队对goroutine实现做大修。</p>
<p>如果task是网络通信相关的，可以使用类似gnet这样的直接基于epoll建构的框架，其主要的节省在于不会启动那么多goroutine，而是通过一个goroutine池来处理数据，每个池中的goroutine负责一批网络连接或网络请求。</p>
<p>在一些Gopher的印象中，Goroutine一旦分配就不回收，这会使他们会误认为一旦分配了100w goroutine，这2.5G内存空间将始终被占用，真实情况是这样么？我们用一个示例程序验证一下就好了：</p>
<pre><code>// why-go-sucks/million-tasks/million-tasks.go

package main

import (
    "fmt"
    "log"
    "os"
    "os/signal"
    "runtime"
    "sync"
    "syscall"
    "time"
)

// 打印当前内存使用情况和相关信息
func printMemoryUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&amp;m)

    // 获取当前 goroutine 数量
    numGoroutines := runtime.NumGoroutine()

    // 获取当前线程数量
    numThreads := runtime.NumCPU() // Go runtime 不直接提供线程数量，但可以通过 NumCPU 获取逻辑处理器数量

    fmt.Printf("======&gt;\n")
    fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
    fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
    fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
    fmt.Printf("\tNumGC = %v", m.NumGC)
    fmt.Printf("\tNumGoroutines = %v", numGoroutines)
    fmt.Printf("\tNumThreads = %v\n", numThreads)
    fmt.Printf("&lt;======\n\n")
}

// 将字节转换为 MB
func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

func main() {
    const signal1Goroutines = 900000
    const signal2Goroutines = 90000
    const signal3Goroutines = 10000

    // 用于接收退出信号
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    // 控制 goroutine 的退出
    signal1Chan := make(chan struct{})
    signal2Chan := make(chan struct{})
    signal3Chan := make(chan struct{})

    var wg sync.WaitGroup
    ticker := time.NewTicker(5 * time.Second)
    go func() {
        for range ticker.C {
            printMemoryUsage()
        }
    }()

    // 等待退出信号
    go func() {
        count := 0
        for {
            &lt;-sigChan
            count++
            if count == 1 {
                log.Println("收到第一类goroutine退出信号")
                close(signal1Chan) // 关闭 signal1Chan，通知第一类 goroutine 退出
                continue
            }
            if count == 2 {
                log.Println("收到第二类goroutine退出信号")
                close(signal2Chan) // 关闭 signal2Chan，通知第二类 goroutine 退出
                continue
            }
            log.Println("收到第三类goroutine退出信号")
            close(signal3Chan) // 关闭 signal3Chan，通知第三类 goroutine 退出
            return
        }
    }()

    // 启动第一类 goroutine（在收到 signal1 时退出）
    log.Println("开始启动第一类goroutine...")
    for i := 0; i &lt; signal1Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal1Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第一类goroutine(900000) ok")

    time.Sleep(time.Second * 5)

    // 启动第二类 goroutine（在收到 signal2 时退出）
    log.Println("开始启动第二类goroutine...")
    for i := 0; i &lt; signal2Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal2Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第二类goroutine(90000) ok")

    time.Sleep(time.Second * 5)

    // 启动第三类goroutine（随程序退出而退出）
    log.Println("开始启动第三类goroutine...")
    for i := 0; i &lt; signal3Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal3Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第三类goroutine(90000) ok")

    // 等待所有 goroutine 完成
    wg.Wait()
    fmt.Println("所有 goroutine 已退出，程序结束")
}
</code></pre>
<p>这个程序我就不详细解释了。大致分三类goroutine，第一类90w个，在我发送第一个ctrl+c信号后退出，第二类9w个，在我发送第二个ctrl+c信号后退出，最后一类1w个，随着程序退出而退出。</p>
<p>在我的执行环境下编译和执行一下这个程序，并结合runtime输出以及使用top -p pid的方式查看其内存占用：</p>
<pre><code>$go build million-tasks.go
$./million-tasks 

2024/12/01 22:07:03 开始启动第一类goroutine...
2024/12/01 22:07:05 启动第一类goroutine(900000) ok
======&gt;
Alloc = 511 MiB TotalAlloc = 602 MiB    Sys = 2311 MiB  NumGC = 9   NumGoroutines = 900004  NumThreads = 8
&lt;======

2024/12/01 22:07:10 开始启动第二类goroutine...
2024/12/01 22:07:11 启动第二类goroutine(90000) ok
======&gt;
Alloc = 577 MiB TotalAlloc = 668 MiB    Sys = 2553 MiB  NumGC = 9   NumGoroutines = 990004  NumThreads = 8
&lt;======

2024/12/01 22:07:16 开始启动第三类goroutine...
2024/12/01 22:07:16 启动第三类goroutine(90000) ok
======&gt;
Alloc = 597 MiB TotalAlloc = 688 MiB    Sys = 2593 MiB  NumGC = 9   NumGoroutines = 1000004 NumThreads = 8
&lt;======

======&gt;
Alloc = 600 MiB TotalAlloc = 690 MiB    Sys = 2597 MiB  NumGC = 9   NumGoroutines = 1000004 NumThreads = 8
&lt;======
... ...

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 1000004 NumThreads = 8
&lt;======
</code></pre>
<p>100w goroutine全部创建ok后，我们查看一下top输出：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875556   2.5g    988 S  54.0  8.2   0:30.92 million-tasks
</code></pre>
<p>我们看到RES为2.5g，和我们预期的一致！</p>
<p>接下来，我们停掉第一批90w个goroutine，看RES是否会下降，何时会下降！</p>
<p>输入ctrl+c，停掉第一批90w goroutine：</p>
<pre><code>^C2024/12/01 22:10:15 收到第一类goroutine退出信号
======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 723198  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 723198  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 100004  NumThreads = 8
&lt;======
... ...
</code></pre>
<p>但同时刻的top显示RES并没有变化：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812   2.5g    988 S   0.0  8.2   0:56.38 million-tasks
</code></pre>
<p>等待两个GC间隔的时间后(大约4分)，Goroutine的栈空间被释放：</p>
<pre><code>======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======
</code></pre>
<p>top显示RES从2.5g下降为大概700多MB（RES的单位是KB）：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 764136    992 S   0.0  2.4   1:01.87 million-tasks
</code></pre>
<p>接下来，我们再停掉第二批9w goroutine：</p>
<pre><code>^C2024/12/01 22:16:21 收到第二类goroutine退出信号
======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 10004   NumThreads = 8
&lt;======

</code></pre>
<p>此时，top值也没立即改变：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 764136    992 S   0.0  2.4   1:05.99 million-tasks
</code></pre>
<p>大约等待一个GC间隔(2分钟)后，top中RES下降：</p>
<pre><code>======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======
</code></pre>
<p>RES变为不到700M：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 699156    992 S   0.0  2.2   1:06.75 million-tasks
</code></pre>
<p>第三次按下ctrl+c，程序退出：</p>
<pre><code>^C2024/12/01 22:18:46 收到第三类goroutine退出信号
======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10003   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10003   NumThreads = 8
&lt;======

所有 goroutine 已退出，程序结束
</code></pre>
<p>我们看到Go是会回收goroutine占用的内存空间的，并且归还给OS，只是这种归还比较lazy。尤其是，第二次停止goroutine前，go程序剩下10w goroutine，按理论来讲需占用大约200MB的空间，实际上却是700多MB；第二次停止goroutine后，goroutine数量降为1w，理论占用应该在20MB，但实际却是600多MB，我们看到go运行时这种lazy归还OS内存的行为可能也是“故意为之”，是为了避免反复从OS申请和归还内存。</p>
<h2>3. 小结</h2>
<p>本文主要探讨了Go语言在十亿次循环和百万任务的测试中的表现令人意外地逊色于Java和C语言的原因。我认为Go在循环执行中的慢速表现，主要是其编译器优化不足，影响了执行效率。 而在内存开销方面，Go的Goroutine实现是使得内存使用量大幅增加的“罪魁祸首”，这是由于每个Goroutine初始都会分配固定大小的栈空间。</p>
<p>通过本文的探讨，我的主要目的是希望大家不要以讹传讹，而是要搞清楚背后的真正原因，并正视Go在某些方面的不足，以及其当前在某些应用上下文中的局限性。 同时，也希望Go开发团队在编译器优化方面进行更多投入，以提升Go在高性能计算领域的竞争力。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/why-go-sucks">这里</a>下载。</p>
<h2>4. 参考资料</h2>
<ul>
<li><a href="https://benjdd.com/languages/">Billion nested loop iterations</a> &#8211; https://benjdd.com/languages/</li>
<li><a href="https://hez2010.github.io/async-runtimes-benchmarks-2024/">How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?</a> &#8211; https://hez2010.github.io/async-runtimes-benchmarks-2024/</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/12/02/why-go-sucks/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go unique包：突破字符串局限的通用值Interning技术实现</title>
		<link>https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/</link>
		<comments>https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/#comments</comments>
		<pubDate>Tue, 17 Sep 2024 22:06:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[go4org]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go标准库]]></category>
		<category><![CDATA[Handle]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Lisp]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[netaddr]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[std]]></category>
		<category><![CDATA[strong]]></category>
		<category><![CDATA[tailscale]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[value]]></category>
		<category><![CDATA[weak]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[值比较]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[内存优化]]></category>
		<category><![CDATA[内存管理]]></category>
		<category><![CDATA[哈希表]]></category>
		<category><![CDATA[字符串]]></category>
		<category><![CDATA[字符串interning]]></category>
		<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=4283</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/09/18/understand-go-unique-package-by-example Go的1.23版本中引入了一个新的标准库包unique，为Go开发者带来了高效的值interning能力。这种能力不仅适用于字符串类型值，还可应用于任何可比较(comparable)类型的值。 本文将简要探讨interning技术及其在Go中的实现方式，通过介绍unique包的功能，帮助读者更好地理解这一技术及其实际应用。 1. 从string interning技术说起 通常提到interning技术时，指的是传统的字符串驻留（string interning）技术。它是一种优化方法，旨在减少程序中重复字符串的内存占用，并提高字符串比较操作的效率。其基本原理是将相同的字符串值在内存中只存储一次，所有对该字符串的引用都指向同一内存地址，而不是为每个相同字符串创建单独的副本。下图展示了使用和不使用string interning技术的对比: 这个图直观地展示了string interning如何通过共享相同的字符串来节省内存和提高效率。我们看到：在不使用string interning的情况下，每个字符串都有自己的内存分配，即使内容相同，比如”Hello”字符串出现两次，占用了两块不同的内存空间。而在使用string interning的情况下，相同内容的字符串只存储一次，比如：两个”Hello”字符串引用指向同一个内存位置。 string interning在多种场景下非常有用，比如在解析文本格式(如XML、JSON)时，interning能高效处理标签名称经常重复的问题；在编译器或解释器的实现时，interning能够减少符号表中的重复项等。 传统的string interning通常使用哈希表或字典来存储字符串的唯一实例。每次出现新字符串时，程序首先会检查哈希表中是否已有相同的字符串，若存在则返回其引用，若不存在则将其存储在表中。 Michael Knyszek在Go官博介绍interning技术时，也给出了一个传统实现的代码片段： var internPool map[string]string // Intern returns a string that is equal to s but that may share storage with // a string previously passed to Intern. func Intern(s string) string { pooled, ok := [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/09/18/understand-go-unique-package-by-example">本文永久链接</a> &#8211; https://tonybai.com/2024/09/18/understand-go-unique-package-by-example</p>
<p><a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23">Go的1.23版本</a>中引入了一个<a href="https://pkg.go.dev/unique?ref=tonybai.com">新的标准库包unique</a>，为Go开发者带来了高效的值<a href="https://en.wikipedia.org/wiki/Interning_(computer_science)">interning能力</a>。这种能力不仅适用于字符串类型值，还可应用于任何可比较(comparable)类型的值。</p>
<p>本文将简要探讨interning技术及其在Go中的实现方式，通过介绍unique包的功能，帮助读者更好地理解这一技术及其实际应用。</p>
<h2>1. 从string interning技术说起</h2>
<p>通常提到interning技术时，指的是传统的字符串驻留（string interning）技术。它是一种优化方法，旨在<strong>减少程序中重复字符串的内存占用</strong>，并<strong>提高字符串比较操作的效率</strong>。其基本原理是将相同的字符串值在内存中只存储一次，所有对该字符串的引用都指向同一内存地址，而不是为每个相同字符串创建单独的副本。下图展示了使用和不使用string interning技术的对比:</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-2.png" alt="" /></p>
<p>这个图直观地展示了string interning如何通过共享相同的字符串来节省内存和提高效率。我们看到：在不使用string interning的情况下，每个字符串都有自己的内存分配，即使内容相同，比如”Hello”字符串出现两次，占用了两块不同的内存空间。而在使用string interning的情况下，相同内容的字符串只存储一次，比如：两个”Hello”字符串引用指向同一个内存位置。</p>
<p>string interning在多种场景下非常有用，比如在解析文本格式(如XML、JSON)时，interning能高效处理标签名称经常重复的问题；在编译器或解释器的实现时，interning能够减少符号表中的重复项等。</p>
<p>传统的string interning通常使用哈希表或字典来存储字符串的唯一实例。每次出现新字符串时，程序首先会检查哈希表中是否已有相同的字符串，若存在则返回其引用，若不存在则将其存储在表中。</p>
<p>Michael Knyszek在<a href="https://go.dev/blog/unique?ref=tonybai.com">Go官博介绍interning技术</a>时，也给出了一个传统实现的代码片段：</p>
<pre><code>var internPool map[string]string

// Intern returns a string that is equal to s but that may share storage with
// a string previously passed to Intern.
func Intern(s string) string {
    pooled, ok := internPool[s]
    if !ok {
        // Clone the string in case it's part of some much bigger string.
        // This should be rare, if interning is being used well.
        pooled = strings.Clone(s)
        internPool[pooled] = pooled
    }
    return pooled
}
</code></pre>
<p>这种实现虽然简单，但Knyszek指出了其存在几个问题：</p>
<ul>
<li>一旦字符串被intern，就永远不会被释放。</li>
<li>在多goroutine环境下使用需要额外的同步机制。</li>
<li>仅限于字符串类型值，不能用于其他类型的值。</li>
</ul>
<p>Go 1.23版本引入的unique包就是string interning技术的一种Go官方实现，当然就像前面所说，unique包不仅仅支持传统的string interning，还支持任何支持比较的类型的值的interning。</p>
<p>不过，在介绍unique包之前，我们简单看看这些年来Go社区对interning技术的贡献。</p>
<h2>2. Go社区interning技术的实现简史</h2>
<p>由于其他主流语言都或多或少有了对string interning的支持，Go社区显然也需要这样的包，在Go issues列表中，我能找到的最早提出在Go中添加interning技术实现的是2013年go核心开发人员Brad Fitzpatrick提出的”<a href="https://github.com/golang/go/issues/5160?ref=tonybai.com">proposal: runtime: optionally allow callers to intern strings</a>“。</p>
<p>2019年，Josh Bleecher Snyder发表了一篇博文<a href="https://commaok.xyz/post/intern-strings?ref=tonybai.com">Interning strings in Go</a>，探讨了interning的Go实现方法，并给出一个<a href="https://github.com/josharian/intern">简单但重度使用sync.Pool的interning实现</a>，该实现支持对string和字节切片的interning。</p>
<p>2021年，tailscale为了实现<a href="https://github.com/inetaf/netaddr">可以高效表示ip地址的netaddr包</a>，构建和开源了<a href="https://github.com/go4org/intern">go4.org/intern包</a>，这是一个可用于量产级别的interning实现。</p>
<blockquote>
<p>注：go4.org中这个go4的名字很可能就是因为go4.org这个组织只有四个contributors：Brad Fitzpatrick、Josh Bleecher Snyder、Dave Anderson和Matt Layher。之前的一篇文章《<a href="https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc/">理解unsafe-assume-no-moving-gc包</a>》中的unsafe-assume-no-moving-gc包也是go4.org下面的。</p>
</blockquote>
<p>之后，Brad Fitzpatrick将inetaf/netaddr包的实现合并到了Go标准库net/netip中，而netaddr包依赖的go4.org/intern包也被移入Go项目，变为internal/intern包，并被net/netip包所使用。</p>
<p>直到2023年9月，mknyszek提出”<a href="https://github.com/golang/go/issues/62483">unique: new package with unique.Handle</a>“的proposal，给出unique包的API设计和参考实现。unique落地后，原先使用internal/intern包的net/netip也都改为使用unique包了，internal/intern在Go 1.23版本被移除。</p>
<p>接下来，我们来看看这篇文章的主角unique包。</p>
<h2>3. Go的unique包介绍</h2>
<p>相较于传统的interning实现以及Go社区之前的实现，Go 1.23引入的unique包提供了一个更加通用和高效的interning实现方案。下面我们就分别从API、unique包的优势以及实现原理等几个方面介绍一下这个包。</p>
<h3>3.1 unique包的API</h3>
<p>从用户角度看，unique包提供的核心API非常简洁：</p>
<pre><code>$go doc unique.Handle
package unique // import "unique"

type Handle[T comparable] struct {
    // Has unexported fields.
}

func Make[T comparable](value T) Handle[T]
func (h Handle[T]) Value() T
</code></pre>
<p>Make函数就是unique包的”Intern”函数，它接受一个可比较类型的值，返回一个intern后的值，不过和前面那个传统实现方式的Intern函数不同，Make函数返回的是一个Handle&#91;T&#93;类型的值。针对同一个传给Make函数的值，返回的Handle&#91;T&#93;类型的值是相同的：</p>
<pre><code>// unique-examples/string_interning.go
package main

import "unique"

func main() {
    h1 := unique.Make("hello")
    h2 := unique.Make("hello")
    h3 := unique.Make("hello")
    h4 := unique.Make("golang")
    println(h1 == h2) // true
    println(h1 == h3) // true
    println(h1 == h4) // false
    println(h2 == h4) // false
}
</code></pre>
<p>unique包的作者Knyszek认为Handle&#91;T&#93;和Lisp语言中的Symbol十分类似，Symbol在Lisp中是interned后的字符串，Lisp确保相同的字符串只存储一次，提高内存存储和使用效率。</p>
<p>不过前面说了，unique不仅支持字符串值的interning，还支持其他可比较类型的值的interning，下面是一个int interning和一个自定义可比较类型的interning的例子：</p>
<pre><code>// unique-examples/int_interning.go

package main

import "unique"

func main() {
    var a, b int = 5, 6
    h1 := unique.Make(a)
    h2 := unique.Make(a)
    h3 := unique.Make(b)
    println(h1 == h2) // true
    println(h1 == h3) // false
}

// unique-examples/user_type_interning.go

package main

import "unique"

type UserType struct {
    a int
    z float64
    s string
}

func main() {
    var u1 = UserType{
        a: 5,
        z: 3.14,
        s: "golang",
    }
    var u2 = UserType{
        a: 5,
        z: 3.15,
        s: "golang",
    }
    h1 := unique.Make(u1)
    h2 := unique.Make(u1)
    h3 := unique.Make(u2)
    println(h1 == h2) // true
    println(h1 == h3) // false
}
</code></pre>
<blockquote>
<p>注：如果要intern的类型T是包含指针的结构体，这些指针指向的值几乎总是会逃逸到堆上。</p>
</blockquote>
<p>通过Make获得的Handle&#91;T&#93;的Value方法可以获取到interning值的原始值，我们看下面示例：</p>
<pre><code>// unique-examples/value.go
package main

import (
    "fmt"
    "unique"
)

type UserType struct {
    a int
    z float64
    s string
}

func main() {
    var u1 = UserType{
        a: 5,
        z: 3.14,
        s: "golang",
    }
    h1 := unique.Make(u1)
    h2 := unique.Make("hello, golang")
    h3 := unique.Make(567890)
    v1 := h1.Value()
    v2 := h2.Value()
    v3 := h3.Value()
    fmt.Printf("%T: %v\n", v1, v1) // main.UserType: {5 3.14 golang}
    fmt.Printf("%T: %v\n", v2, v2) // string: hello, golang
    fmt.Printf("%T: %v\n", v3, v3) // int: 567890
}
</code></pre>
<blockquote>
<p>注：Value方法返回的是值的浅拷贝，对于复合类型可能存在共享底层数据的情况。</p>
</blockquote>
<h3>3.2 unique包的实现原理</h3>
<p>传统的字符串interning实现起来可能并不难，但unique包的目标是设计支持可比较类型、interning值也可被GC且支持快速interning值比较的方案，unique包的实现涉及到hashtrimap、细粒度锁以及与runtime内gc相关函数结合的技术难题，因此其门槛还是很高的，即便是Go核心团队成员Knyszek实现的unique包，在Go 1.23发布后也被发现了<a href="https://github.com/golang/go/issues/69370">较为“严重”的bug</a>，该问题将<a href="https://github.com/golang/go/issues/69383">在Go 1.23.2版本修正</a>。</p>
<p>下面是一个unique包实现原理的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-3.png" alt="" /></p>
<p>上图展示了Make、Handle&#91;T&#93;和Value方法之间的关系，以及它们如何与内部的map(hashtrieMap)交互。</p>
<p>我们看到，图中三次调用Make(“hello”)都返回相同的Handle&#91;string&#93;{ptr1}，即无论调用多少次Make，对于相同的输入值，Make总是返回相同的Handle。</p>
<p>图中的Handle&#91;string&#93;{ptr1}是一个包含指向存储”hello”的内存位置指针的结构，所有三次Make调用返回的Handle都指向同一个内存位置。下面是Handle结构体的定义，看了你就明白了这句话的含义：</p>
<pre><code>// $GOROOT/src/unique/handle.go
type Handle[T comparable] struct {
    value *T
}
</code></pre>
<blockquote>
<p>注：这里Handle内部的指针&#42;T都是strong pointer(强指针)，以图中示例，只要有一个Handle实例(由Make返回的)存在，内存中的”hello”就不会被GC。</p>
</blockquote>
<p>Handle&#91;string&#93;{ptr1}的Value()方法返回存储的字符串值”hello”。</p>
<p>unique包有一个内部map(hashtrieMap)存储键值对，键是字符串”hello”的clone，值是一个weak.Pointer，指向存储实际字符串值的内存位置。weak.Pointer 是Go 1.23版本的内部包internal/weak中的一个类型，主要用于实现弱指针（weak pointer）的功能。weak.Pointer的主要作用是允许引用一个对象，而不会阻止该对象被垃圾收集器回收。具体来说，它允许你持有一个指向对象的指针，但当该对象的强指针消失时，垃圾收集器仍然可以回收该对象。下面是一张weak Pointer工作机制的示意图，展示了弱指针的生命周期以及对GC行为的影响：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-4.png" alt="" /></p>
<p>初始状态下，应用创建一个对象，同时创建一个强指针和一个weak.Pointer指向该对象。GC检查对象，但因为存在强指针，所以不能回收。强指针被移除，只剩下weak.Pointer指向对象。GC检查对象，发现没有强指针，于是回收对象。内存被释放，weak.Pointer变为nil。</p>
<p>由于weak包位于internal包中，它只能在Go的标准库或特定包中使用，我们只能用下面的伪代码来展示weak.Pointer的机制：</p>
<pre><code>package main

import (
    "fmt"
    "runtime"
    "unsafe"
    "internal/weak"
)

type MyStruct struct {
    name string
}

func main() {
    // 创建一个对象，obj可以理解为该对象的强指针
    obj := &amp;MyStruct{name: "object1"} 

    // 创建一个weak.Pointer指向obj，weakPtr是对obj指向内存的弱指针
    weakPtr := weak.Make(obj)

    // 显示对象的值，通过强指针和弱指针都可以
    fmt.Println("Before GC:", weakPtr.Value())
    fmt.Println("Before GC:", *obj)

    // 释放原始对象的强指针
    obj = nil

    // 强制执行GC，这时由于弱指针无法阻止GC，obj指向的内存可能被回收
    runtime.GC()

    // 查看弱指针是否仍然有效，这里不能直接使用obj，因为对象可能已经被回收
    fmt.Println("After GC:", weakPtr.Value())
}
</code></pre>
<p>弱指针有一些典型的使用场景，比如在缓存机制中，可能希望引用某些对象而不阻止它们被垃圾回收。这样可以在内存不足时自动释放不再使用的缓存对象；又比如在某些场景下，不希望对象长时间驻留在内存中，但仍然希望能够在需要时重新创建或加载它们，即延迟加载的对象；在某些数据结构中（如哈希表或链表），持有强指针可能会导致内存泄漏，弱指针可以有效避免这种情况。</p>
<blockquote>
<p>注：目前Knyszek已经提出proposal，<a href="https://github.com/golang/go/issues/67552">将weak包提升为标准库公共API</a>，该proposal已经被accept，最早将在Go 1.24版本落地。</p>
</blockquote>
<h3>3.3 unique包的优势</h3>
<p>从上面示例和原理示意图来看，unique包的设计和实现有几个显著的优势：</p>
<ul>
<li>泛型支持</li>
</ul>
<p>通过使用Go的泛型特性，unique包可以处理任何可比较的类型，大大扩展了其应用范围，不再局限于字符串类型。</p>
<ul>
<li>高效的内存管理</li>
</ul>
<p>unique包使用了运行时级别的弱指针实现，确保当所有相关的Handle&#91;T&#93;(即强指针)都不再被使用时，内部map中的值可以被垃圾回收，这既避免了内存长期占用，也避免了内存泄漏问题。</p>
<ul>
<li>快速比较操作</li>
</ul>
<p>Handle&#91;T&#93;类型的比较操作被优化为简单的指针比较，这比直接比较值(特别是对于大型结构体或长字符串内容)要快得多。</p>
<h3>3.4 unique包的实际应用</h3>
<p>unique包刚刚诞生，目前在Go标准库中的实际应用主要就是在net/netip包中，替代了之前由go4.org/intern移植到标准库中的internal/intern包。</p>
<p>net/netip包使用unique来优化Addr结构体中的addrDetail字段：</p>
<pre><code>type Addr struct {
    // 其他字段...

    // Details about the address, wrapped up together and canonicalized.
    z unique.Handle[addrDetail]
}

// addrDetail represents the details of an Addr, like address family and IPv6 zone.
type addrDetail struct {
    isV6   bool   // IPv4 is false, IPv6 is true.
    zoneV6 string // != "" only if IsV6 is true.
}

// z0, z4, and z6noz are sentinel Addr.z values.
// See the Addr type's field docs.
var (
    z0    unique.Handle[addrDetail]
    z4    = unique.Make(addrDetail{})
    z6noz = unique.Make(addrDetail{isV6: true})
)

// WithZone returns an IP that's the same as ip but with the provided
// zone. If zone is empty, the zone is removed. If ip is an IPv4
// address, WithZone is a no-op and returns ip unchanged.
func (ip Addr) WithZone(zone string) Addr {
    if !ip.Is6() {
        return ip
    }
    if zone == "" {
        ip.z = z6noz
        return ip
    }
    ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone})
    return ip
}
</code></pre>
<p>通过使用unique，net/netip包能够显著减少处理大量IP地址时的内存占用。特别是对于具有相同zone的IPv6地址，内存使用可以大幅降低。</p>
<p>下面我们也通过一个简单的示例来看看使用unique包的内存占用减少的效果。</p>
<h3>3.5 内存占用减少的效果</h3>
<p>现在我们创建100w个长字符串，这100w个字符串中，有1000种不同的字符串，相当于每种字符串有1000个重复值。下面分别用unique包和不用unique包来演示这个示例，看看内存占用情况：</p>
<pre><code>// unique-examples/effect_with_unique.go 

package main

import (
    "fmt"
    "runtime"
    "strings"
    "unique"
)

const (
    numItems    = 1000000
    stringLen   = 20
    numDistinct = 1000
)

func main() {
    // 创建一些不同的字符串
    distinctStrings := make([]string, numDistinct)
    for i := 0; i &lt; numDistinct; i++ {
        distinctStrings[i] = strings.Repeat(string(rune('A'+i%26)), stringLen)
    }

    // 使用unique包
    withUnique := make([]unique.Handle[string], numItems)
    for i := 0; i &lt; numItems; i++ {
        withUnique[i] = unique.Make(distinctStrings[i%numDistinct])
    }

    runtime.GC() // 强制GC
    printMemUsage("With unique")

    runtime.KeepAlive(withUnique)
}

func printMemUsage(label string) {
    var m runtime.MemStats
    runtime.ReadMemStats(&amp;m)
    fmt.Printf("%s:\n", label)
    fmt.Printf("  Alloc = %v MiB\n", bToMb(m.Alloc))
    fmt.Printf("  TotalAlloc = %v MiB\n", bToMb(m.TotalAlloc))
    fmt.Printf("  Sys = %v MiB\n", bToMb(m.Sys))
    fmt.Printf("  HeapAlloc = %v MiB\n", bToMb(m.HeapAlloc))
    fmt.Printf("  HeapSys = %v MiB\n", bToMb(m.HeapSys))
    fmt.Printf("  HeapInuse = %v MiB\n", bToMb(m.HeapInuse))
    fmt.Println()
}

func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

// unique-examples/effect_without_unique.go
... 

func main() {
    // 创建一些不同的字符串
    distinctStrings := make([]string, numDistinct)
    for i := 0; i &lt; numDistinct; i++ {
        distinctStrings[i] = strings.Repeat(string(rune('A'+i%26)), stringLen)
    }

    // 不使用unique包
    withoutUnique := make([]string, numItems)
    for i := 0; i &lt; numItems; i++ {
        withoutUnique[i] = distinctStrings[i%numDistinct]
    }

    runtime.GC() // 强制GC以确保准确的内存使用统计
    printMemUsage("Without unique")

    runtime.KeepAlive(withoutUnique)
}

...
</code></pre>
<p>下面分别运行这两个源码：</p>
<pre><code>$go run effect_with_unique.go
With unique:
  Alloc = 7 MiB
  TotalAlloc = 7 MiB
  Sys = 15 MiB
  HeapAlloc = 7 MiB
  HeapSys = 11 MiB
  HeapInuse = 8 MiB

$go run effect_without_unique.go
Without unique:
  Alloc = 15 MiB
  TotalAlloc = 15 MiB
  Sys = 22 MiB
  HeapAlloc = 15 MiB
  HeapSys = 19 MiB
  HeapInuse = 15 MiB
</code></pre>
<p>这个结果清楚地显示了使用unique包后的内存节省。不使用unique包时，每个重复的字符串都会单独分配内存。而使用unique包后，相同的字符串只会分配一次，大大减少了内存使用。在实际应用中，内存节省的效果可能更加显著，特别是在处理大量重复数据（如日志处理、文本分析等）的场景中。</p>
<h2>4. 小结</h2>
<p>本文粗略探讨了Go 1.23版本引入的unique包：我们从字符串interning技术说起，介绍了Go社区在interning技术实现方面的努力历程，重点阐述了unique包的API设计、实现原理及其优势。</p>
<p>我们看到：unique包不仅支持传统的字符串interning，还扩展到任何可比较类型的值。其核心API设计简洁，通过Handle&#91;T&#93;类型和Make、Value方法实现了高效的值interning。</p>
<p>在实现原理上，unique包巧妙地结合了hashtrieMap、细粒度锁以及与runtime内gc相关函数，实现了支持可比较类型、interned值可被GC且支持快速比较的方案。</p>
<p>总的来说，unique包为Go开发者提供了一个强大而灵活的interning工具，有望在未来的Go社区项目中得到广泛应用。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/unique-examples">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://commaok.xyz/post/intern-strings/">Interning strings in Go</a> &#8211; https://commaok.xyz/post/intern-strings/</li>
<li><a href="https://en.wikipedia.org/wiki/String_interning">Interning</a> &#8211; https://en.wikipedia.org/wiki/String_interning</li>
<li><a href="https://github.com/golang/go/issues/62483">unique: new package with unique.Handle</a> &#8211; https://github.com/golang/go/issues/62483</li>
<li><a href="https://go.dev/blog/unique">New unique package</a> &#8211; https://go.dev/blog/unique</li>
<li><a href="https://github.com/golang/go/issues/69370">unique: large string still referenced, after interning only a small substring</a> &#8211; https://github.com/golang/go/issues/69370</li>
<li><a href="https://tailscale.com/blog/netaddr-new-ip-type-for-go?ref=tonybai.com">netaddr.IP: a new IP address type for Go</a> &#8211; https://tailscale.com/blog/netaddr-new-ip-type-for-go</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Go：值与指针</title>
		<link>https://tonybai.com/2023/05/05/go-value-and-pointer/</link>
		<comments>https://tonybai.com/2023/05/05/go-value-and-pointer/#comments</comments>
		<pubDate>Fri, 05 May 2023 14:10:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[bit]]></category>
		<category><![CDATA[BSS]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[decode]]></category>
		<category><![CDATA[encode]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[receiver]]></category>
		<category><![CDATA[segment]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[value]]></category>
		<category><![CDATA[位]]></category>
		<category><![CDATA[值]]></category>
		<category><![CDATA[值拷贝]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[变量]]></category>
		<category><![CDATA[可变性]]></category>
		<category><![CDATA[堆栈]]></category>
		<category><![CDATA[复合类型值]]></category>
		<category><![CDATA[字符]]></category>
		<category><![CDATA[字符串]]></category>
		<category><![CDATA[字面值]]></category>
		<category><![CDATA[小数]]></category>
		<category><![CDATA[布尔值]]></category>
		<category><![CDATA[常量]]></category>
		<category><![CDATA[引用]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[指针类型值]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[数组]]></category>
		<category><![CDATA[数量]]></category>
		<category><![CDATA[整数]]></category>
		<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=3870</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/05/05/go-value-and-pointer 1. 计算机中的值 在百万年的演化历史中，人类对事物的属性进行了抽象，有了数量、精度、信息等概念的表示，对应的我们称之为整数、小数、文本文字等。计算机出现后，我们使用计算机对真实世界的问题进行建模，通过计算机的高效计算解决这些问题并输出答案。为了建模，计算机需要建立对上述基本概念的抽象和表示，于是有了类型与值的概念。 计算机中所有数据都存储在内存中并参与问题解决的计算，真实世界的概念表示与内存中的数据的转换关系如下图： 图中的有界比特序列(bounded bit sequence)就是真实世界概念表示在计算机内存中的存储形式，我们可以统称它为一个值(value)。这个值的比特序列形式由类型决定。举个例子：一个公司的员工数量为1000人，这个真实世界的概念在计算机中的表示过程如下： 我们用uint16类型来表示员工数量，这样它在内存存储形式为0000 0011 1110 1000。如果你用不同的类型来表示员工数量，那么在内存中表示员工数量的值的比特序列将是不同的。 反之，对于内存中的一段有界比特序列，在不同类型guided的decode下，得到的结果也是不同的，如下图。 我们看到：在uint64的guided下，0000 0011 1110 1000这个比特序列被解释为1000；而在[2]byte的guided下，0000 0011 1110 1000这个同样的比特序列则被解释成了2个数字。 计算机中的值不仅仅可以表示一个数字，也可以表示一个字符串，甚至是像结构体这样的复合类型，它本质上就是一块儿连续的内存，内存单元是有地址的，通过该地址访问和更新内存单元中的值。 但在编程过程中直接使用内存地址是十分不便的，因此在高级编程语言中，编程语言通过具名的标识符与内存单元建立“绑定”关系，就得到了我们通常说的常量和变量，而内存单元中存储的数据(即值)也可说成是常量持有的数据和变量持有的数据。 当然也有一些不和任何标识符“绑定”的值，我们称之为字面值(literal value)。我们通常用字面值为变量和常量赋[初]值： var a int = 17 s := "hello" const f float64 = 3.1415926 原生类型的字面值，可以简单理解为汇编中的立即数；而复杂类型(比如结构体)的字面值，则一般是临时存储在栈上的有界比特序列。 2. 一切皆是值 根据上一节关于值的定义，我们可以认为：在Go语言中，所有东西都是以值的形式存在的。在Go语言中，不仅仅是基本类型如整数、浮点数、布尔值等，就连复杂的数据结构，如结构体、数组、切片、map、channel等都以值的形式存在。 到这里有小伙伴可能会问：“不对啊，map、channel等应该是指针吧”。别急，要解答这个问题，我们就要来看看值的分类。 2.1 值的分类 在Go中，值可分为以下几种类型： 基本类型值 基本类型是Go语言中最基础的数据类型，它们是直接由语言定义的。基本类型的值通常是简单的值，比如整数、浮点数、布尔值等。在Go语言中，基本类型的值可以进行各种运算和比较操作。 复合类型值 复合类型则是由基本类型组成的更复杂的数据类型。它们的值由多个基本类型值组合而成，并且可以使用结构化的方式进行访问和操作。在Go语言中，复合类型包括分为数组、切片、map、结构体、接口、channel等多种类型。这些复合类型在不同的场景下都有不同的用途，可以用于表示不同的数据结构或者实现不同的算法。 字符串在Go中是一个特殊的存在，从Go类型角度来看，它应该属于原生内置的基本类型，但从值的角度考虑，由于在运行时字符串类型表示为一个两字段的结构(如下) type StringHeader struct [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/advanced-go/go-value-and-pointer-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/05/05/go-value-and-pointer">本文永久链接</a> &#8211; https://tonybai.com/2023/05/05/go-value-and-pointer</p>
<h2>1. 计算机中的值</h2>
<p>在百万年的演化历史中，人类对事物的属性进行了抽象，有了数量、精度、信息等概念的表示，对应的我们称之为整数、小数、文本文字等。计算机出现后，我们使用计算机对真实世界的问题进行建模，通过计算机的高效计算解决这些问题并输出答案。为了建模，计算机需要建立对上述基本概念的抽象和表示，于是有了类型与值的概念。</p>
<p>计算机中所有数据都存储在内存中并参与问题解决的计算，真实世界的概念表示与内存中的数据的转换关系如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/advanced-go/go-value-and-pointer-2.png" alt="" /></p>
<p>图中的<strong>有界比特序列(bounded bit sequence)</strong>就是真实世界概念表示在计算机内存中的存储形式，我们可以统称它为<strong>一个值(value)</strong>。这个值的比特序列形式由类型决定。举个例子：一个公司的员工数量为1000人，这个真实世界的概念在计算机中的表示过程如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/advanced-go/go-value-and-pointer-3.png" alt="" /></p>
<p>我们用uint16类型来表示员工数量，这样它在内存存储形式为0000 0011 1110 1000。如果你用不同的类型来表示员工数量，那么在内存中表示员工数量的值的比特序列将是不同的。</p>
<p>反之，对于内存中的一段有界比特序列，在不同类型guided的decode下，得到的结果也是不同的，如下图。</p>
<p><img src="https://tonybai.com/wp-content/uploads/advanced-go/go-value-and-pointer-4.png" alt="" /></p>
<p>我们看到：在uint64的guided下，0000 0011 1110 1000这个比特序列被解释为1000；而在[2]byte的guided下，0000 0011 1110 1000这个同样的比特序列则被解释成了2个数字。</p>
<p>计算机中的值不仅仅可以表示一个数字，也可以表示一个字符串，甚至是像结构体这样的复合类型，它本质上就是<strong>一块儿连续的内存</strong>，内存单元是有地址的，通过该地址访问和更新内存单元中的值。</p>
<p>但在编程过程中直接使用内存地址是十分不便的，因此在<a href="https://en.wikipedia.org/wiki/High-level_programming_language">高级编程语言</a>中，编程语言通过具名的标识符与内存单元建立“绑定”关系，就得到了我们通常说的常量和变量，而内存单元中存储的数据(即值)也可说成是常量持有的数据和变量持有的数据。</p>
<p>当然也有一些不和任何标识符“绑定”的值，我们称之为<strong>字面值(literal value)</strong>。我们通常用字面值为变量和常量赋[初]值：</p>
<pre><code>var a int = 17
s := "hello"
const f float64 = 3.1415926
</code></pre>
<p>原生类型的字面值，可以简单理解为汇编中的立即数；而复杂类型(比如结构体)的字面值，则一般是临时存储在栈上的有界比特序列。</p>
<h2>2. 一切皆是值</h2>
<p>根据上一节关于值的定义，我们可以认为：<strong>在Go语言中，所有东西都是以值的形式存在的</strong>。在Go语言中，不仅仅是基本类型如整数、浮点数、布尔值等，就连复杂的数据结构，如结构体、数组、切片、map、channel等都以值的形式存在。</p>
<p>到这里有小伙伴可能会问：“不对啊，map、channel等应该是指针吧”。别急，要解答这个问题，我们就要来看看值的分类。</p>
<h3>2.1 值的分类</h3>
<p>在Go中，值可分为以下几种类型：</p>
<ul>
<li>基本类型值</li>
</ul>
<p>基本类型是Go语言中最基础的数据类型，它们是直接由语言定义的。基本类型的值通常是简单的值，比如整数、浮点数、布尔值等。在Go语言中，基本类型的值可以进行各种运算和比较操作。</p>
<ul>
<li>复合类型值</li>
</ul>
<p>复合类型则是由基本类型组成的更复杂的数据类型。它们的值由多个基本类型值组合而成，并且可以使用结构化的方式进行访问和操作。在Go语言中，复合类型包括分为数组、切片、map、结构体、接口、channel等多种类型。这些复合类型在不同的场景下都有不同的用途，可以用于表示不同的数据结构或者实现不同的算法。</p>
<p>字符串在Go中是一个特殊的存在，从Go类型角度来看，它应该属于原生内置的基本类型，但从值的角度考虑，由于在运行时字符串类型表示为一个两字段的结构(如下)</p>
<pre><code>type StringHeader struct {
    Data uintptr
    Len  int
}
</code></pre>
<p>因此，我们将其归为<strong>复合类型值</strong>范畴。</p>
<ul>
<li>指针类型值</li>
</ul>
<p>有一类值十分特殊，它自身是一个基本类型值，更准确的说是一个整型值，但<strong>这个整型值的含义却是另外一个值所在内存单元的地址</strong>。如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/advanced-go/go-value-and-pointer-5.png" alt="" /></p>
<p>我们看到：指针类型值为0&#215;12345678，这个值是另外一个内存块(值为0&#215;17)的地址。指针类型值在Go语言以及C、C++这一的静态语言中扮演着极其重要的角色。</p>
<p>回答前面小伙伴的问题：map、channel是不是值? 是值，只不过是指针类型值。从Go语法上来说，map、channel是某个runtime指针类型的实例。</p>
<h3>2.2 值的可变性</h3>
<p>在继续深入指针之前，我们先来插播一个内容：<strong>值的可变性</strong>。</p>
<p>前面说过值是一段连续内存，是一个有界比特序列。原理上来说，内存中的值都是可变的。但现实中，考虑到操作系统管理以及应用安全的需要，暴露给开发人员的值被做了限定，即有些值(内存单元中的数据)是可变的，而有一些值是不可变的。</p>
<p>首先，操作系统负责物理内存与虚拟内存的映射，应用开发人员面对的是平坦的虚拟内存。这部分平坦的虚拟内存也被分为了几个段(segment)，比如：BSS段、数据段、代码段、堆栈等，有些segment上的值是只读的，不可变的，比如代码段，有些则是可读写的可变的，比如堆栈。</p>
<p>此外，Go在编程语言层面也对值做了限制，常量值是不可变的，字符串类型值是不可变的，其他则为可变值。</p>
<h3>2.3 指针类型</h3>
<p>针对指针这类值，编程语言抽象出了一种类型：<strong>指针类型</strong>，指针类型的变量与指针类型值绑定，它内部存储的是另外一个内存单元的地址。这样就衍生出通过指针读取和更新指针指向的值的操作方法：</p>
<pre><code>var a int = 5 // 基础类型值
var p = &amp;a    // p为指针类型变量(*int)，其值为变量a的地址。

println(*p)   // 通过指针读取其指向的变量a的值
*p = 15       // 通过指针更新其指向的变量a的值
</code></pre>
<p>不过，指针更大的好处在于传递开销低，且传递后，接收指针的函数/方法体中依然可以修改指针指向的内存单元的值。</p>
<p>接下来，我们来详细说一下值的传递。</p>
<h3>2.4. 值的传递</h3>
<p>无论是赋值还是传参，Go语言中的所有值的传递的方法都是<strong>值拷贝</strong>，也称为<strong>逐位拷贝(bitwise copy)</strong>。</p>
<p>不过即便是值拷贝，也会带来三种不同效果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/advanced-go/go-value-and-pointer-6.png" alt="" /></p>
<ul>
<li>传值：你是你，我是我</li>
</ul>
<p>效果：传递前后的变量各自独立更新，互不影响。</p>
<p>示例：传整型、浮点型、布尔值等。</p>
<ul>
<li>传指针：你是你，我是我，但我们共同指向他</li>
</ul>
<p>效果：传递前后的指针变量拥有相同的指针值，因此共同指向同一个内存对象(d)。通过其中一个指针变量对指向的内存对象进行更新后(e)，另一个指针变量可以感知到相同的变化。</p>
<p>示例：传&#42;T指针类型变量。包括在Go runtime层面本质是一个指针的类型，比如map、channel等。</p>
<ul>
<li>传“引用”：你是你，我是我，但我们有一部分共同指向他</li>
</ul>
<p>首先要注意，Go语言规范中没有“引用类型”这一表述。其次，也不要将这里的“引用”与其他语言的“引用类型”相提并论。</p>
<p>这里传“引用”的效果是：传递前后的变量一部分是独立更新互不影响的，一部分则是有共同指向，相互影响的。最典型的例子就是切片。当我们将切片传入函数后，函数内对切片的更新操作会影响到原切片，包括更新切片元素的值、向切片追加元素等。尤其是向切片追加(append)元素后，会导致传递前后的两个切片出现“不一致”，详情可以参考我之前写的一篇文章<a href="https://tonybai.com/2022/10/27/when-encountering-slice-during-function-design">《当函数设计遇到切片》</a>。</p>
<p>这里之所以使用的“引用”来形容这种效果，主要是像slice这样的类型与我们熟知的其他语言中的引用(reference)很像，都是它们以“值”的形态传递，但却能干着“指针”的活儿。</p>
<h2>3. 关于值的一些tips</h2>
<h3>3.1 零值</h3>
<p>在Go语言中，每个变量都有一个默认的零值，即在变量未被初始化时的默认值。这个默认值取决于变量的类型，可以是一个数字、布尔值、字符串、指针、数组、结构体等等。</p>
<p>在Go语言中，零值可以用来初始化变量的默认值，也可以用来清空变量的值。</p>
<pre><code>var i int // i的零值为0
var s string // s的零值为""
var p *int // p的零值为nil
var a [3]int // a的零值为[0 0 0]
var b struct { x int; y float64 } // b的零值为{0 0.0}
</code></pre>
<p>在这个例子中，我们使用var关键字声明了5个变量，并使用它们的零值来初始化这些变量的值。</p>
<p>另外，我们可以使用零值来清空变量的值，例如：</p>
<pre><code>var i int = 10 // 初始化i的值为10
i = 0 // 使用i的零值来清空它的值
</code></pre>
<p>在使用零值时，需要注意以下两个问题：</p>
<ul>
<li>指针类型的零值为nil，不能直接使用nil指针来访问变量的值，否则会导致panic。</li>
<li>可声明零长度数组类型，这样的类型的实例不占用内存空间，这在一些特殊场合下会很有用。</li>
</ul>
<h3>3.2 值的比较</h3>
<p>Go语言的值比较是通过比较两个值的二进制表示来实现的。在Go语言中，值比较主要用于判断两个值是否相等。下面是Go语言值比较的场景、规则和注意事项：</p>
<h4>场景</h4>
<ul>
<li>判断两个值是否相等；</li>
<li>判断两个值是否不相等；</li>
<li>判断一个值是否为nil；</li>
<li>判断两个指针是否指向同一个对象。</li>
</ul>
<h4>规则</h4>
<ul>
<li>对于基本类型（如int、float、bool等），只需要比较它们的值就可以了；</li>
<li>对于复合类型（如数组、切片、map等），需要递归比较它们的元素或键值对；</li>
<li>对于结构体类型，需要递归比较它们的字段；</li>
<li>对于接口类型，需要判断它们是否指向同一个动态类型以及动态值是否相等。</li>
</ul>
<h4>注意事项</h4>
<ul>
<li>对于浮点数类型，不能使用“==”运算符进行比较，因为浮点数的精度问题可能导致比较结果不正确，应该使用math包中的函数进行比较；</li>
<li>对于切片类型，Go不支持直接使用“==”运算符进行比较，因为它们的底层数据结构可能不同，应该使用reflect包中的函数DeepEqual进行比较；</li>
<li>对于结构体类型，如果其中包含不可比较的字段（如切片、映射、函数等），则整个结构体类型也是不可比较的；</li>
<li>对于指针类型，需要注意空指针的情况，应该先判断指针是否为nil，再进行比较。</li>
</ul>
<h3>3.3 method receiver的值与指针类型的选择</h3>
<p>在Go语言中，method receiver可以是值类型或指针类型。这个选择可能会影响代码的性能、正确性和可读性等方面。</p>
<p>当一个方法的receiver是一个值类型时，receiver的传递会出现“传值”效果，方法体中对这个值的修改不会影响原来的值。但是，如果这个值类型的对象非常大，每次调用方法都需要进行复制，这会导致一定的性能损失。</p>
<p>当一个方法的receiver是一个指针类型时，这个方法操作的就是原来的对象，并且可以修改原来的对象。这种方式可以避免复制对象的开销，并且可以访问和修改对象的内部状态。但是，如果多个goroutine同时访问同一个对象时，就会发生竞争条件，导致程序出现不可预料的行为。</p>
<p>在选择method receiver的类型时，可考虑以下几个因素：</p>
<ul>
<li>对象的大小：如果对象很小，可以选择值类型的method receiver，避免复制对象的开销；如果对象很大，可以选择指针类型的method receiver，避免复制整个对象的开销。</li>
<li>对象的可变性：如果对象需要被修改，应该选择指针类型的method receiver；如果对象不需要被修改，可以选择值类型的method receiver，保证代码的可预测性和可读性。</li>
<li>对象类型或对象的指针类型是否需要实现特定的接口。</li>
</ul>
<blockquote>
<p>注：关于method receiver的类型选择问题，在<a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>的第25讲有系统的讲解。</p>
</blockquote>
<h3>3.4 使用unsafe.Pointer进行不同type guided的值decode</h3>
<p>前面说过，值是一个“有界比特序列”，在不同类型guided的decode下，得到的结果也是不同的。我们可以通过unsafe.Pointer来进行不同的decode，比如下面例子将一个uint32的值重新分别decode为一个[2]uint16和[4]uint8数组：</p>
<pre><code>package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a uint32 = 0x12345678

    b := (*[2]uint16)(unsafe.Pointer(&amp;a))
    c := (*[4]uint8)(unsafe.Pointer(&amp;a))

    fmt.Println(*b) // [22136 4660]
    fmt.Println(*c) // [120 86 52 18]
}
</code></pre>
<h2>4. 小结</h2>
<p>本文对Go语言中值做了重新解读，我们认为Go中的值就是一个<strong>有界比特序列(bounded bit sequence)</strong>，是真实世界概念表示在计算机内存中的存储形式。</p>
<p>围绕着值这个概念，我们指出Go中一切皆是值。在这一观点的基础上，重新了解了值的分类、值的可变性、指针类型以及重要的值的传递，学习了值的传递的本质：bitwise-copy，以及这个传递过程针对不同类型值所取得的不同效果。</p>
<p>最后，我们了解了一些与值有关的tips，包括零值、值比较、method receiver类型选择以及值decode。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/05/05/go-value-and-pointer/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
