<?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; Pointer</title>
	<atom:link href="http://tonybai.com/tag/pointer/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/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>从 Rob Pike 的提案到社区共识：Go 或将通过 new(v) 彻底解决指针初始化难题</title>
		<link>https://tonybai.com/2025/08/17/create-pointer-to-simple-types/</link>
		<comments>https://tonybai.com/2025/08/17/create-pointer-to-simple-types/#comments</comments>
		<pubDate>Sun, 17 Aug 2025 01:14:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[builtin]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[int]]></category>
		<category><![CDATA[new]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[proposal]]></category>
		<category><![CDATA[ptr]]></category>
		<category><![CDATA[ref]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[内置函数]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[提案]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[类型推断]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5043</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/17/create-pointer-to-simple-types 大家好，我是Tony Bai。 在 Go 中创建一个指向基本类型（如 int 或 string）的指针，为何比创建一个指向结构体的指针更繁琐？这个长期存在的“人体工程学”问题，由 Go 语言的共同创造者之一 Rob Pike 在提案 #45624 中再次带入公众视野，并由此引发了一场长达数年、充满深度思辨的社区大讨论。最终，在权衡了多种方案的利弊后，社区逐渐形成共识，Go 提案委员会倾向于接受 new(v) 语法。本文将和大家一起回顾这场关于指针初始化的“十年之辩”，深入探讨各种方案的优劣，并解读为何 new(v) 可能成为最终赢家。 背景：一个困扰开发者多年的“小”问题 在 Go 中，我们可以用 p := &#38;S{a: 3} 这样简洁的语法，一步到位地创建一个指向已初始化结构体的指针。但如果我们想创建一个指向 int 值 3 的指针，就必须写成： a := 3 p := &#38;a 这种不对称性在处理大量使用指针来表示“可选”字段的场景时（例如，与 JSON、Protobuf 或 AWS SDK 交互），会变得异常繁琐。开发者往往不得不在项目中定义或引入大量的辅助函数，如： func StringPtr(s string) *string { return &#38;s [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/create-pointer-to-simple-types-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/17/create-pointer-to-simple-types">本文永久链接</a> &#8211; https://tonybai.com/2025/08/17/create-pointer-to-simple-types</p>
<p>大家好，我是Tony Bai。</p>
<p>在 Go 中创建一个指向基本类型（如 int 或 string）的指针，为何比创建一个指向结构体的指针更繁琐？这个长期存在的“人体工程学”问题，由 Go 语言的共同创造者之一 <strong>Rob Pike</strong> 在提案 <a href="https://github.com/golang/go/issues/45624">#45624</a> 中再次带入公众视野，并由此引发了一场长达数年、充满深度思辨的社区大讨论。最终，在权衡了多种方案的利弊后，社区逐渐形成共识，Go 提案委员会倾向于接受 new(v) 语法。本文将和大家一起回顾这场关于指针初始化的“十年之辩”，深入探讨各种方案的优劣，并解读为何 new(v) 可能成为最终赢家。</p>
<h2>背景：一个困扰开发者多年的“小”问题</h2>
<p>在 Go 中，我们可以用 p := &amp;S{a: 3} 这样简洁的语法，一步到位地创建一个指向已初始化结构体的指针。但如果我们想创建一个指向 int 值 3 的指针，就必须写成：</p>
<pre><code class="go">a := 3
p := &amp;a
</code></pre>
<p>这种不对称性在处理大量使用指针来表示“可选”字段的场景时（例如，与 JSON、Protobuf 或 AWS SDK 交互），会变得异常繁琐。开发者往往不得不在项目中定义或引入大量的辅助函数，如：</p>
<pre><code class="go">func StringPtr(s string) *string {
    return &amp;s
}
// 还有 Int64Ptr, BoolPtr, Float64Ptr...
</code></pre>
<p>正如 @adonovan 在提案讨论中通过代码分析所展示的，这种模式在 Go 开源生态中极为普遍，存在<strong>数千个</strong>这样的辅助函数和<strong>数十万次</strong>的调用。这清晰地表明，语言层面提供一个更简洁的解决方案是众望所归。</p>
<h2>方案之争：一场关于语法、语义与哲学的辩论</h2>
<p>Rob Pike 的提案及其漫长的讨论过程，涌现了多种解决方案，每种方案都代表了一种不同的语言设计哲学。</p>
<h3>方案一：扩展 &amp; 操作符</h3>
<p>这是最直观的想法，主要有两种变体：</p>
<ol>
<li><strong>&amp;T(v)</strong> (让类型转换变得可寻址): p := &amp;int(3)。这是 Rob Pike 最初提出的方案之一。它利用了“类型转换必然会创建新值”这一语义，逻辑自洽。</li>
<li><strong>&amp;v</strong> (让非地址表达式变得可寻址): p := &amp;3 或 p := &amp;time.Now()。这个方案更通用，但也最危险。正如 rsc 和其他核心成员指出的，这会产生严重的歧义。例如，&amp;m[k] 在 m 是 slice 时是取地址，但在 m 是 map 时却变成了“拷贝值并取地址”，这会引入大量难以察觉的 bug。</li>
</ol>
<p>由于存在严重的“最小惊动原则”问题，扩展 &amp; 的方案最终未被采纳。</p>
<h3>方案二：引入新的泛型内建函数</h3>
<p>随着 <a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18 泛型的引入</a>，一个显而易见的解决方案是提供一个泛型辅助函数。</p>
<pre><code class="go">// 可以是内置的，也可以是开发者自己写的
func ptr[T any](v T) *T {
    return &amp;v
}

// 使用方式:
p := ptr(3)
p2 := ptr(time.Now())
</code></pre>
<p>这个方案得到了许多开发者的支持，因为它无需对语言规范做任何大的改动。然而，它的缺点也很明显：</p>
<ul>
<li><strong>命名之争</strong>：应该叫 ptr, ref, addr, newOf 还是 varOf？每种名称都有其支持者和反对者。例如，ptr 和 ref 可能会让人误以为是取现有变量的引用，而不是创建一个新的拷贝。</li>
<li><strong>标准库位置</strong>：这样一个基础的函数应该放在哪里？builtin？还是一个新的标准库包？这本身就是一个难题。</li>
</ul>
<h3>方案三：扩展 new 内建函数 (最可能的胜出者)</h3>
<p>这是提案的核心，也是最终获得Go提案委员会青睐的方向。它同样有几种变体：</p>
<ol>
<li><strong>new(T, v)</strong>：new 接受一个可选的第二个参数用于初始化。例如 p := new(int, 3)。这非常明确，但缺点是类型 T 往往是冗余的，显得很“啰嗦”，例如 new(time.Duration, time.Second)。</li>
<li><strong>new(v)</strong>：new 可以直接接受一个值，并根据值的类型推断出要分配的指针类型。例如 p := new(3) 会创建一个 *int。这是最简洁的方案。</li>
</ol>
<p><strong>new(v) 的核心争议与共识</strong></p>
<p>new(v) 的主要争议在于<strong>语法歧义</strong>。当看到 new(pkg.X) 时，读者无法仅从语法上判断 pkg.X 是一个类型（new(T)）还是一个常量值（new(v)）。</p>
<p>然而，经过深入讨论，提案委员会认为：<br />
*   这种歧义在实践中<strong>问题不大</strong>，因为绝大多数情况下，上下文足以让开发者区分类型和值。<br />
*   相比于 &amp;v 带来的严重语义混乱，new(v) 的语法歧义是次要的、可接受的。<br />
*   new 这个词本身就清晰地传达了<strong>“创建新事物”</strong>的意图，避免了 &amp; 操作符的“拷贝还是引用”的混淆。<br />
*   考虑到 new(T) 的使用频率远低于 &amp;T{}，将其“回收”并赋予更强大的功能，是对语言的一次有益的“清理”。</p>
<p>最终，提案委员会倾向于接受 new(expr) 的形式。</p>
<h2>new(expr) 将如何工作</h2>
<p>根据讨论的共识，未来的 new(expr) 将遵循以下规则：</p>
<ul>
<li><strong>基本用法</strong>: p := new(3) 将创建一个 *int，其值为 3。s := new(“hello”) 将创建一个 *string，其值为 “hello”。</li>
<li><strong>类型推断</strong>: 对于无类型常量，将使用 Go 的默认类型规则（例如，整数默认为 int，浮点数默认为 float64）。</li>
<li><strong>显式类型</strong>: 如果需要指定不同于默认的类型，需要使用类型转换：p64 := new(int64(3))来创建一个<em>int64类型变量p64，而不是默认的</em>int指针类型变量。</li>
<li><strong>无上下文类型推断</strong>: new(v) <strong>不会</strong>根据赋值的上下文来推断类型。例如，var p *int64 = new(3) 将会编译失败，因为 new(3) 的类型是 *int，不能赋值给 *int64。</li>
</ul>
<h2>结论：小改动，大便利</h2>
<p>从 Rob Pike 最初的提案，到社区长达数年的激烈辩论，new(v) 的最终可能胜出是 Go 语言演进过程的一个缩影。它通过一个微小但精心设计的语法扩展，解决了困扰社区多年的一个普遍痛点。</p>
<p>这个决策过程本身，也充分体现了 Go 团队的设计哲学：</p>
<ol>
<li><strong>优先考虑语言的一致性和无歧义性</strong>，因此拒绝了看似更简洁但充满陷阱的 &amp;expr 方案。</li>
<li><strong>在不破坏兼容性的前提下，勇于重塑旧有特性</strong>，将使用率不高的 new 重新利用，赋予其更强大的生命力。</li>
<li><strong>充分倾听并分析社区的真实数据</strong>，@adonovan 的大规模代码分析为该功能的需求提供了强有力的数据支撑。</li>
</ol>
<p>虽然我们仍需等待该提案在未来某个 Go 版本中正式落地，但可以预见，当它到来时，我们代码库中那些重复的 Ptr 辅助函数将成为历史。这正是 Go 语言持续进化、不断提升开发者幸福感的魅力所在。</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/17/create-pointer-to-simple-types/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>GCP大面积故障，Go语言是“元凶”还是“背锅侠”？</title>
		<link>https://tonybai.com/2025/06/16/go-avoid-critical-incident/</link>
		<comments>https://tonybai.com/2025/06/16/go-avoid-critical-incident/#comments</comments>
		<pubDate>Mon, 16 Jun 2025 00:09:47 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Assist]]></category>
		<category><![CDATA[CodeAssist]]></category>
		<category><![CDATA[Copilot]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[FeatureFlags]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[gemini]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[linter]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[代码审查]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[幻觉]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[模糊测试]]></category>
		<category><![CDATA[测试覆盖率]]></category>
		<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=4825</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/16/go-avoid-critical-incident 大家好，我是Tony Bai。 科技圈的每一次“风吹草动”，尤其是大型云服务的故障，总能引发我们技术人无数的讨论与反思。最近，一则关于“Google Cloud Platform (GCP) Service Control 在 2025 年 6 月发生重大故障”的消息，及其事后分析报告中直指的“null pointer crash loop”，在技术社区掀起了不小的波澜。 故障报告中还提到了几个雪上加霜的因素：没有特性标志 (Feature Flags) 进行高风险部署、缺乏优雅的错误处理（二进制文件直接崩溃而非优雅降级）、以及没有回退机制导致系统过载。 考虑到 Go 语言在 Google 内部（如 Kubernetes, Cloud Run 等）以及整个云原生领域的广泛应用，一个自然而然的疑问浮出水面：Go语言是否是这次 GCP 故障的“元凶”？或者说，Go 的某些特性，是否在某种程度上“助长”了这类问题的发生？反过来，Go 的设计又是否本可以帮助避免这样的灾难？ 这这篇文章中，我们就结合社区的智慧，从Go语言特性和更广泛的软件工程实践角度，来剖析一下这类故障背后的深层原因。这不仅是对一个故障的假想复盘，更是对我们日常开发实践的一次警醒。 Go 语言特性：是“防火墙”还是“导火索”？ 社区论坛上的讨论，首先就聚焦在了 Go 语言本身的一些特性上。 显式错误返回 (if err != nil)：万无一失还是“防君子不防小人”？ 有开发者认为，Go 标志性的显式错误返回设计（即函数返回 (value, error)，调用者必须检查 err），本应是避免错误的有力武器。但也有观点指出，这种模式的“简洁性”（或者说，可以通过 _ 忽略错误的便利性）有时反而可能在项目压力大、追求快速上线时，被开发者有意或无意地跳过，导致潜在的错误处理缺失。比如常见的 value, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-avoid-critical-incident-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/16/go-avoid-critical-incident">本文永久链接</a> &#8211; https://tonybai.com/2025/06/16/go-avoid-critical-incident</p>
<p>大家好，我是Tony Bai。</p>
<p>科技圈的每一次“风吹草动”，尤其是大型云服务的故障，总能引发我们技术人无数的讨论与反思。最近，一则关于“Google Cloud Platform (GCP) Service Control 在 2025 年 6 月发生重大故障”的消息，及其事后分析报告中直指的“null pointer crash loop”，在技术社区掀起了不小的波澜。</p>
<p>故障报告中还提到了几个雪上加霜的因素：没有特性标志 (Feature Flags) 进行高风险部署、缺乏优雅的错误处理（二进制文件直接崩溃而非优雅降级）、以及没有回退机制导致系统过载。</p>
<p>考虑到 Go 语言在 Google 内部（如 Kubernetes, Cloud Run 等）以及整个云原生领域的广泛应用，一个自然而然的疑问浮出水面：<strong>Go语言是否是这次 GCP 故障的“元凶”？或者说，Go 的某些特性，是否在某种程度上“助长”了这类问题的发生？反过来，Go 的设计又是否本可以帮助避免这样的灾难？</strong></p>
<p>这这篇文章中，我们就结合社区的智慧，从Go语言特性和更广泛的软件工程实践角度，来剖析一下这类故障背后的深层原因。这不仅是对一个故障的假想复盘，更是对我们日常开发实践的一次警醒。</p>
<h2>Go 语言特性：是“防火墙”还是“导火索”？</h2>
<p>社区论坛上的讨论，首先就聚焦在了 Go 语言本身的一些特性上。</p>
<h3>显式错误返回 (if err != nil)：万无一失还是“防君子不防小人”？</h3>
<p>有开发者认为，Go 标志性的显式错误返回设计（即函数返回 (value, error)，调用者必须检查 err），本应是避免错误的有力武器。但也有观点指出，这种模式的“简洁性”（或者说，可以通过 _ 忽略错误的便利性）有时反而可能在项目压力大、追求快速上线时，被开发者有意或无意地跳过，导致潜在的错误处理缺失。比如常见的 value, _ := someFunction() 写法。</p>
<p>Go的显式错误返回，确实为构建健壮软件提供了坚实的基础。它将错误视为一等公民，迫使开发者直面错误处理。但语言提供的机制，终究不能替代开发者的责任心和良好的编码习惯。正如有些开发者提到的，golangci-lint 这样的静态检查工具可以有效地发现未检查的错误，但这需要团队将其融入开发流程并严格执行。**语言设计提供了“防火墙”，但工程师的素养和流程的完备性，才是决定防火墙是否真正起作用的关键。</p>
<h3>Nil Pointer Panic：Go 也难逃的“魔爪”？</h3>
<p>针对报告中提到的“null pointer crash loop”，许多评论者指出，nil 指针 panic 在 Go 中也并非罕见。Go 语言本身允许指针存在，也允许指针为 nil，并且不像 Rust 的 Option/Result 类型或 C# 的可空引用类型那样，在语言层面强制开发者处理潜在的 nil 情况。</p>
<p>的确，Go 语言的设计哲学是简洁，它相信开发者有能力正确处理指针。避免 nil panic 的核心在于良好的编码实践：<strong>防御性编程</strong>（在使用指针前进行检查）、<strong>最小化指针使用</strong>（Go 鼓励值传递，许多场景可以完全避免指针）、以及<strong>充分的测试</strong>（特别是边界条件和异常路径）。虽然 Go 没有语言层面的强制 nil 检查，但其简洁性也使得这类检查的成本相对较低。</p>
<h3>panic/recover 机制：救命稻草还是饮鸩止渴？</h3>
<p>有开发者分享经验，倾向于用 panic/recover 包裹所有核心逻辑，试图捕获所有潜在的运行时崩溃。但针对像故障中提到的 Service Control 这样的有状态、高关键性的系统，这种做法也引发了质疑：recover 后的程序状态是否真的可靠？强行“续命”一个可能已处于不一致状态的进程，是否比让它快速失败并由外部监控系统（如 Kubernetes）重启更安全？关于这个问题，我曾在《<a href="https://tonybai.com/2025/05/31/six-smells-in-go/">“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道</a>》一文中也讨论过。</p>
<p>panic/recover 在 Go 中有其特定的适用场景，例如在库的边界将内部的 panic 转换为 error 返回给调用者，或者处理真正意外且难以通过常规错误处理覆盖的严重问题。但对于关键业务服务，尤其是有状态的服务，<strong>“fail fast”</strong> 依然是目前社区认为的更可取的设计。让服务在遇到严重内部错误时快速、干净地退出，依赖外部的健康检查和自动重启机制来恢复服务，往往比试图在不确定的状态下继续运行更稳妥。</p>
<p>这样来看，Go 语言的设计，如显式错误处理，确实为构建可靠系统提供了工具。但它并不提供“银弹”，也不能完全消除诸如 nil 指针解引用这类逻辑错误的可能性。语言特性是基础，但绝非全部。</p>
<h2>超越语言：流程、测试与工程文化的“灵魂拷问”</h2>
<p>在针对该故障的讨论中，一个压倒性的共识是：<strong>这类大型系统故障，往往更多是软件工程流程、测试策略和工程文化上的问题，而非单一语言设计所能左右。</strong></p>
<h3>“100% 测试覆盖率”的迷思与测试策略的缺位</h3>
<p>有开发者提出“你可以覆盖 100% 的代码行，但你永远无法覆盖 100% 的输入和状态组合。” 这句话一针见血。过度迷信行覆盖率，而忽略了测试的深度和广度，是许多团队的通病。</p>
<p>那么真正有效的测试策略应该是什么呢？显然单一的测试策略是无法保证程序上线后的质量的。下面是几种常见的测试策略：</p>
<ul>
<li>单元测试 (Unit Testing): 验证开发者对代码单元在预期输入下的行为。</li>
<li>模糊测试 (Fuzz Testing): 通过自动生成大量随机或变异输入，探索代码的边缘情况和未知缺陷。Go 1.18 已将 Fuzz Testing 内置到标准工具链中，这是一个强大的武器。</li>
<li>集成测试 (Integration Testing): 验证模块间的交互。</li>
<li>端到端测试 (End-to-End Testing): 模拟真实用户场景。</li>
<li>生产测试/灰度发布 (Staged Rollouts / Canary Releases): 在真实生产环境中，小范围、逐步地验证变更的可靠性，这是大型系统发布的“金丝雀”。</li>
</ul>
<p>这些策略显而易见，但又有多少团队能真正全面的做到呢？</p>
<h3>特性标志 (Feature Flags)：高风险变更的“安全阀”</h3>
<p>故障报告中提到了“没有特性标志进行风险部署”，这几乎是大型系统发布的“大忌”。特性标志允许团队在不重新部署代码的情况下，动态地开启或关闭某项功能，从而：</p>
<ul>
<li>安全地进行 A/B 测试。</li>
<li>逐步向用户灰度上线新功能，控制风险。</li>
<li>在出现问题时，能够快速关闭故障功能，实现秒级“回滚”（功能层面）。</li>
</ul>
<p>缺乏特性标志，意味着任何高风险的变更都像是在“裸奔”。</p>
<h3>优雅降级与回滚预案：Plan B 的重要性</h3>
<p>系统出错在所难免，关键在于出错后如何表现。故障报告中“二进制崩溃而非优雅降级”以及“没有随机回退导致过载”，都指向了系统鲁棒性的缺失。</p>
<ul>
<li>优雅降级: 当核心服务出现问题时，非关键功能是否可以降级服务，保证核心可用性？例如，推荐系统不可用时，是否可以展示默认热门内容，而不是整个页面崩溃？</li>
<li>回滚计划: 任何部署都应该有明确、经过演练的回滚计划。出现问题时，能否快速、安全地回退到上一个稳定版本？</li>
</ul>
<h3>代码审查、自动化工具与工程文化</h3>
<ul>
<li>严格的代码审查: 是发现逻辑错误、不规范写法（如忽略错误、滥用指针）的重要手段。</li>
<li>静态分析与 Linter：golangci-lint 等工具可以自动化地检查出大量潜在问题，包括未处理的错误、不安全的并发操作等。但正如有些开发者在评论中所言，“linters can be disabled”，关键还是在于流程的执行。</li>
<li>警惕“Vibe Coding”：有开发者犀利地指出“Garbage in, garbage out”。如果团队强依赖AI的“氛围”编码，而缺乏对生成代码的审查，那么无论用什么语言，都可能埋下隐患。</li>
<li>重视流程而非迷信工具：许多评论都强调，即使有再好的语言特性或工具，如果缺乏健全的开发、测试、部署流程，以及对质量负责的工程文化，故障依然难以避免。</li>
</ul>
<h2>AI 辅助编程：是“帮手”还是新的“风险源”？</h2>
<p>一个有趣的衍生讨论是关于 AI 辅助编程（如 GitHub Copilot、Google Gemini Code Assist）在其中的角色。</p>
<p>有开发者提到，Google 内部已有大量代码由 Gemini 生成。也有人分享使用 AI 辅助编程的体验，认为其在作为“结对编程伙伴”或“辅助搜索”时有价值，但完全自动生成的代码质量参差不齐，有时甚至会引入“幻觉”和新的 bug。</p>
<p>AI 辅助编程无疑是未来的趋势，它有可能提高开发效率，辅助开发者处理重复性工作。但目前来看，AI 生成的代码<strong>更需要、而不是更不需要</strong>人类的严格审查和充分测试。将 AI 视为一个能提供建议、加速编码的助手是合适的，但如果过度依赖，甚至将其生成的代码不经审视直接合入生产，那无异于引入了新的、更不可控的风险源。特别是在错误处理、并发安全、边界条件这些需要深度思考的领域，AI至少目前还难以完全替代经验丰富的工程师，尤其是一些mission critical的系统中。不要被那些用AI生成一个简单工具站的“AI战果”所迷惑。</p>
<h2>小节：语言是利器，工程实践才是灵魂</h2>
<p>回到最初的问题：GCP Service Control 的这次故障，Go 语言是“元凶”还是“背锅侠”？</p>
<p>从 社区的讨论和我们的分析来看，将板子完全打在 Go 语言身上，显然是有失公允的。Go 语言的设计，如其显式错误处理、简洁性带来的高可读性、以及强大的并发能力，都为构建健壮、高效的系统提供了良好的基础。</p>
<p>然而，<strong>语言终究只是工具，它不能替代健全的软件工程流程和严谨的工程文化。</strong> 此次 GCP 故障所暴露出的问题——无论是可能的 nil 指针解引用，还是更宏观的缺乏特性标志、部署策略失当、错误处理不优雅——更多地指向了在测试、部署、风险控制、质量保障等一系列工程实践环节可能存在的缺失。</p>
<p>对于我们 Go 开发者而言，这次事件给我们带来的启示应该是：</p>
<ul>
<li><strong>充分利用 Go 的优势：</strong> 写出符合 Go 惯例的、清晰的错误处理逻辑；审慎使用指针，做好 nil 检查；发挥 Go 并发模型的威力。</li>
<li><strong>拥抱并严格执行工程最佳实践：</strong> 将单元测试、集成测试、模糊测试落到实处；在重要变更上线时，务必使用特性标志和灰度发布策略；建立严格的代码审查机制；利用好静态分析工具。</li>
<li><strong>对 AI 保持理性：</strong> 善用 AI 辅助工具提高效率，但绝不能放松对代码质量的把控和人工审查的力度。</li>
</ul>
<p>最终，构建一个真正高可用、高可靠的大型系统，依赖的绝不仅仅是选择一门“好”的语言，更在于整个团队对卓越工程实践的持续追求和严格执行。</p>
<p>你对这次讨论有什么看法？或者在你的 Go 项目中，是如何保障系统稳定性的？欢迎在评论区留下你的宝贵经验！</p>
<hr />
<p><strong>精进有道，更上层楼</strong></p>
<p><a href="https://mp.weixin.qq.com/s/GWGWTfCRCsOJ_4Pk-pxpHA">极客时间《Go语言进阶课》上架刚好一个月</a>，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。</p>
<p>现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质>量 Go 代码的关键要素。</p>
<p>这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、<br />
更高效、更可靠的生产级代码！</p>
<p>扫描下方二维码，立即开启你的 Go 语言进阶之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<p><strong>感谢阅读！</strong></p>
<p>如果这篇文章让你对Go语言有了新的认识，请帮忙转发，让更多朋友一起学习和进步！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/06/16/go-avoid-critical-incident/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！</title>
		<link>https://tonybai.com/2025/06/14/go-1-25-foresight/</link>
		<comments>https://tonybai.com/2025/06/14/go-1-25-foresight/#comments</comments>
		<pubDate>Sat, 14 Jun 2025 00:06:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Cgroup]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[CoreType]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[DWARF5]]></category>
		<category><![CDATA[encoding]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[godoc]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOMAXPROCS]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[ignore]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[jsonv2]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Sprintf]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[synctest]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TLS]]></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>

		<guid isPermaLink="false">https://tonybai.com/?p=4817</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/14/go-1-25-foresight 大家好，我是Tony Bai。 每年，Go 语言都会以其严谨而高效的节奏，带来两次版本更新。每一次迭代，Go 团队都在底层、工具链和标准库上持续深耕，为我们开发者提供更稳健、更高效、更安全的开发体验。虽然 Go 1.25 的正式版预计在 2025 年 8 月发布，但随着近期Go 1.25RC1版本的推出，我们基于其非最终版的 Release Notes，已经能一窥其核心亮点了。并且，和之前的版本一样，Go 1.25 带来的许多改进，都如同“无形之手”，你可能无需修改一行代码，甚至无需刻意感知，只需简单升级，便能享受到性能的飞跃、诊断能力的提升以及潜藏错误的暴露。这正是 Go 团队践行其核心原则的极致体现。 今天，就让我们一起“未雨绸缪”，聚焦 Go 1.25 中的核心特性，看看它将如何让 Go 语言变得更加强大。 语言层面：兼容至上，细微进化 Go语言对向后兼容性的承诺，是其最受开发者赞誉的特性之一。Go 1.25 再次延续了这一传统：它没有引入任何影响现有 Go 程序的语言语法变更！ 这意味着你可以放心地升级到 Go 1.25，而无需担忧已有的代码库会因此“崩溃”。 尽管如此，语言规范层面仍有细微的整理和优化，例如移除了“core type”的概念，代之以更详细的描述。这些更多是内部设计文档的完善，对日常 Go 程序的编写并无直接影响，但体现了 Go 语言设计本身的严谨性和持续迭代。兼容性，依然是 Go 坚不可摧的基石。 更详细地说明可以参考我之前的文章《Go 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路》。 运行时与编译器：性能与可靠性的“幕后推手” 这一部分是 Go 1.25 带来诸多“无形”强大之处的集中体现，它们直接影响着 Go 程序的运行效率和稳定性。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-1-25-foresight-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/14/go-1-25-foresight">本文永久链接</a> &#8211; https://tonybai.com/2025/06/14/go-1-25-foresight</p>
<p>大家好，我是Tony Bai。</p>
<p>每年，Go 语言都会以其严谨而高效的节奏，带来两次版本更新。每一次迭代，Go 团队都在底层、工具链和标准库上持续深耕，为我们开发者提供更稳健、更高效、更安全的开发体验。虽然 Go 1.25 的正式版预计在 2025 年 8 月发布，但随着近期Go 1.25RC1版本的推出，我们基于其非最终版的 Release Notes，已经能一窥其核心亮点了。并且，和之前的版本一样，Go 1.25 带来的许多改进，都如同“无形之手”，你可能无需修改一行代码，甚至无需刻意感知，只需简单升级，便能享受到性能的飞跃、诊断能力的提升以及潜藏错误的暴露。这正是 Go 团队践行其核心原则的极致体现。</p>
<p>今天，就让我们一起“未雨绸缪”，聚焦 Go 1.25 中的核心特性，看看它将如何让 Go 语言变得更加强大。</p>
<h2>语言层面：兼容至上，细微进化</h2>
<p>Go语言对<strong>向后兼容性</strong>的承诺，是其最受开发者赞誉的特性之一。Go 1.25 再次延续了这一传统：<strong>它没有引入任何影响现有 Go 程序的语言语法变更！</strong> 这意味着你可以放心地升级到 Go 1.25，而无需担忧已有的代码库会因此“崩溃”。</p>
<p>尽管如此，语言规范层面仍有细微的整理和优化，例如<a href="https://tonybai.com/2025/03/27/remove-coretypes-from-go-spec">移除了“core type”的概念</a>，代之以更详细的描述。这些更多是内部设计文档的完善，对日常 Go 程序的编写并无直接影响，但体现了 Go 语言设计本身的严谨性和持续迭代。兼容性，依然是 Go 坚不可摧的基石。</p>
<blockquote>
<p>更详细地说明可以参考我之前的文章《<a href="https://tonybai.com/2025/03/27/remove-coretypes-from-go-spec/">Go 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路</a>》。</p>
</blockquote>
<h2>运行时与编译器：性能与可靠性的“幕后推手”</h2>
<p>这一部分是 Go 1.25 带来诸多“无形”强大之处的集中体现，它们直接影响着 Go 程序的运行效率和稳定性。</p>
<h3>容器感知型 GOMAXPROCS：更懂容器的 CPU 脾气</h3>
<p>在容器化部署日益普及的今天，Go 程序在 Kubernetes 等环境中运行，常常会遇到一个问题：GOMAXPROCS（控制 Go 运行时使用的最大 CPU 核心数）默认值是宿主机逻辑 CPU 数，而非容器实际被分配的 CPU 限制。这可能导致 CPU 资源浪费，或程序试图抢占过多资源，进而引发调度问题。</p>
<p>Go 1.25 带来了重大改进：在 Linux 系统上，Go 运行时现在会<strong>默认考虑 cgroup 的 CPU 限制（即容器的 CPU limit）</strong> 来设置 GOMAXPROCS 的默认值。如果 CPU limit 低于宿主机核心数，GOMAXPROCS 将自动降到这个更低的限制。此外，Go 运行时还会<strong>定期更新 GOMAXPROCS</strong>，以适应 cgroup 限制的动态变化。这一改进，直接解决了 Go 应用在容器环境中可能存在的资源配置不当问题，使得 Go 程序在 K8s 等云原生环境中运行时更加高效和“智能”，真正做到“物尽其用”。</p>
<blockquote>
<p>更详细地说明可以参考我之前的文章《<a href="https://tonybai.com/2025/04/09/gomaxprocs-defaults-add-cgroup-aware/">Go 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？</a>》。</p>
</blockquote>
<h3>新的实验性垃圾收集器：GC开销有望显著降低</h3>
<p>Go 1.25 引入了一个<strong>新的实验性垃圾收集器</strong>，可以通过设置 GOEXPERIMENT=greenteagc 在构建时启用。这个新 GC 的设计旨在改进小对象的标记和扫描性能，并提升 CPU 可扩展性。</p>
<p>根据官方的基准测试，在实际应用中，垃圾回收的开销有望减少 <strong>10% 到 40%</strong>！如果这一实验性优化最终成熟并默认启用，将显著降低 Go 程序的 GC 停顿和整体资源消耗，对于所有 Go 应用（尤其是内存密集型应用）来说，这无疑是巨大的性能红利。</p>
<blockquote>
<p>更详细地说明可以参考我之前的文章《<a href="https://tonybai.com/2025/05/03/go-green-tea-garbage-collector/">Go新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？</a>》。</p>
</blockquote>
<h3>更精准的 Nil Pointer Panic：让隐藏的 Bug 无所遁形</h3>
<p>这是一个虽然可能“打破”一些旧代码，但从长远来看极为重要的改进。Go 1.21 到 1.24 版本之间曾存在一个编译器 bug，导致某些在 os.Open 返回 nil 错误时，仍能“幸运地”继续运行并访问 nil 指针，而没有立即 panic。</p>
<pre><code class="go">// Go 1.21-1.24 曾因编译器bug可能不panic的示例
package main
import "os"
func main() {
    f, err := os.Open("nonExistentFile") // err != nil, f 是 nil
    name := f.Name() // 这里访问了 nil.Name()，但可能不panic
    if err != nil {
        return
    }
    println(name)
}
</code></pre>
<p>在 Go 1.25 中，这个编译器 bug 已经被修复，确保 nil 指针检查会及时且准确地执行。这意味着，上述示例中的代码在 Go 1.25 中将明确引发 nil 指针 panic。</p>
<p>这一变化提高了 Go 程序的运行时可靠性，让那些原本被编译器“侥幸放过”的隐藏 Bug 得以暴露。如果你的代码中存在类似问题，升级后可能需要进行修正，将非 nil 错误检查提前到使用变量之前。</p>
<h3>DWARF版本5 支持：更小更快，调试无忧</h3>
<p>Go 1.25 的编译器和链接器现在默认生成 <strong>DWARFv5 调试信息</strong>。这种更新的调试信息格式，可以有效减少 Go 二进制文件中调试信息所需的空间，并缩短程序的链接时间，对于构建大型 Go 应用程序尤其有利，有助于提升开发效率和 CI/CD 流程的速度。</p>
<blockquote>
<p>更详细地说明可以参考我之前的文章《<a href="https://tonybai.com/2025/05/08/go-dwarf5/">Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地</a>》。</p>
</blockquote>
<h2>工具链：武装开发者，提升效率</h2>
<p>Go 语言强大的工具链是其生产力的重要保障。Go 1.25 在此基础上进一步发力，带来多项实用改进。</p>
<ul>
<li><strong>go build -asan 默认内存泄漏检测：Cgo 混合编程更安全</strong></li>
</ul>
<p>对于涉及到 Go 与 C/C++ 代码混合编程的场景，内存泄漏诊断一直是个挑战。Go 1.25 中，go build -asan 选项现在默认在程序退出时进行<strong>内存泄漏检测</strong>，能够报告 C 语言分配但未释放的内存。这大大增强了 Go 混合编程时的内存安全性，有助于发现原生代码中的隐蔽内存问题。</p>
<ul>
<li><strong>go.mod ignore directive：灵活管理超大型仓库</strong></li>
</ul>
<p>go.mod 文件新增了 ignore directive，允许你指定 Go 命令在匹配包模式（如 all 或 ./&#8230;）时应忽略的目录。这些目录下的文件不会被 Go 命令扫描和处理。这对于管理包含大量非 Go 代码、文档、或子模块的超大型代码仓库（Monorepo）非常有用，可以减少构建和扫描时间，提高 Go Modules 的灵活性。</p>
<blockquote>
<p>更详细地说明可以参考我之前的文章《<a href="https://tonybai.com/2025/05/22/go-mod-ignore-directive/">Go工具链进化：go.mod新增ignore指令，破解混合项目构建难题</a>》。</p>
</blockquote>
<ul>
<li><strong>go doc -http：本地文档，即开即用</strong></li>
</ul>
<p>一个看似小巧但能极大提升开发体验的改进。新的 go doc -http 选项，可以启动一个本地文档服务器，显示指定 Go 对象的文档，并自动在浏览器中打开。从此，查阅 Go 文档变得更加便捷、直观。</p>
<blockquote>
<p>更详细地说明可以参考我之前的文章《<a href="https://tonybai.com/2024/09/06/go-doc-add-http-support/">重拾精髓：go doc -http让离线包文档浏览更便捷</a>》。</p>
</blockquote>
<ul>
<li><strong>Vet 工具新分析器：提前发现常见 Bug</strong></li>
</ul>
<p>go vet 工具新增了两个实用的分析器。一个是waitgroup，能报告 sync.WaitGroup.Add 的不正确调用位置（例如在 go 协程内部调用）。另外一个是hostport，能检测并建议修正 fmt.Sprintf(“%s:%d”, host, port) 这种不兼容 IPv6 的地址构造方式，推荐使用 net.JoinHostPort。</p>
<p>这些分析器能帮助开发者在编码阶段就避免常见的并发和网络编程陷阱，进一步提升代码质量和可靠性。</p>
<h2>标准库：功能增强与实验性探索</h2>
<p>标准库的不断演进是 Go 保持活力的重要源泉。Go 1.25 在此也带来了多项关键变化。</p>
<h3>testing/synctest：并发测试的新利器</h3>
<p>Go 1.25 引入了全新的 testing/synctest 包，为并发代码的测试提供了原生支持。它允许你在一个隔离的“气泡”（bubble）中运行测试函数，并且能够控制测试环境中时间（使用伪造时钟）和协程的阻塞/恢复。这极大地方便了并发代码的调试和测试，尤其是那些依赖时间或 Goroutine 调度顺序的复杂场景，提高了测试的可靠性和可控性。</p>
<p>关于该特性，我曾编写过一个“<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=1509674724631609344#wechat_redirect">征服Go并发测试</a>”的微专栏，欢迎大家扫描订阅，了解关于synctest的设计、实现以及实践方式。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-concurrent-test-qr.png" alt="" /></p>
<h3>encoding/json/v2 实验性版本：高性能 JSON 编解码展望</h3>
<p>Go 1.25 引入了一个<strong>新的、实验性的 encoding/json/v2 包</strong>，可以通过设置 GOEXPERIMENT=jsonv2 环境变量在构建时启用。这是对 Go 核心 encoding/json 包的一次重大修订，旨在提升性能和提供更灵活的配置选项。根据初步测试，新实现<strong>在解码性能上显著优于现有版本</strong>，并提供了更多配置 marshaler 和 unmarshaler 的选项。</p>
<p>这是一个令人兴奋的实验性功能，预示着 Go 的 JSON 编解码能力未来将更上一层楼。但作为实验性特性，Go 团队鼓励开发者积极测试自己的程序，并向社区提供反馈，帮助其持续演进。</p>
<blockquote>
<p>关于jsonv2使用的更详细地介绍可以参考我之前的文章《<a href="https://tonybai.com/2025/05/15/go-json-v2/">手把手带你玩转GOEXPERIMENT=jsonv2：Go下一代JSON库初探</a>》。</p>
</blockquote>
<h3>crypto/tls 持续增强：安全与隐私不放松</h3>
<p>Go 在密码学领域的投入从未停止。Go 1.25 中的 crypto/tls 包获得了多项改进：</p>
<ul>
<li>新增 Config.GetEncryptedClientHelloKeys 回调，支持 <strong>Encrypted Client Hello (ECH)</strong> 扩展，进一步提升 TLS 客户端的连接隐私。</li>
<li>默认禁用 TLS 1.2 握手中的 SHA-1 签名算法（但可以通过 tlssha1=1 的 GODEBUG 选项重新启用）。</li>
<li>在<a href="https://tonybai.com/2025/05/21/go-crypto-audit/"> FIPS 140-3 模式</a>下，允许使用更现代的 Ed25519 和 X25519MLKEM768 密钥交换算法。</li>
</ul>
<p>这些改进持续强化了 Go TLS 的安全性、隐私保护和合规性，为迎接未来的量子安全和更严格的安全标准做准备。</p>
<h3>unique 包改进：内存优化再进一步</h3>
<p>unique 包现在能更积极、高效地回收内部化值，有效减少在处理大量重复值时可能出现的内存膨胀问题。这对于 Go 编译器、LSP (Language Server Protocol) 等会大量使用 unique 包的场景，将带来显著的内存和性能优化。</p>
<h3>sync.WaitGroup.Go：并发模式更便捷</h3>
<p>sync.WaitGroup 新增了 Go 方法，为创建和计数 goroutine 提供了一个更便捷的封装，进一步简化了 Go 中常见的并发模式的写法。在之前的文章《<a href="https://tonybai.com/2025/04/03/waitgroup-go-proposal/">WaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码</a>》有对这一特性来龙去脉的纤细说明。</p>
<h2>小结</h2>
<p>Go 1.25 的预发布版本，清晰地展现了 Go 语言在性能、可靠性、安全性和开发者体验上的全面提升。这些变化，无论是底层运行时的“无形”优化，还是工具链的智能辅助，都紧密围绕着 Go“生产力”和“生产就绪”的核心原则。</p>
<p>作为 Go 开发者，我们能从中获得的益处是巨大的：你不需要成为系统底层的专家，便能享受到 Go 团队带来的最新技术红利。这种“升级即获益”的模式，正是 Go 语言独特魅力的体现。</p>
<p>Go 语言的旅程永不停歇，它在不断地进化和完善。我鼓励所有 Go 开发者，积极尝试 Go 1.25 RC1 版本，将其应用到你的开发、测试环境中，并向 Go 团队提供宝贵的反馈。你的参与，将是对Go 团队最大的帮助。</p>
<hr />
<p><strong>精进有道，更上层楼</strong></p>
<p><a href="https://mp.weixin.qq.com/s/GWGWTfCRCsOJ_4Pk-pxpHA">极客时间《Go语言进阶课》上架刚好一个月</a>，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。</p>
<p>现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。</p>
<p>这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、更高效、更可靠的生产级代码！</p>
<p>扫描下方二维码，立即开启你的 Go 语言进阶之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<p><strong>感谢阅读！</strong></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/06/14/go-1-25-foresight/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！</title>
		<link>https://tonybai.com/2025/05/12/go-advanced-course/</link>
		<comments>https://tonybai.com/2025/05/12/go-advanced-course/#comments</comments>
		<pubDate>Mon, 12 May 2025 00:33:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopherchina]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go语言进阶课]]></category>
		<category><![CDATA[Go高级工程师必修课]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[MCP]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[pitfall]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[trap]]></category>
		<category><![CDATA[TypeScript]]></category>
		<category><![CDATA[TypeSystem]]></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>

		<guid isPermaLink="false">https://tonybai.com/?p=4687</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/12/go-advanced-course 大家好，我是Tony Bai。 今天，怀着一丝激动和期待，我想向大家宣布一个酝酿已久的好消息：我的新专栏“TonyBai · Go 语言进阶课” 终于在极客时间正式上架了！ 这门课程的诞生，其实有一段不短的故事。它并非一时兴起，而是源于我对 Go 语言多年实践的沉淀、对 Gopher 们进阶痛点的洞察，以及一份希望能帮助更多开发者突破瓶颈、实现精通的心愿。 缘起：从 GopherChina 的线下训练营开始 故事的起点，要追溯到 GopherChina 2023 大会前夕。当时，我应邀开设了一期名为“Go 高级工程师必修课”的线下训练营。至今还清晰记得，在滴滴的一个会议室里，我与一群对 Go 语言充满热忱的开发者们，共同探讨、深入剖析了 Go 进阶之路上的种种挑战与关键技能。 那次线下课程的反馈非常积极，也让我深刻感受到，许多 Gopher 在掌握了 Go 的基础之后，普遍面临着“如何从熟练到精通”的困惑。他们渴望写出更优雅、更高性能的代码，希望提升复杂项目的设计能力，也期盼着能掌握更硬核的工程实践经验。 同年，我还临危受命，在 GopherChina 2023 上加了一场 “The State Of Go” 的演讲，与大家分享了我对 Go 语言发展趋势的观察与思考。这些经历，都让我更加坚信，系统性地梳理和分享 Go 语言的进阶知识，是非常有价值且必要的。 打磨：从线下到线上，不变的是匠心 将线下课程的精华沉淀下来，打磨成一门更普惠、更系统的线上专栏，这个想法在 2024 年就已萌生。但由于种种原因，特别是档期的冲突，这个计划暂时搁置了。 直到 2025 年，我与极客时间的老师们再次携手，投入了大量心血，对课程内容进行了反复打磨和精心编排。我们不仅希望传递知识，更希望启发思考，帮助大家建立起真正的“Go 语言设计思维和工程思维”。 正如我在专栏开篇词中提到的，如果你也正面临这些困惑： 感觉到了瓶颈？ [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-advanced-course-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/12/go-advanced-course">本文永久链接</a> &#8211; https://tonybai.com/2025/05/12/go-advanced-course</p>
<p>大家好，我是Tony Bai。</p>
<p>今天，怀着一丝激动和期待，我想向大家宣布一个酝酿已久的好消息：我的新专栏<strong>“<a href="http://gk.link/a/12yGY">TonyBai · Go 语言进阶课</a>”</strong> 终于在极客时间正式上架了！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-1.jpg" alt="" /></p>
<p>这门课程的诞生，其实有一段不短的故事。它并非一时兴起，而是源于我对 Go 语言多年实践的沉淀、对 Gopher 们进阶痛点的洞察，以及一份希望能帮助更多开发者突破瓶颈、实现精通的心愿。</p>
<h2>缘起：从 GopherChina 的线下训练营开始</h2>
<p>故事的起点，要追溯到 GopherChina 2023 大会前夕。当时，我应邀开设了一期名为“Go 高级工程师必修课”的线下训练营。至今还清晰记得，在滴滴的一个会议室里，我与一群对 Go 语言充满热忱的开发者们，共同探讨、深入剖析了 Go 进阶之路上的种种挑战与关键技能。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-advanced-training-2023-2.png" alt="GopherChina 2023 “Go高级工程师必修课”线下训练营图片" /></p>
<p>那次线下课程的反馈非常积极，也让我深刻感受到，许多 Gopher 在掌握了 Go 的基础之后，普遍面临着“如何从熟练到精通”的困惑。他们渴望写出更优雅、更高性能的代码，希望提升复杂项目的设计能力，也期盼着能掌握更硬核的工程实践经验。</p>
<p>同年，我还临危受命，在 GopherChina 2023 上加了一场 “The State Of Go” 的演讲，与大家分享了我对 Go 语言发展趋势的观察与思考。这些经历，都让我更加坚信，系统性地梳理和分享 Go 语言的进阶知识，是非常有价值且必要的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-advanced-course-2.png" alt="" /></p>
<h2>打磨：从线下到线上，不变的是匠心</h2>
<p>将线下课程的精华沉淀下来，打磨成一门更普惠、更系统的线上专栏，这个想法在 2024 年就已萌生。但由于种种原因，特别是档期的冲突，这个计划暂时搁置了。</p>
<p>直到 2025 年，我与极客时间的老师们再次携手，投入了大量心血，对课程内容进行了反复打磨和精心编排。我们不仅希望传递知识，更希望启发思考，帮助大家建立起真正的“Go 语言设计思维和工程思维”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-3.jpg" alt="" /></p>
<p>正如我在专栏开篇词中提到的，如果你也正面临这些困惑：</p>
<ul>
<li><strong>感觉到了瓶颈？</strong> 写了不少 Go 代码，但总觉得离“精通”还差一口气？</li>
<li><strong>设计能力跟不上？</strong> 面对复杂的业务需求，如何进行合理的项目布局、包设计、接口设计？</li>
<li><strong>工程实践经验不足？</strong> 知道要测试、要监控、要优化，但具体到 Go 项目，如何落地？</li>
</ul>
<p>那么，这门“Go 语言进阶课”正是为你量身打造的。</p>
<h2>蜕变：从“熟练工”到“专家”，三大模块助你突破</h2>
<p>课程摒弃了简单罗列知识点的方式，聚焦于 Go 工程师能力提升的三个核心维度，精心设计了三大模块：</p>
<ul>
<li><strong>模块一：夯实基础，突破语法认知瓶颈</strong><br />
这里我们不满足于“知道”，而是追求“理解”。深入类型系统、值与指针、切片与 map 陷阱、接口与组合、泛型等核心概念的底层逻辑与设计哲学，让你写出更地道、更健壮的 Go 代码。</li>
<li><strong>模块二：设计先行，奠定高质量代码基础</strong><br />
从宏观的项目布局、包设计，到具体的并发模型选择、接口设计原则，再到实用的错误处理策略和 API 设计规范。提升你的软件设计能力，让你能驾驭更复杂的项目。</li>
<li><strong>模块三：工程实践，锻造生产级 Go 服务</strong><br />
聚焦于将 Go 代码变成可靠线上服务的关键环节。从应用骨架、核心组件、可观测性，到故障排查、性能调优、云原生部署以及与 AI 大模型集成，全是硬核干货。</li>
</ul>
<p>此外，课程还安排了<strong>实战串讲项目</strong>，带你将学到的知识融会贯通，亲手构建并完善一个真实的 Go 服务。</p>
<p>我深知，从“熟练”到“精通”，不是一蹴而就的。但这门课程，希望能成为你进阶路上的助推器和导航仪。它凝聚了我 20 多年的行业经验，特别是我在电信领域高并发网关和智能网联汽车车云平台使用 Go 语言构建大规模生产系统的实践与思考。</p>
<p>在课程中，你不仅能学到 Go 的高级特性和用法，更能体会到 Go 语言“组合优于继承”、“显式错误处理”等设计哲学的精髓，以及在大模型时代如何让 AI 赋能你的 Go 应用。</p>
<h2>现在，是时候了！</h2>
<p>正如我在开篇词中强调的，Go 语言正迎来它的黄金十年。从 TIOBE 榜单的稳步攀升（2025 年 4 月份额已突破 3%），到全球 GopherCon 的回归，再到各大主流厂商对 Go 的拥抱（比如 TypeScript 编译器向 Go 移植、Grafana 和 GitHub 用 Go 重写 MCP Server），都预示着 Go 在云原生、微服务、AI 后端等领域的强劲势头。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-advanced-course-3.png" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/2025/go-advanced-course-4.png" alt="" /></p>
<p>现在，正是学习和进阶 Go 的最佳时机！</p>
<p>如果你渴望突破瓶颈，实现从“Go 熟练工”到“Go 专家”的蜕变，那么，我在极客时间的《TonyBai · Go 语言进阶课》等你！</p>
<p><strong>扫描下方二维码或点击[阅读原文]，立即加入，开启你的 Go 语言精进之旅！</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-4.png" alt="" /></p>
<p>期待与你在课程中相遇，共同探索 Go 语言的精妙与强大！</p>
<p>最后，一个小小的请求：</p>
<p>如果你身边有正在 Go 语言进阶道路上摸索，或者渴望提升 Go 工程实践与设计能力的 Gopher 朋友、同事，<strong>请将这篇文章或课程信息分享给他们</strong>。 每一份善意的传递，都可能为他人的技术成长点亮一盏灯。</p>
<p>也欢迎大家在评论区踊跃交流，分享你对 Go 进阶的困惑、经验或对课程的期待。让我们一起，在 Go 的世界里，持续学习，共同进步！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/12/go-advanced-course/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技</title>
		<link>https://tonybai.com/2025/05/06/cheating-the-reaper-in-go/</link>
		<comments>https://tonybai.com/2025/05/06/cheating-the-reaper-in-go/#comments</comments>
		<pubDate>Tue, 06 May 2025 04:12:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alloc]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[chunk]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Memory]]></category>
		<category><![CDATA[Object]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[pool]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[sync.Pool]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[位图]]></category>
		<category><![CDATA[内存管理]]></category>
		<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=4654</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/06/cheating-the-reaper-in-go 大家好，我是Tony Bai。 Go语言以其强大的垃圾回收 (GC) 机制解放了我们这些 Gopher 的心智，让我们能更专注于业务逻辑而非繁琐的内存管理。但你有没有想过，在 Go 这个看似由 GC “统治”的世界里，是否也能体验一把“手动管理”内存带来的极致性能？甚至，能否与 GC “斗智斗勇”，让它为我们所用？ 事实上，Go 官方也曾进行过类似的探索。 他们尝试在标准库中加入一个arena包，提供一种基于区域 (Region-based) 的内存管理机制。测试表明，这种方式确实能在特定场景下通过更早的内存复用和减少 GC 压力带来显著的性能提升。然而，这个官方的 Arena 提案最终被无限期搁置了。原因在于，Arena 这种手动内存管理机制与 Go 语言现有的大部分特性和标准库组合得很差 (compose poorly)。 官方的尝试尚且受阻，那么个人开发者在 Go 中玩转手动内存管理又会面临怎样的挑战呢？最近，一篇名为 “Cheating the Reaper in Go” (在 Go 中欺骗死神/收割者) 的文章在技术圈引起了不小的关注。作者 mcyoung 以其深厚的底层功底，展示了如何利用unsafe包和对 Go GC 内部运作机制的深刻理解，构建了一个非官方的、实验性的高性能内存分配器——Arena。 这篇文章的精彩之处不仅在于其最终实现的性能提升，更在于它揭示了在 Go 中进行底层内存操作的可能性、挑战以及作者与 GC “共舞”的巧妙思路。需要强调的是，本文的目的并非提供一个生产可用的 Arena 实现（官方尚且搁置，其难度可见一斑），而是希望通过解读作者这次与 GC [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/cheating-the-reaper-in-go-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/06/cheating-the-reaper-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/06/cheating-the-reaper-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>Go语言以其强大的垃圾回收 (GC) 机制解放了我们这些 Gopher 的心智，让我们能更专注于业务逻辑而非繁琐的内存管理。但你有没有想过，在 Go 这个看似由 GC “统治”的世界里，是否也能体验一把“手动管理”内存带来的极致性能？甚至，能否与 GC “斗智斗勇”，让它为我们所用？</p>
<p><strong>事实上，Go 官方也曾进行过类似的探索。</strong> 他们尝试在标准库中加入一个arena包，提供一种基于区域 (Region-based) 的内存管理机制。测试表明，这种方式确实能在特定场景下通过<strong>更早的内存复用</strong>和<strong>减少 GC 压力</strong>带来显著的性能提升。然而，这个官方的 Arena 提案最终<strong>被无限期搁置了</strong>。原因在于，Arena 这种手动内存管理机制与 Go 语言现有的大部分特性和标准库<strong>组合得很差 (compose poorly)</strong>。</p>
<p>官方的尝试尚且受阻，那么个人开发者在 Go 中玩转手动内存管理又会面临怎样的挑战呢？最近，一篇名为 <strong>“Cheating the Reaper in Go”</strong> (在 Go 中欺骗死神/收割者) 的文章在技术圈引起了不小的关注。作者 mcyoung 以其深厚的底层功底，展示了如何利用unsafe包和对 Go GC 内部运作机制的深刻理解，构建了一个<strong>非官方的、实验性的</strong>高性能内存分配器——Arena。</p>
<p>这篇文章的精彩之处不仅在于其最终实现的性能提升，更在于它<strong>揭示了在 Go 中进行底层内存操作的可能性、挑战以及作者与 GC “共舞”的巧妙思路</strong>。<strong>需要强调的是，本文的目的并非提供一个生产可用的 Arena 实现（官方尚且搁置，其难度可见一斑），而是希望通过解读作者这次与 GC “斗智斗勇”的“黑科技”，和大家一起更深入地理解 Go 的底层运作机制。</strong></p>
<h2>为何还要探索 Arena？理解其性能诱惑</h2>
<p>即使官方受阻，理解 Arena 的理念依然有价值。它针对的是 Go 自动内存管理在某些场景下的潜在瓶颈：</p>
<ul>
<li><strong>高频、小对象的分配与释放：</strong> 频繁触碰 GC 可能带来开销。</li>
<li><strong>需要统一生命周期管理的内存：</strong> 一次性处理比零散回收更高效。</li>
</ul>
<p>Arena 通过<strong>批量申请、内部快速分配、集中释放</strong>（在 Go 中通常是让 Arena 不可达由 GC 回收）的策略，试图在这些场景下取得更好的性能。</p>
<h2>核心挑战：Go 指针的“特殊身份”与 GC 的“规则”</h2>
<p>作者很快指出了在 Go 中实现 Arena 的核心障碍：<strong>Go 的指针不是普通的数据</strong>。GC 需要通过<strong>指针位图 (Pointer Bits)</strong> 来识别内存中的指针，进行可达性分析。而自定义分配的原始内存块缺乏这些信息。</p>
<p>作者提供了一个类型安全的泛型函数New[T]来在 Arena 上分配对象：</p>
<pre><code class="go">type Allocator interface {
  Alloc(size, align uintptr) unsafe.Pointer
}

// New allocates a fresh zero value of type T on the given allocator, and
// returns a pointer to it.
func New[T any](a Allocator) *T {
  var t T
  p := a.Alloc(unsafe.Sizeof(t), unsafe.Alignof(t))
  return (*T)(p)
}
</code></pre>
<p>但问题来了，如果我们这样使用：</p>
<pre><code class="go">p := New[*int](myAlloc) // myAlloc是一个实现了Allocator接口的arena实现
*p = new(int)
runtime.GC()
**p = 42  // Use after free! 可能崩溃!
</code></pre>
<p>因为 Arena 分配的内存对 GC 不透明，GC 看不到里面存储的指向new(int)的指针。当runtime.GC()执行时，它认为new(int)分配的对象已经没有引用了，就会将其回收。后续访问**p就会导致 Use After Free。</p>
<h2>“欺骗”GC 的第一步：让 Arena 整体存活</h2>
<p>面对这个难题，作者的思路是：<strong>让 GC 知道 Arena 的存在，并间接保护其内部分配的对象</strong>。关键在于确保：<strong>只要 Arena 中有任何一个对象存活，整个 Arena 及其所有分配的内存块（Chunks）都保持存活。</strong></p>
<p>这至关重要，通过强制标记整个 arena，arena 中存储的任何指向其自身的指针将自动保持活动状态，而无需 GC 知道如何扫描它们。所以，虽然这样做后， &#42;New&#91;&#42;int&#93;(a) = new(int) 仍然会导致释放后重用，但 &#42;New&#91;&#42;int&#93;(a) = New&#91;int&#93;(a) 不会！即arena上分配的指针仅指向arena上的内存块。 这个小小的改进并不能保证 arena 本身的安全，但只要进入 arena 的指针完全来自 arena 本身，那么拥有内部 arena 的数据结构就可以完全安全。</p>
<p><strong>1. 基本 Arena 结构与快速分配</strong></p>
<p>首先，定义 Arena 结构，包含指向下一个可用位置的指针next和剩余空间left。其核心分配逻辑 (Alloc) 主要是简单的指针碰撞：</p>
<pre><code class="go">package arena

import "unsafe"

type Arena struct {
    next  unsafe.Pointer // 指向当前 chunk 中下一个可分配位置
    left  uintptr        // 当前 chunk 剩余可用字节数
    cap   uintptr        // 当前 chunk 的总容量 (用于下次扩容参考)
    // chunks 字段稍后添加
}

const (
    maxAlign uintptr = 8 // 假设 64 位系统最大对齐为 8
    minWords uintptr = 8 // 最小分配块大小 (以字为单位)
)

func (a *Arena) Alloc(size, align uintptr) unsafe.Pointer {
    // 1. 对齐 size 到 maxAlign (简化处理)
    mask := maxAlign - 1
    size = (size + mask) &amp;^ mask
    words := size / maxAlign

    // 2. 检查当前 chunk 空间是否足够
    if a.left &lt; words {
        // 空间不足，分配新 chunk
        a.newChunk(words) // 假设 newChunk 会更新 a.next, a.left, a.cap
    }

    // 3. 在当前 chunk 中分配 (指针碰撞)
    p := a.next
    // (优化后的代码，去掉了检查 one-past-the-end)
    a.next = unsafe.Add(a.next, size)
    a.left -= words

    return p
}

</code></pre>
<p><strong>2. 持有所有 Chunks</strong></p>
<p>为了防止 GC 回收 Arena 已经分配但next指针不再指向的旧 Chunks，需要在 Arena 中明确持有它们的引用：</p>
<pre><code class="go">type Arena struct {
    next  unsafe.Pointer
    left, cap uintptr
    chunks []unsafe.Pointer  // 新增：存储所有分配的 chunk 指针
}

// 在 Alloc 函数的 newChunk 调用之后，需要将新 chunk 的指针追加到 a.chunks
// 例如，在 newChunk 函数内部实现: a.chunks = append(a.chunks, newChunkPtr)
</code></pre>
<p>原文测试表明，这个append操作的成本是摊销的，对整体性能影响不大，结果基本与没有chunks字段时持平。</p>
<p><strong>3. 关键技巧：Back Pointer</strong></p>
<p>是时候保证整个arena安全了！这是“欺骗”GC 的核心。通过reflect.StructOf动态创建包含unsafe.Pointer字段的 Chunk 类型，并在该字段写入指向 Arena 自身的指针：</p>
<pre><code class="go">import (
    "math/bits"
    "reflect"
    "unsafe"
)

// allocChunk 创建新的内存块并设置 Back Pointer
func (a *Arena) allocChunk(words uintptr) unsafe.Pointer {
    // 使用 reflect.StructOf 创建动态类型 struct { Data [N]uintptr; BackPtr unsafe.Pointer }
    chunkType := reflect.StructOf([]reflect.StructField{
        {
            Name: "Data", // 用于分配
            Type: reflect.ArrayOf(int(words), reflect.TypeFor[uintptr]()),
        },
        {
            Name: "BackPtr", // 用于存储 Arena 指针
            Type: reflect.TypeFor[unsafe.Pointer](), // !! 必须是指针类型，让 GC 扫描 !!
        },
    })

    // 分配这个动态结构体
    chunkPtr := reflect.New(chunkType).UnsafePointer()

    // 将 Arena 自身指针写入 BackPtr 字段 (位于末尾)
    backPtrOffset := words * maxAlign // Data 部分的大小
    backPtrAddr := unsafe.Add(chunkPtr, backPtrOffset)
    *(**Arena)(backPtrAddr) = a // 写入 Arena 指针

    // 返回 Data 部分的起始地址，用于后续分配
    return chunkPtr
}

// newChunk 在 Alloc 中被调用，用于更新 Arena 状态
func (a *Arena) newChunk(requestWords uintptr) {
    newCapWords := max(minWords, a.cap*2, nextPow2(requestWords)) // 计算容量
    a.cap = newCapWords

    chunkPtr := a.allocChunk(newCapWords) // 创建新 chunk 并写入 BackPtr

    a.next = chunkPtr // 更新 next 指向新 chunk 的 Data 部分
    a.left = newCapWords // 更新剩余容量

    // 将新 chunk (整个 struct 的指针) 加入列表
    a.chunks = append(a.chunks, chunkPtr)
}

// (nextPow2 和 max 函数省略)
</code></pre>
<p>通过这个 Back Pointer，任何指向 Arena 分配内存的外部指针，最终都能通过 GC 的扫描链条将 Arena 对象本身标记为存活，进而保活所有 Chunks。这样，Arena 内部的指针（指向 Arena 分配的其他对象）也就安全了！原文的基准测试显示，引入 Back Pointer 的reflect.StructOf相比直接make([]uintptr)对性能有轻微但可察觉的影响。</p>
<h2>性能再“压榨”：消除冗余的 Write Barrier</h2>
<p>分析汇编发现，Alloc函数中更新a.next(如果类型是unsafe.Pointer) 会触发 <strong>Write Barrier</strong>。这是 GC 用来追踪指针变化的机制，但在 Back Pointer 保证了 Arena 整体存活的前提下，这里的 Write Barrier 是冗余的。</p>
<p>作者的解决方案是将next改为uintptr：</p>
<pre><code class="go">type Arena struct {
    next  uintptr // &lt;--- 改为 uintptr
    left  uintptr
    cap   uintptr
    chunks []unsafe.Pointer
}

func (a *Arena) Alloc(size, align uintptr) unsafe.Pointer {
    // ... (对齐和检查 a.left &lt; words 逻辑不变) ...
    if a.left &lt; words {
        a.newChunk(words) // newChunk 内部会设置 a.next (uintptr)
    }

    p := a.next // p 是 uintptr
    a.next += size // uintptr 直接做加法，无 Write Barrier
    a.left -= words

    return unsafe.Pointer(p) // 返回时转换为 unsafe.Pointer
}

// newChunk 内部设置 a.next 时也应存为 uintptr
func (a *Arena) newChunk(requestWords uintptr) {
    // ... (allocChunk 不变) ...
    chunkPtr := a.allocChunk(newCapWords)
    a.next = uintptr(chunkPtr) // &lt;--- 存为 uintptr
    // ... (其他不变) ...
}
</code></pre>
<p>这个优化效果如何？原文作者在一个 GC 压力较大的场景下（通过一个 goroutine 不断调用runtime.GC()模拟）进行了测试，结果表明，<strong>对于小对象的分配，消除 Write Barrier 带来了大约 20% 的性能提升</strong>。这证明了在高频分配场景下，即使是 Write Barrier 这样看似微小的开销也可能累积成显著的性能瓶颈。</p>
<h2>更进一步的可能：Arena 复用与sync.Pool</h2>
<p>文章还提到了一种潜在的优化方向：<strong>Arena 的复用</strong>。当一个 Arena 完成其生命周期后（例如，一次请求处理完毕），其占用的内存理论上可以被“重置”并重新利用，而不是完全交给 GC 回收。</p>
<p>作者建议，可以将不再使用的 Arena 对象放入sync.Pool中。下次需要 Arena 时，可以从 Pool 中获取一个已经分配过内存块的 Arena 对象，只需重置其next和left指针即可开始新的分配。这样做的好处是：</p>
<ul>
<li><strong>避免了重复向 GC 申请大块内存</strong>。</li>
<li>可能<strong>节省了重复清零内存</strong>的开销（如果 Pool 返回的 Arena 内存恰好未被 GC 清理）。</li>
</ul>
<p>这需要更复杂的 Arena 管理逻辑（如 Reset 方法），但对于需要大量、频繁创建和销毁 Arena 的场景，可能带来进一步的性能提升。</p>
<h2>unsafe：通往极致性能的“危险边缘”</h2>
<p>贯穿整个 Arena 实现的核心是unsafe包。作者坦诚地承认，这种实现方式严重依赖 Go 的内部实现细节和unsafe提供的“后门”。</p>
<p>这再次呼应了 Go 官方搁置 Arena 的原因——它与语言的安全性和现有机制的兼容性存在天然的矛盾。使用unsafe意味着：</p>
<ul>
<li>放弃了类型和内存安全保障。</li>
<li>代码变得脆弱，可能因 Go 版本升级而失效（尽管作者基于<a href="https://tonybai.com/2025/04/26/13-laws-of-software-engineering">Hyrum 定律</a>认为风险相对可控）。</li>
<li>可读性和可维护性显著降低。</li>
</ul>
<h2>小结</h2>
<p>“Cheating the Reaper in Go” 为我们呈现了一场精彩的、与 Go GC “共舞”的“黑客艺术”。通过对 GC 原理的深刻洞察和对unsafe包的大胆运用，作者展示了在 Go 中实现高性能自定义内存分配的可能性，虽然作者的实验性实现是一个toy级别的。</p>
<p>然而，正如 Go 官方的 Arena 实验所揭示的，将这种形式的手动内存管理完美融入 Go 语言生态，面临着巨大的挑战和成本。因此，我们应将这篇文章更多地视为一次理解 Go 底层运作机制的“思想实验”和“案例学习”，而非直接照搬用于生产环境的蓝图。</p>
<p>对于绝大多数 Go 应用，内建的内存分配器和 GC 依然是最佳选择。但通过这次“与死神共舞”的探索之旅，我们无疑对 Go 的底层世界有了更深的敬畏和认知。</p>
<p><strong>你如何看待在 Go 中使用unsafe进行这类底层优化？官方 Arena 实验的受阻说明了什么？欢迎在评论区分享你的思考！</strong> 如果你对 Go 的底层机制和性能优化同样充满好奇，别忘了点个【赞】和【在看】！</p>
<p>原文链接：https://mcyoung.xyz/2025/04/21/go-arenas</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/06/cheating-the-reaper-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化</title>
		<link>https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver/</link>
		<comments>https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver/#comments</comments>
		<pubDate>Fri, 25 Apr 2025 04:16:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[dolt]]></category>
		<category><![CDATA[escape-analysis]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[heap]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[profiling]]></category>
		<category><![CDATA[receiver]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[STW]]></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>

		<guid isPermaLink="false">https://tonybai.com/?p=4617</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver 大家好，我是Tony Bai。 在软件开发的世界里，细节决定成败，这句话在以简洁著称的Go语言中同样适用，甚至有时会以更出人意料的方式体现出来。 想象一下这个场景：你正在对一个稳定的Go项目进行一次看似无害的“无操作（no-op）”重构，目标只是为了封装一些实现细节，提高代码的可维护性。然而，提交代码后，CI系统却亮起了刺眼的红灯——某个核心基准测试（比如 sysbench）的性能竟然骤降了30%！ （图片来源：Dolt博客原文） 这可不是什么虚构的故事，而是最近发生在Dolt（一个我长期关注的一个Go编写的带版本控制的SQL数据库）项目中的真实“性能血案”。一次旨在改进封装的重构，却意外触发了严重的性能衰退。 经过一番追踪和性能分析（Profiling），罪魁祸首竟然隐藏在代码中一个极其微小的改动里。今天，我们就来解剖这个案例，看看Go语言的内存分配机制，特别是值接收者（Value Receiver），是如何在这个过程中悄无声息地埋下性能地雷的。 案发现场：代码的前后对比 这次重构涉及一个名为 ImmutableValue 的类型，它大致包含了一个内容的哈希地址 (Addr)、一个可选的缓存字节切片 (Buf)，以及一个能根据哈希解析出数据的ValueStore接口。其核心方法 GetBytes 用于获取数据，如果缓存为空，则通过 ValueStore 加载。 重构的目标是将ValueStore的部分实现细节移入接口方法ReadBytes中。 重构前的简化代码： // (ImmutableValue 的定义和部分字段省略) func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) { if t.Buf == nil { // 直接调用内部的 load 方法填充 t.Buf err := t.load(ctx) if err != nil { return nil, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/hidden-costs-of-go-value-receiver-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver">本文永久链接</a> &#8211; https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver</p>
<p>大家好，我是Tony Bai。</p>
<p>在软件开发的世界里，细节决定成败，这句话在以简洁著称的Go语言中同样适用，甚至有时会以更出人意料的方式体现出来。</p>
<p>想象一下这个场景：你正在对一个稳定的Go项目进行一次看似无害的“无操作（no-op）”重构，目标只是为了封装一些实现细节，提高代码的可维护性。然而，提交代码后，CI系统却亮起了刺眼的红灯——某个核心基准测试（比如 sysbench）的<strong>性能竟然骤降了30%</strong>！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/hidden-costs-of-go-value-receiver-2.png" alt="" /><br />
<center>（图片来源：Dolt博客原文）</center></p>
<p>这可不是什么虚构的故事，而是最近发生在Dolt（一个我长期关注的一个Go编写的带版本控制的SQL数据库）项目中的真实“性能血案”。一次旨在改进封装的重构，却意外触发了严重的性能衰退。</p>
<p>经过一番追踪和性能分析（Profiling），罪魁祸首竟然隐藏在代码中一个极其微小的改动里。今天，我们就来解剖这个案例，看看Go语言的内存分配机制，特别是<strong>值接收者（Value Receiver）</strong>，是如何在这个过程中悄无声息地埋下性能地雷的。</p>
<h2>案发现场：代码的前后对比</h2>
<p>这次重构涉及一个名为 ImmutableValue 的类型，它大致包含了一个内容的哈希地址 (Addr)、一个可选的缓存字节切片 (Buf)，以及一个能根据哈希解析出数据的ValueStore接口。其核心方法 GetBytes 用于获取数据，如果缓存为空，则通过 ValueStore 加载。</p>
<p>重构的目标是将ValueStore的部分实现细节移入接口方法ReadBytes中。</p>
<p><strong>重构前的简化代码：</strong></p>
<pre><code>// (ImmutableValue 的定义和部分字段省略)

func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) {
  if t.Buf == nil {
      // 直接调用内部的 load 方法填充 t.Buf
      err := t.load(ctx)
      if err != nil {
          return nil, err
      }
  }
  return t.Buf[:], nil
}

func (t *ImmutableValue) load(ctx context.Context) error {
  // ... (省略部分检查)
  // 假设 valueStore 是 t 的一个字段，类型是 nodeStore 或类似具体类型
  t.valueStore.WalkNodes(ctx, t.Addr, func(ctx context.Context, n Node) error {
        if n.IsLeaf() {
            // 直接 append 到 t.Buf
            t.Buf = append(t.Buf, n.GetValue(0)...)
        }
        return nil // 简化错误处理
  })
  return nil
}
</code></pre>
<p><strong>重构后的简化代码：</strong></p>
<pre><code>// (ImmutableValue 定义同上)

func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) {
    if t.Buf == nil {
        if t.Addr.IsEmpty() {
            t.Buf = []byte{}
            return t.Buf, nil
        }
        // 通过 ValueStore 接口的 ReadBytes 方法获取数据
        buf, err := t.valueStore.ReadBytes(ctx, t.Addr)
        if err != nil {
            return nil, err
        }
        t.Buf = buf // 将获取到的 buf 赋值给 t.Buf
    }
    return t.Buf, nil
}

// ---- ValueStore 接口的实现 ----

// 假设 nodeStore 是 ValueStore 的一个实现
type nodeStore struct {
  chunkStore interface { // 假设 chunkStore 是另一个接口或类型
    WalkNodes(ctx context.Context, h hash.Hash, cb CallbackFunc) error
  }
  // ... 其他字段
}

// 注意这里的接收者类型是 nodeStore (值类型)
func (vs nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) {
    err = vs.chunkStore.WalkNodes(ctx, h, func(ctx context.Context, n Node) error {
        if n.IsLeaf() {
            // append 到局部变量 result
            result = append(result, n.GetValue(0)...)
        }
        return nil // 简化错误处理
    })
    return result, err
}

// 确保 nodeStore 实现了 ValueStore 接口
var _ ValueStore = nodeStore{} // 注意这里用的是值类型
</code></pre>
<p>代码逻辑看起来几乎没变，只是将原来load方法中的 WalkNodes 调用和 append 逻辑封装到了 nodeStore 的 ReadBytes 方法中。</p>
<p>然而，性能分析（Profiling）结果显示，在新的实现中，ReadBytes 方法耗费了大量时间（约 1/3 的运行时）在调用 runtime.newobject 上。Go老手都知道：runtime.newobject是Go用于<strong>在堆上分配内存</strong>的内建函数。这意味着，新的实现引入了额外的堆内存分配。</p>
<p>那么问题来了（这也是原文留给读者的思考题）：</p>
<ul>
<li><strong>额外的堆内存在哪里分配的？</strong></li>
<li><strong>为什么这次分配发生在堆（Heap）上，而不是通常更廉价的栈（Stack）上？</strong></li>
</ul>
<p>到这里可能即便经验丰富的Go开发者可能也没法一下子看出端倪。如果你和我一样在当时还没想到，不妨暂停一下，仔细看看重构后的代码，特别是ReadBytes方法的定义。</p>
<p>当你准备好后，我们来一起揭晓答案。</p>
<h2>破案：罪魁祸首——那个被忽略的&#42;号</h2>
<p>造成性能骤降的罪魁祸首，竟然只是ReadBytes方法定义中的一个字符差异！</p>
<p><strong>修复方法：</strong></p>
<pre><code>diff
- func (vs nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) {
+ func (vs *nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) {
</code></pre>
<p>是的，仅仅是将 ReadBytes 方法的接收者从<strong>值类型 nodeStore</strong> 改为<strong>指针类型 *nodeStore</strong>，就挽回了那丢失的 30% 性能。</p>
<p>那么，这背后到底发生了什么？我们逐层剥丝去茧的看一下。</p>
<h3>第一层：值接收者 vs 指针接收者 —— 不仅仅是语法糖</h3>
<p>我们需要理解Go语言中方法接收者的两种形式：</p>
<ul>
<li><strong>值接收者 (Value Receiver):</strong> func (v MyType) MethodName() {}</li>
<li><strong>指针接收者 (Pointer Receiver):</strong> func (p *MyType) MethodName() {}</li>
</ul>
<p>虽然Go允许你用值类型调用指针接收者的方法（Go会自动取地址），或者用指针类型调用值接收者的方法（Go会自动解引用），但这<strong>并非没有代价</strong>。</p>
<p>关键在于：<strong>当使用值接收者时，方法内部操作的是接收者值的一个副本（Copy）。</strong></p>
<p>在我们的案例中，ReadBytes 方法使用了值接收者 (vs nodeStore)。这意味着，每次通过 t.valueStore.ReadBytes(&#8230;) 调用这个方法时（t.valueStore 是一个接口，其底层具体类型是 nodeStore），Go 运行时会<strong>创建一个 nodeStore 结构体的副本</strong>，并将这个副本传递给 ReadBytes 方法内部的vs变量。</p>
<p><strong>正是这个结构体的复制操作，构成了“第一重罪”——它带来了额外的开销。</strong></p>
<p>但仅仅是复制，通常还不至于引起如此大的性能问题。毕竟，Go 语言函数参数传递也是值传递（pass-by-value），复制是很常见的。问题在于，这次复制产生的开销，并不仅仅是简单的内存拷贝。</p>
<h3>第二层：栈分配 vs 堆分配 —— 廉价与昂贵的抉择</h3>
<p>通常情况下，函数参数、局部变量，以及这种方法接收者的副本，会被分配在<strong>栈（Stack）</strong>上。栈分配非常快速，因为只需要移动栈指针即可，并且随着函数返回，栈上的内存会自动回收，几乎没有管理成本。</p>
<p>但是，在某些情况下，Go 编译器（通过<strong>逃逸分析 Escape Analysis</strong>）会判断一个变量不能安全地分配在栈上，因为它可能在函数返回后仍然被引用（即“逃逸”到函数作用域之外）。这时，编译器会选择将这个变量分配在<strong>堆（Heap）</strong>上。</p>
<p>堆分配相比栈分配要昂贵得多：</p>
<ol>
<li><strong>分配本身更慢：</strong> 需要在堆内存中找到合适的空间。</li>
<li><strong>需要垃圾回收（GC）：</strong> 堆上的内存需要垃圾回收器来管理和释放，这会带来额外的 CPU 开销和潜在的 STW (Stop-The-World) 暂停。</li>
</ol>
<p>在Dolt的这个案例中，性能分析工具明确告诉我们，ReadBytes 方法中出现了大量的 runtime.newobject 调用，这表明 nodeStore 的那个<strong>副本</strong>被分配到了<strong>堆</strong>上。</p>
<p><strong>这就是“第二重罪”——本该廉价的栈上复制，变成了昂贵的堆上分配。</strong></p>
<blockquote>
<p>注：这里有些读者可能注意到了WalkNodes传入了一个闭包，闭包是在堆上分配的，但这个无论方法接收者是指针还是值，其固定开销都是存在的。不是此次“血案”的真凶。</p>
</blockquote>
<h3>第三层：逃逸分析的“无奈”——为何会逃逸到堆？</h3>
<p>为什么编译器会认为 nodeStore 的副本需要分配在堆上呢？按照代码逻辑，vs 这个副本变量似乎并不会在 ReadBytes 函数返回后被引用。</p>
<p>原文作者使用go build -gcflags “-m” 工具（这个命令可以打印出编译器的逃逸分析和内联决策）发现，编译器给出的原因是：</p>
<pre><code>store/prolly/tree/node_store.go:93:7: parameter ns leaks to {heap} with derefs=1:
  ...
  from ns.chunkStore (dot of pointer) at ...
  from ns.chunkStore.WalkNodes(ctx, ref) (call parameter) at ...
leaking param content: ns
</code></pre>
<blockquote>
<p>注：这里原文也有“笔误”，代码定义用的接收者名是vs，这里逃逸分析显示的是ns。可能是后期方法接收者做了改名。</p>
</blockquote>
<p>编译器认为，当 vs.chunkStore.WalkNodes(&#8230;) 被调用时，由于 chunkStore 是一个接口类型，编译器<strong>无法在编译时完全确定</strong> WalkNodes 方法的具体实现是否会导致 vs （或者其内部字段的地址）以某种方式“逃逸”出去（比如被一个长期存活的 goroutine 捕获）。</p>
<p>Go 的逃逸分析虽然很智能，但并非万能。官方文档也提到它是一个“基本的逃逸分析”。当编译器<strong>不能百分之百确定</strong>一个变量不会逃逸时，为了保证内存安全（这是 Go 的最高优先级之一），它会采取<strong>保守策略</strong>，将其分配到堆上。堆分配永远是安全的（因为有 GC），尽管可能不是最高效的。</p>
<p>在这个案例中，接口方法调用成为了逃逸分析的“盲点”，导致编译器做出了保守的堆分配决策。</p>
<h2>眼见为实：一个简单的复现与逃逸分析</h2>
<p>理论讲完了，我们不妨动手实践一下，用一个极简的例子来复现并观察这个逃逸现象。</p>
<h3>第一步：使用值接收者 (Value Receiver)</h3>
<p>下面是模拟Dolt问题代码的示例，这里大幅做了简化。我们先用值接收者定义方法：</p>
<pre><code>package main

import "fmt"

// 1. 接口
type Executor interface {
    Execute()
}

// 2. 具体实现
type SimpleExecutor struct{}

func (se SimpleExecutor) Execute() {
    // fmt.Println("Executing...") // 实际操作可以省略
}

// 3. 包含接口字段的结构体
type Container struct {
    exec Executor
}

// 4. 值接收者方法 (我们期望这里的 c 逃逸)
func (c Container) Run() {
    fmt.Println("Running via value receiver...")
    // 调用接口方法，这是触发逃逸的关键
    c.exec.Execute()
}

func main() {
    impl := SimpleExecutor{}
    cInstance := Container{exec: impl}

    // 调用值接收者方法
    cInstance.Run()

    // 确保 cInstance 被使用，防止完全优化
    _ = cInstance.exec
}
</code></pre>
<p><strong>运行逃逸分析 (值接收者版本):</strong></p>
<p>我们在终端中运行 go build -gcflags=”-m -l” main.go。这里关闭了内联优化，避免对结果的影响。</p>
<p><strong>观察输出:</strong> 你应该会看到类似以下的行 (行号可能略有不同):</p>
<pre><code>$go run -gcflags="-m -l" main.go
# command-line-arguments
./main.go:24:7: leaking param: c
./main.go:25:13: ... argument does not escape
./main.go:25:14: "Running via value receiver..." escapes to heap
./main.go:36:31: impl escapes to heap
Running via value receiver...
</code></pre>
<p>我们发现：leaking param: c 这条输出明确地告诉我们，Run 方法的值接收者 c（一个 Container 的副本）因为内部调用了接口方法而逃逸到了堆上。</p>
<h3>第二步：改为指针接收者 (Pointer Receiver)</h3>
<p>现在，我们将 Run 方法改为使用指针接收者，其他代码不变：</p>
<pre><code>func (c *Container) Run() {
    fmt.Println("Running via pointer receiver...")
    c.exec.Execute()
}
</code></pre>
<p><strong>再来运行逃逸分析 (指针接收者版本):</strong></p>
<pre><code>$go run -gcflags="-m -l" main.go
# command-line-arguments
./main.go:24:7: leaking param content: c
./main.go:26:13: ... argument does not escape
./main.go:26:14: "Running via pointer receiver..." escapes to heap
./main.go:36:31: impl escapes to heap
Running via pointer receiver...
</code></pre>
<p>对于之前的输出，两者的<strong>主要区别</strong>在于对接收者参数c的逃逸报告不同：</p>
<ul>
<li>值接收者: leaking param: c -> 接收者c的副本本身因为接口方法调用而逃逸到了堆上。</li>
<li>指针接收者: leaking param content: c -> 接收者指针c本身并未因为接口方法调用而逃逸，但它指向或访问的内容与堆内存有关，在此例中， main函数中将具体实现赋值给接口字段时，impl会逃逸到堆(impl escapes to heap)，无论接收者类型为值还是指针。</li>
</ul>
<p>这个对比清晰地表明，使用指针接收者可以避免接收者参数本身因为在方法内部调用接口字段的方法而逃逸到堆。这通常是更优的选择，可以减少不必要的堆分配。</p>
<p>这个简单的重现实验清晰地印证了我们的分析：</p>
<ul>
<li>当<strong>值接收者</strong>的方法内部调用了其包含的<strong>接口字段</strong>的方法时，编译器出于保守策略，可能会将<strong>值接收者的副本分配到堆上</strong>，导致额外的性能开销。</li>
<li>而使用<strong>指针接收者</strong>时，方法传递的是指针，编译器通过指针进行接口方法的动态分发，这个过程<strong>通常不会导致接收者指针本身逃逸到堆上</strong>。</li>
</ul>
<h2>小结：细节里的魔鬼与性能优化的启示</h2>
<p>这个由一个*号引发的30%性能“血案”，给我们带来了几个深刻的启示：</p>
<ol>
<li><strong>值接收者有隐形成本：</strong> 每次调用都会产生接收者值的副本。虽然 Go 会自动处理值/指针的转换，但这背后是有开销的，尤其是在拷贝较大的结构体时。</li>
<li><strong>拷贝可能导致堆分配：</strong> 如果编译器无法通过逃逸分析确定副本只在栈上活动（尤其是在涉及接口方法调用等复杂情况时），它就会被分配到堆上，带来显著的性能损耗（分配开销 + GC 压力）。</li>
<li><strong>接口调用可能影响逃逸分析：</strong> 动态派发使得编译器难以在编译时完全分析清楚变量的生命周期，可能导致保守的堆分配决策。</li>
<li><strong>优先使用指针接收者：</strong> 尤其对于体积较大的结构体，或者在性能敏感的代码路径中，<strong>使用指针接收者可以避免不必要的拷贝和潜在的堆分配</strong>，是更安全、通常也更高效的选择。当然，如果你的类型是“不可变”的，或者逻辑上确实需要操作副本，值接收者也有其用武之地，但要意识到潜在的性能影响。</li>
<li><strong>善用工具：</strong> go build -gcflags “-m” 是我们理解编译器内存分配决策、发现潜在性能问题的有力武器。当遇到意外的性能问题时，检查逃逸分析的结果往往能提供关键线索。</li>
</ol>
<p>一个小小的星号，背后却牵扯出 Go 语言关于方法接收者、内存分配和编译器优化的诸多细节。理解这些细节，正是我们写出更高性能、更优雅 Go 代码的关键。</p>
<p>希望这个真实的案例和简单的复现能让你对 Go 的内存管理有更深的认识。<strong>你是否也曾遇到过类似的、由微小代码改动引发的性能问题？欢迎在评论区分享你的故事和看法！</strong></p>
<blockquote>
<p>Dolt原文链接：https://www.dolthub.com/blog/2025-04-18-optimizing-heap-allocations/</p>
</blockquote>
<hr />
<p>今天我们深入探讨了值接收者、堆分配和逃逸分析这些相对底层的 Go 语言知识点。如果你对这些内容意犹未尽，希望：</p>
<ul>
<li>系统性地学习 Go 语言，从基础原理到并发编程，再到工程实践，构建扎实的知识体系；</li>
<li>深入理解 Go 的设计哲学与底层实现，知其然更知其所以然；</li>
<li>掌握更多 Go 语言的进阶技巧与避坑经验，在实践中写出更健壮、更高效的代码；</li>
</ul>
<p>那么，我为你准备了两份“精进食粮”：</p>
<ul>
<li>极客时间专栏《Go 语言第一课》：这门课程覆盖了 Go 语言从入门到进阶所需的核心知识，包含大量底层原理讲解和实践案例，是系统学习 Go 的绝佳起点。</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /></p>
<ul>
<li>我的书籍《Go 语言精进之路》：这本书侧重于连接 Go 语言理论与一线工程实践，深入探讨了 Go 的设计哲学、关键特性、常见陷阱以及在真实项目中应用 Go 的最佳实践，助你打通进阶之路上的“任督二脉”。</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p>希望它们能成为你 Go 语言学习和精进道路上的得力助手！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>“Go is badly designed”？它像极了我们当年恨过的物理老师！</title>
		<link>https://tonybai.com/2025/04/17/go-is-badly-designed/</link>
		<comments>https://tonybai.com/2025/04/17/go-is-badly-designed/#comments</comments>
		<pubDate>Thu, 17 Apr 2025 13:30:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[catch]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[range]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[simplicity]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[try]]></category>
		<category><![CDATA[try-catch]]></category>
		<category><![CDATA[哲学]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[开发效率]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[空指针]]></category>
		<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=4583</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/17/go-is-badly-designed 大家好，我是Tony Bai。 今天刷X (前Twitter) 的时候，看到Golang Insiders社区下面这条推文，真是差点扑哧一声笑出来，感觉说得太形象了，必须分享给大家： 这位叫Lyes的开发者回应 “Go is badly designed” (Go 语言设计得很糟糕) 的说法，他打了个比方： 这让我想起了我的高中物理老师，我们当时都恨他，因为他从不‘放水’简化物理知识。课难、考试难，大部分人在他手下分数都不高，所以他自然成了‘坏老师’。 Go 语言就有点像他。它从不‘放水’，直面问题。你可以很快用它变得高效，写出远比用 Python 或 JavaScript 写得更好的软件。 但你也得知道，这门语言不会‘溺爱’你。当你的服务器因为一个 nil map 或其他新手常犯的错误而 panic 时，别生气。 不像 Rust，Go 的编译器不会在你编程生涯的每一刻都‘牵着你的手’。它会给你足够的方向让你知道该往哪走，满足你 80% 的需求，同时仍然保持你的生产力。 怎么样？看完这段话，是不是像极了我们初学Go时，被nil pointer dereference 或 index out of range 当头棒喝的瞬间？ 像极了我们当年一边抱怨物理老师太严格、考试太变态，一边又不得不硬着头皮去啃那些公式和定理的样子？ Lyes 的这个比喻，可以说精准地戳中了 Go 语言的一些核心特质，也解释了为什么关于“Go是否设计糟糕”的争论从未停止。咱们今天就借着这个“物理老师”的比喻，好好聊聊Go的“坏脾气”和它背后的设计哲学。 那个从不“放水”的“严格老师” Lyes 说 Go 不会 “dumb [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-badly-designed-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/17/go-is-badly-designed">本文永久链接</a> &#8211; https://tonybai.com/2025/04/17/go-is-badly-designed</p>
<p>大家好，我是Tony Bai。</p>
<p>今天刷X (前Twitter) 的时候，看到<a href="https://x.com/i/communities/1685641800449462272">Golang Insiders社区</a>下面这条推文，真是差点扑哧一声笑出来，感觉说得太形象了，必须分享给大家：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-badly-designed-2.png" alt="" /></p>
<p>这位叫Lyes的开发者回应 “Go is badly designed” (Go 语言设计得很糟糕) 的说法，他打了个比方：</p>
<blockquote>
<p>这让我想起了我的高中物理老师，我们当时都恨他，因为他从不‘放水’简化物理知识。课难、考试难，大部分人在他手下分数都不高，所以他自然成了‘坏老师’。</p>
<p>Go 语言就有点像他。它从不‘放水’，直面问题。你可以很快用它变得高效，写出远比用 Python 或 JavaScript 写得更好的软件。</p>
<p>但你也得知道，这门语言不会‘溺爱’你。当你的服务器因为一个 nil map 或其他新手常犯的错误而 panic 时，别生气。</p>
<p>不像 Rust，Go 的编译器不会在你编程生涯的每一刻都‘牵着你的手’。它会给你足够的方向让你知道该往哪走，满足你 80% 的需求，同时仍然保持你的生产力。</p>
</blockquote>
<p>怎么样？看完这段话，<strong>是不是像极了我们初学Go时，被nil pointer dereference 或 index out of range 当头棒喝的瞬间？</strong> 像极了我们当年一边抱怨物理老师太严格、考试太变态，一边又不得不硬着头皮去啃那些公式和定理的样子？</p>
<p>Lyes 的这个比喻，可以说精准地戳中了 Go 语言的一些核心特质，也解释了为什么关于“Go是否设计糟糕”的争论从未停止。咱们今天就借着这个“物理老师”的比喻，好好聊聊Go的“坏脾气”和它背后的设计哲学。</p>
<h2>那个从不“放水”的“严格老师”</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-badly-designed-3.png" alt="" /></p>
<p>Lyes 说 Go 不会 “dumb down anything(简化任何事物，去除复杂性)”，这太对了。Go语言的设计哲学里，<strong>“简洁”（Simplicity）</strong> 是核心原则之一，但这不代表“简单化”到隐藏问题的程度。相反，它选择<strong>直面问题</strong>：</p>
<ul>
<li>
<p><strong>显式的错误处理 (if err != nil)</strong>：不像某些语言用try-catch将错误“藏”起来，Go强迫你几乎在每次可能出错的操作后都检查错误。这很“烦”，但它逼着你思考每一步潜在的风险，就像物理老师逼着你弄懂每个公式的推导过程。</p>
</li>
<li>
<p><strong>直白的运行时Panic</strong>：当你对一个 nil 的 map 或 slice 进行操作时，Go 不会帮你“优雅地”处理，而是直接给你一个运行时 panic，程序崩溃。这很“粗暴”，但它用最直接的方式告诉你：“同学，你这里犯了个基础错误，赶紧改！” 这不就是物理老师发现你基本概念没搞懂时，直接点名批评，让你印象深刻吗？</p>
</li>
<li>
<p><strong>没有“溺爱”的语法糖</strong>：相比一些现代语言，Go 的语法糖相对较少。它没有泛滥的操作符重载，没有复杂的<a href="https://tonybai.com/2021/12/02/go-has-implicit-type-convertion">隐式转换</a>。很多事情需要你明确地写出来。这让代码有时候显得“啰嗦”，但大大降低了阅读和理解他人代码时的歧义，保证了大规模团队协作的效率。就像物理老师坚持用标准的符号和单位，不允许自创“简写”，是为了保证科学的严谨性。</p>
</li>
</ul>
<h2>“坏老师”真的“坏”吗？—— 严格背后的价值</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-badly-designed-4.png" alt="" /></p>
<p>我们当年可能都偷偷抱怨过物理老师不近人情，但多年后回想，是不是反而感谢他的严格，才让我们打下了坚实的基础？Go 语言的“严格”同样如此：</p>
<ol>
<li>
<p><strong>逼你养成好习惯</strong>：被 nil panic 搞崩溃几次后，你自然就学会了在使用 map/slice/pointer 前做检查，学会了初始化，学会了更严谨地思考边界条件。这种被“教训”出来的习惯，最终会融入你的编程血液，让你写出更健壮、更可靠的代码。这比那些“温柔”地帮你掩盖了问题，直到生产环境才爆发出更大危机的语言，是不是长期来看更负责任？</p>
</li>
<li>
<p><strong>简单直白，易于掌握核心</strong>：虽然会“当头棒喝”，但Go的核心概念相对较少，语法简洁。一旦你掌握了它的规则（比如错误处理模式、接口哲学、goroutine的使用），就能快速上手，并且写出的代码风格差异不会太大，易于团队维护。它不像某些语言，特性繁多，学习曲线陡峭，精通需要漫长时间。Go就像物理老师划定的核心考点，虽然难，但范围明确，努力就有回报。</p>
</li>
<li>
<p><strong>效率与务实：给你“80%的指引”</strong>：Lyes 提到了<a href="https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together">Go与Rust的对比</a>，说Go不会“全程牵手”。这正是<strong>Go的务实之处</strong>。它在编译速度、开发效率和运行时安全之间做了一个取舍。它通过快速编译、垃圾回收、简洁的并发模型，让你能高效地构建系统，满足大部分（比如 80%）场景的需求。它相信开发者是成年人，应该为自己的代码负责，而不是让编译器承担所有检查的重任。这就像物理老师教会你核心原理和解题方法，但不会一步步带着你做完所有练习题，他相信你能举一反三，独立解决问题。</p>
</li>
</ol>
<h2>不是“设计糟糕”，而是哲学不同</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-is-badly-designed-5.png" alt="" /></p>
<p>所以，“Go is badly designed” 吗？</p>
<p>与其说是“糟糕”，不如说是<strong>设计哲学和目标受众的不同</strong>。</p>
<ul>
<li>如果你期望一门语言能像 Rust 那样，在编译期就为你消除几乎所有内存安全和并发风险，愿意为此付出更陡峭的学习曲线和更长的编译时间，那么 Go 可能确实“不够好”。</li>
<li>但如果你追求的是<strong>快速构建、高效部署、简单可靠、易于维护</strong>的大型后端系统，能接受在运行时处理一些本可避免的错误（并通过良好的实践和工具来减少它们），那么Go的设计哲学可能恰恰是它的<strong>优点</strong>。</li>
</ul>
<p>Go 就像那位严格的物理老师，他可能不会让你在学习过程中时刻感到“舒适”，甚至会让你经历挫败和“阵痛”。但他目标明确，方法直接，逼着你打好基础，养成严谨的习惯，最终让你能够独立、高效地解决实际问题。</p>
<p><strong>那么，你怎么看？</strong></p>
<ul>
<li>你觉得Go语言像不像你当年“恨过”的某位老师？</li>
<li>你第一次遇到 nil panic 时是什么感受？是觉得Go设计糟糕，还是反思自己代码的问题？</li>
<li>你更喜欢 Go 这种“给你方向，但不全程牵手”的方式，还是 Rust 那种“无微不至的保护”？</li>
</ul>
<p><strong>欢迎在评论区留下你的看法，分享你和 Go “相爱相杀”的故事！</strong></p>
<hr />
<p><strong>原「Gopher部落」已重装升级为「Go &amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！</strong></p>
<p>我们致力于打造一个高品质的 <strong>Go 语言深度学习</strong> 与 <strong>AI 应用探索</strong> 平台。在这里，你将获得：</p>
<ul>
<li><strong>体系化 Go 核心进阶内容:</strong> 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。</li>
<li><strong>前沿 Go+AI 实战赋能:</strong> 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 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>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/17/go-is-badly-designed/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go导出标识符：那些鲜为人知的细节</title>
		<link>https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/</link>
		<comments>https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/#comments</comments>
		<pubDate>Wed, 22 Jan 2025 19:24:06 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[embeded]]></category>
		<category><![CDATA[field]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goplus]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[structtag]]></category>
		<category><![CDATA[typeparameter]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[七牛云]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[变量]]></category>
		<category><![CDATA[字段]]></category>
		<category><![CDATA[字符]]></category>
		<category><![CDATA[导出方法]]></category>
		<category><![CDATA[导出标识符]]></category>
		<category><![CDATA[嵌入类型]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[接口]]></category>
		<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=4471</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers 前不久，在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈七牛云Go SDK中的某些类型没有导出，导致外部包无法使用的问题(如下图)： 七牛开发人员迅速对该问题做出了“更正”，将问题反馈中涉及的类型saveasArgs和saveasReply改为了导出类型，即首字母大写： 不过，这看似寻常的问题反馈与修正却引发了我的一些思考。 我们大胆臆想一下：如果saveasReply类型的开发者是故意将saveasReply类型设置为非导出的呢？看一下“更正”之前的saveasReply代码： type saveasReply struct { Fname string `json:"fname"` PersistenId string `json:"persistentId,omitempty"` Bucket string `json:"bucket"` Duration int `json:"duration"` // ms } 有读者可能会问：那为什么还将saveasReply结构体的字段设置为导出字段呢？请注意每个字段后面的结构体标签(struct tag)。这显然是为了进行JSON 编解码，因为目前Go的encoding/json包仅会对导出字段进行编解码处理。 除了这个原因，原开发者可能还希望包的使用者能够访问这些导出字段，而又不想完全暴露该类型。我在此不对这种设计的合理性进行评价，而是想探讨这种做法是否可行。 我们对Go导出标识符的传统理解是：导出标识符（以大写字母开头的标识符）可以在包外被访问和使用，而非导出标识符（以小写字母开头的标识符）只能在定义它们的包内访问。这种机制帮助开发者控制类型和函数的可见性，确保内部实现细节不会被随意访问，从而增强封装性。 但实际上，Go的导出标识符机制是否允许在某些情况下，即使类型本身是非导出的，其导出字段依然可以被包外的代码访问呢？该类型的导出方法呢？这些关于Go导出标识符的细节可能是鲜少人探讨的，在这篇博文中，我们将系统地了解这些机制，希望能为各位小伙伴带来更深入的理解。 1. Go对导出标识符的定义 我们先回顾一下Go语言规范(go spec)对导出标识符的定义： 我们通常使用英文字母来命名标识符，因此可以将上述定义中的第一句理解为：以大写英文字母开头的标识符即为导出标识符。 注：Unicode字符类别Lu（Uppercase Letter）包含所有的大写字母。这一类别不仅包括英文大写字母，还涵盖多种语言的大写字符，例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而，我非常不建议大家使用非英文大写字母来表示导出标识符，因为这可能会挑战大家的认知习惯。 而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的，但并没有明确要求其关联的类型本身也必须是导出的。 这为我们提供了进一步探索Go导出标识符细节的机会。接下来，我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。 2. 在包外访问非导出类型的导出字段 我们首先定义一个带有导出字段的非导出类型myStruct，并将它放在mypackage里： // go-exported-identifiers/field/mypackage/mypackage.go package mypackage type myStruct struct { Field string [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers">本文永久链接</a> &#8211; https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers</p>
<p>前不久，在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈<a href="https://github.com/qiniu/go-sdk/blob/bb391c9d9ea2c115494df5c38d058cb3b673a29f/qvs/record.go#L41">七牛云Go SDK中的某些类型没有导出，导致外部包无法使用的问题(如下图)</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-2.png" alt="" /></p>
<p>七牛开发人员迅速对该问题做出了“更正”，将问题反馈中涉及的类型saveasArgs和saveasReply改为了导出类型，即首字母大写：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-4.png" alt="" /></p>
<p>不过，这看似寻常的问题反馈与修正却引发了我的一些思考。</p>
<p>我们大胆臆想一下：如果saveasReply类型的开发者是故意将saveasReply类型设置为非导出的呢？看一下“更正”之前的saveasReply代码：</p>
<pre><code>type saveasReply struct {
    Fname       string `json:"fname"`
    PersistenId string `json:"persistentId,omitempty"`
    Bucket      string `json:"bucket"`
    Duration    int    `json:"duration"` // ms
}
</code></pre>
<p>有读者可能会问：那为什么还将saveasReply结构体的字段设置为导出字段呢？请注意每个字段后面的结构体标签(struct tag)。这显然是为了进行JSON 编解码，因为目前Go的encoding/json包仅会对导出字段进行编解码处理。</p>
<p>除了这个原因，原开发者可能还希望包的使用者能够访问这些导出字段，而又不想完全暴露该类型。我在此不对这种设计的合理性进行评价，而是想探讨这种做法是否可行。</p>
<p>我们对Go导出标识符的传统理解是：导出标识符（以大写字母开头的标识符）可以在包外被访问和使用，而非导出标识符（以小写字母开头的标识符）只能在定义它们的包内访问。这种机制帮助开发者控制类型和函数的可见性，确保内部实现细节不会被随意访问，从而增强封装性。</p>
<p>但实际上，Go的导出标识符机制是否允许在某些情况下，即使类型本身是非导出的，其导出字段依然可以被包外的代码访问呢？该类型的导出方法呢？这些关于Go导出标识符的细节可能是鲜少人探讨的，在这篇博文中，我们将系统地了解这些机制，希望能为各位小伙伴带来更深入的理解。</p>
<h2>1. Go对导出标识符的定义</h2>
<p>我们先回顾一下<a href="https://go.dev/ref/spec#Exported_identifiers">Go语言规范(go spec)对导出标识符的定义</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-3.png" alt="" /></p>
<p>我们通常使用英文字母来命名标识符，因此可以将上述定义中的第一句理解为：以大写英文字母开头的标识符即为导出标识符。</p>
<blockquote>
<p>注：Unicode字符类别Lu（Uppercase Letter）包含所有的大写字母。这一类别不仅包括英文大写字母，还涵盖多种语言的大写字符，例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而，我非常<strong>不建议大家使用非英文大写字母来表示导出标识符</strong>，因为这可能会挑战大家的认知习惯。</p>
</blockquote>
<p>而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的，但<strong>并没有明确要求其关联的类型本身也必须是导出的</strong>。</p>
<p>这为我们提供了进一步探索Go导出标识符细节的机会。接下来，我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。</p>
<h2>2. 在包外访问非导出类型的导出字段</h2>
<p>我们首先定义一个带有导出字段的非导出类型myStruct，并将它放在mypackage里：</p>
<pre><code>// go-exported-identifiers/field/mypackage/mypackage.go

package mypackage

type myStruct struct {
    Field string // 导出的字段
}

// NewMyStruct1是一个导出的函数，返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
    return &amp;myStruct{Field: value}
}

// NewMyStruct1是一个导出的函数，返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
    return myStruct{Field: value}
}
</code></pre>
<p>然后我们在包外尝试访问myStruct类型的导出字段：</p>
<pre><code>// go-exported-identifiers/field/main.go

package main

import (
    "demo/mypackage"
    "fmt"
)

func main() {
    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")

    // 尝试访问Field字段
    fmt.Println(ms1.Field) // Hello1

    // 通过导出的函数获取myStruct类型变量
    ms2 := mypackage.NewMyStruct1("Hello2")

    // 尝试访问Field字段
    fmt.Println(ms2.Field) // Hello2
}
</code></pre>
<p>在go-exported-identifiers/field目录下编译运行该示例：</p>
<pre><code>$go run main.go
Hello1
Hello2
</code></pre>
<p>我们看到，无论是通过myStruct的指针还是实例副本，都可以成功访问其导出变量Field。这个示例的关键就是：我们<strong>使用了短变量声明</strong>直接通过调用myStruct的两个“构造函数(NewXXX)”得到了其指针(ms1)以及实例副本(ms2)。在这个过程中，我们没有在main包中显式使用mypackage.myStruct这个非导出类型。</p>
<p>采用类似的方案，我们接下来再看看是否可以在包外访问非导出类型的导出方法。</p>
<h2>3. 在包外访问非导出类型的导出方法</h2>
<p>我们为非导出类型添加两个导出方法M1和M2：</p>
<pre><code>// go-exported-identifiers/method/mypackage/mypackage.go

package mypackage

import "fmt"

type myStruct struct {
    Field string // 导出的字段
}

// NewMyStruct1是一个导出的函数，返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
    return &amp;myStruct{Field: value}
}

// NewMyStruct1是一个导出的函数，返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
    return myStruct{Field: value}
}

func (m *myStruct) M1() {
    fmt.Println("invoke *myStruct's M1")
}

func (m myStruct) M2() {
    fmt.Println("invoke myStruct's M2")
}
</code></pre>
<p>然后，试着在外部包中调用M1和M2方法：</p>
<pre><code>// go-exported-identifiers/method/main.go

package main

import (
    "demo/mypackage"
)

func main() {
    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")
    ms1.M1()
    ms1.M2()

    // 通过导出的函数获取myStruct类型变量
    ms2 := mypackage.NewMyStruct2("Hello2")
    ms2.M1()
    ms2.M2()
}
</code></pre>
<p>在go-exported-identifiers/method目录下编译运行这个示例：</p>
<pre><code>$go run main.go
invoke *myStruct's M1
invoke myStruct's M2
invoke *myStruct's M1
invoke myStruct's M2
</code></pre>
<p>我们看到，无论是通过非导出类型的指针，还是通过非导出类型的变量复本都可以成功调用非导出类型的导出方法。</p>
<p>提及方法，我们会顺带想到接口，非导出类型是否可以实现某个外部包定义的接口呢？我们继续往下看。</p>
<h2>4. 非导出类型实现某个外部包的接口</h2>
<p>在Go中，如果某个类型T实现了某个接口类型I的方法集合中的所有方法，我们就说T实现了I，T的实例可以赋值给I类型的接口变量。</p>
<p>在下面示例中，我们看看非导出类型是否可以实现某个外部包的接口。</p>
<p>在这个示例中mypackage包中的内容与上面示例一致，主要改动的是main.go，我们来看一下：</p>
<pre><code>// go-exported-identifiers/interface/main.go

package main

import (
    "demo/mypackage"
)

// 定义一个导出的接口
type MyInterface interface {
    M1()
    M2()
}

func main() {
    var mi MyInterface

    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")
    mi = ms1
    mi.M1()
    mi.M2()

    // 通过导出的函数获取myStruct类型变量
    // ms2 := mypackage.NewMyStruct2("Hello2")
    // mi = ms2 // compile error: mypackage.myStruct does not implement MyInterface
    // ms2.M1()
    // ms2.M2()
}
</code></pre>
<p>在这个main.go中，我们定义了一个接口MyInterface，它的方法集合中有两个方法M1和M2。根据类型方法集合的判定规则，&#42;myStruct类型实现了MyInterface的所有方法，而myStruct类型则不满足，没有实现M1方法，我们在go-exported-identifiers/interface目录下编译运行这个示例，看看是否与我们预期的一致：</p>
<pre><code>$go run main.go
invoke *myStruct's M1
invoke myStruct's M2
</code></pre>
<p>如果我们去掉上面代码中对ms2的注释，那么将得到Compiler error: mypackage.myStruct does not implement MyInterface。</p>
<blockquote>
<p>注：关于一个类型的方法集合的判定规则，可以参考我的极客时间<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏的<a href="https://time.geekbang.org/column/article/466221">第25讲</a>。</p>
</blockquote>
<p>接下来，我们再来考虑一个场景，即非导出类型用作嵌入字段的情况，我们要看看该非导出类型的导出方法和导出字段是否会promote到外部类型中。</p>
<h2>5. 非导出类型用作嵌入字段</h2>
<p>我们改造一下示例，新版的带有嵌入字段的结构见下面mypackage包的代码：</p>
<pre><code>// go-exported-identifiers/embedded_field/mypackage/mypackage.go

package mypackage

import "fmt"

type nonExported struct {
    Field string // 导出的字段
}

// Exported 是导出的结构体，嵌入了nonExported
type Exported struct {
    nonExported // 嵌入非导出结构体
}

func NewExported(value string) *Exported {
    return &amp;Exported{
        nonExported: nonExported{
            Field: value,
        },
    }
}

// M1是导出的函数
func (n *nonExported) M1() {
    fmt.Println("invoke nonExported's M1")
}

// M2是导出的函数
func (e *Exported) M2() {
    fmt.Println("invoke Exported's M2")
}
</code></pre>
<p>这里新增一个导出类型Exported，它嵌入了一个非导出类型nonExported，后者拥有导出字段Field，以及两个导出方法M1。我们也Exported类型定义了一个方法M2。</p>
<p>下面我们再来看看main.go中是如何使用Exported的：</p>
<pre><code>// go-exported-identifiers/embedded_field/main.go

package main

import (
    "demo/mypackage"
    "fmt"
)

// 定义一个导出的接口
type MyInterface interface {
    M1()
    M2()
}

func main() {
    ms := mypackage.NewExported("Hello")
    fmt.Println(ms.Field) // 访问嵌入的非导出结构体的导出字段

    ms.M1() // 访问嵌入的非导出结构体的导出方法

    var mi MyInterface = ms
    mi.M1()
    mi.M2()
}
</code></pre>
<p>在go-exported-identifiers/embedded_field目录下编译运行这个示例：</p>
<pre><code>$go run main.go
Hello
invoke nonExported's M1
invoke nonExported's M1
invoke Exported's M2
</code></pre>
<p>我们看到，作为嵌入字段的非导出类型的导出字段与方法会被自动promote到外部类型中，通过外部类型的变量可以直接访问这些字段以及调用这些导出方法。这些方法还可以作为外部类型方法集中的一员，来作为满足特定接口类型(如上面代码中的MyInterface)的条件。</p>
<p>Go 1.18增加了泛型支持，那么非导出类型是否可以用作泛型函数和泛型类型的类型实参呢？最后我们来看看这个细节。</p>
<h2>6. 非导出类型用作泛型函数和泛型类型的类型实参</h2>
<p>和前面一样，我们先定义用于该示例的带有导出字段和导出方法的非导出类型：</p>
<pre><code>// go-exported-identifiers/generics/mypackage/mypackage.go

package mypackage

import "fmt"

// 定义一个非导出的结构体
type nonExported struct {
    Field string
}

// 导出的方法
func (n *nonExported) M1() {
    fmt.Println("invoke nonExported's M1")
}

func (n *nonExported) M2() {
    fmt.Println("invoke nonExported's M2")
}

// 导出的函数，用于创建非导出类型的实例
func NewNonExported(value string) *nonExported {
    return &amp;nonExported{Field: value}
}
</code></pre>
<p>现在我们将其用于泛型函数，下面定义了泛型函数UseNonExportedAsTypeArgument，它的类型参数使用MyInterface作为约束，而上面的nonExported显然满足该约束，我们通过构造函数NewNonExported获得非导出类型的实例，然后将其传递给UseNonExportedAsTypeArgument，Go会通过泛型的类型参数自动推导机制推断出类型实参的类型：</p>
<pre><code>// go-exported-identifiers/generics/main.go

package main

import (
    "demo/mypackage"
)

// 定义一个用作约束的接口
type MyInterface interface {
    M1()
    M2()
}

func UseNonExportedAsTypeArgument[T MyInterface](item T) {
    item.M1()
    item.M2()
}

// 定义一个带有泛型参数的新类型
type GenericType[T MyInterface] struct {
    Item T
}

func NewGenericType[T MyInterface](item T) GenericType[T] {
    return GenericType[T]{Item: item}
}

func main() {
    // 创建非导出类型的实例
    n := mypackage.NewNonExported("Hello")

    // 调用泛型函数，传入实现了MyInterface的非导出类型
    UseNonExportedAsTypeArgument(n) // ok

    // g := GenericType{Item: n} // compiler error: cannot use generic type GenericType[T MyInterface] without instantiation
    g := NewGenericType(n)
    g.Item.M1()
}
</code></pre>
<p>但由于目前Go泛型还不支持对泛型类型的类型参数的自动推导，所以直接通过g := GenericType{Item: n}来初始化一个泛型类型变量将导致编译错误！我们需要借助泛型函数的推导机制将非导出类型与泛型类型进行结合，参见上述示例中的NewGenericType函数，通过泛型函数支持的类型参数的自动推导间接获得GenericType的类型实参。在go-exported-identifiers/generics目录下编译运行这个示例，便可得到我们预期的结果：</p>
<pre><code>$go run main.go
invoke nonExported's M1
invoke nonExported's M2
invoke nonExported's M1
</code></pre>
<h2>7. 非导出类型使用导出字段以及导出方法的用途</h2>
<p>前面的诸多示例证明了：即使类型本身是非导出的，但其内部的导出字段以及它的导出方法依然可以在外部包中使用，并且在实现接口、嵌入字段、泛型等使用场景下均有效。</p>
<p>到这里，你可能会提出这样一个问题：<strong>会有Go开发者使用非导出类型结合导出字段或方法的设计吗</strong>？</p>
<p>其实这种还是很常见的，在Go标准库中就有不少，只不过它们更多是包内使用，类似于非导出类型xxxImpl和它的Wrapper类型XXX的关系，或是xxxImpl或嵌入到XXX中，就像这样：</p>
<pre><code>// 包内实现
type xxxImpl struct {  // 非导出的实现类型
    // 内部字段
}

// 导出的包装类型
type XXX struct {
    impl *xxxImpl  // 包含实现类型
    // 其他字段
}

// 或者通过嵌入方式
type XXX struct {
    *xxxImpl  // 嵌入实现类型
    // 其他字段
}
</code></pre>
<p>但也有一些可以包外使用的，比如实现了某个接口，并通过接口值返回，提供给外部使用，例如下面的valueCtx，它实现了Context接口，并通过WithValue返回，供调用WithValue的外部包使用：</p>
<pre><code>//$GOROOT/src/context/context.go

func WithValue(parent Context, key, val any) Context {  // 构造函数，实现接口
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &amp;valueCtx{parent, key, val}
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val any
}

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}
</code></pre>
<p>这么做的目的是什么呢？大约有如下几点：</p>
<ul>
<li>隐藏实现细节</li>
</ul>
<p>非导出类型的主要作用是防止外部直接使用和依赖其内部实现细节。通过限制类型的直接使用，库作者可以保持实现的灵活性，随时调整或重构类型的内部逻辑，而无需担心破坏外部调用代码； 还可以避免暴露多余的API，使库的接口更加简洁。</p>
<ul>
<li>控制实例的创建和管理</li>
</ul>
<p>通过非导出类型，开发者还可以确保外部代码无法直接实例化类型，而必须通过导出的构造函数或工厂函数，就像前面举的示例那样。这种模式可以保证对象始终以特定的方式初始化，避免错误使用。同时，它还可以用来实现更复杂的初始化逻辑，如依赖注入或资源管理。</p>
<ul>
<li>在接口实现中的作用</li>
</ul>
<p>非导出类型可以用来实现导出的接口，从而将接口的实现细节完全隐藏。对于用户来说，只需要关心接口的定义，而无需关注其实现。</p>
<h2>8. 小结</h2>
<p>本文探讨了Go语言中的导出标识符及其相关细节，特别是非导出类型如何与其导出字段和导出方法结合使用。</p>
<p>尽管某些类型是非导出的，其内部的导出字段和方法依然可以在包外访问。此外，非导出类型在实现接口、嵌入字段和泛型中也展现出良好的应用。这种设计不仅促进了封装和接口实现的灵活性，还允许开发者通过构造函数返回非导出类型的实例，从而有效控制实例的创建与管理。这种方式帮助隐藏实现细节，简化外部接口，使得代码结构更加清晰。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-exported-identifiers">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾<br />
。让我相聚在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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.24新特性前瞻：工具链和标准库</title>
		<link>https://tonybai.com/2024/12/17/go-1-24-foresight-part2/</link>
		<comments>https://tonybai.com/2024/12/17/go-1-24-foresight-part2/#comments</comments>
		<pubDate>Mon, 16 Dec 2024 21:58:05 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AddCleanup]]></category>
		<category><![CDATA[aliastypeparams]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[clear]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[filepath]]></category>
		<category><![CDATA[FIPS]]></category>
		<category><![CDATA[futex]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-playground]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[GOAUTH]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[GOCACHEPROG]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golangci-lint]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[gorun]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[gotip]]></category>
		<category><![CDATA[GoWiki]]></category>
		<category><![CDATA[HashTrieMap]]></category>
		<category><![CDATA[hkdf]]></category>
		<category><![CDATA[Iterator]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[lock]]></category>
		<category><![CDATA[loop]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[max]]></category>
		<category><![CDATA[min]]></category>
		<category><![CDATA[mlkem]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[NIST]]></category>
		<category><![CDATA[nocallback]]></category>
		<category><![CDATA[noescape]]></category>
		<category><![CDATA[omitzero]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[PBKDF2]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[pseudo-version]]></category>
		<category><![CDATA[RAII]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Scaling]]></category>
		<category><![CDATA[SetFinalizer]]></category>
		<category><![CDATA[sha3]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[spinbit]]></category>
		<category><![CDATA[spinning]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[stringer]]></category>
		<category><![CDATA[swiss-table]]></category>
		<category><![CDATA[synctest]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[Thread]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[tool]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[typealias]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[wasmexport]]></category>
		<category><![CDATA[weak]]></category>
		<category><![CDATA[伪版本号]]></category>
		<category><![CDATA[原子操作]]></category>
		<category><![CDATA[后量子密码]]></category>
		<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=4442</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/17/go-1-24-foresight-part2 在上一篇文章中，我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文，重点介绍Go 1.24在工具链和标准库方面的重要更新，供大家参考。 1. 工具链 1.1 go.mod新增tool指示符，支持对tool的依赖管理(#48429) 我们日常编写Go项目代码时常常会依赖一些使用Go编写的工具，比如golang.org/x/tools/cmd/stringer或github.com/kyleconroy/sqlc。我们希望所有项目合作者都使用相同版本的工具，以避免在不同时间、不同环境中的输出不同的结果。因此，Go社区希望通过go.mod将工具的版本以及依赖管理起来。 在Go 1.24版本之前，Go Wiki推荐tools.go的一种来自社区的最佳实践，阐述这种实践的最好的一个示例来自Go modules by example中的一个文档：”Tools as dependencies“，其大致思路是将项目依赖的Go工具以“项目依赖”的方式存放到tools.go文件(放到go module根目录下)中，以golang.org/x/tools/cmd/stringer为例，tools.go的内容大致如下： //go:build tools package tools import ( _ "golang.org/x/tools/cmd/stringer" ) 然后在同一目录下安装stringer或直接go run： $go install golang.org/x/tools/cmd/stringer 在安装stringer时，go.mod会记录下对stringer的依赖以及对应的版本，后续go.mod提交到项目repo中，所有项目成员就都可以使用相同版本的Stringer了。 tools.go实践虽然能解决问题，但这种方式还是存在一些不便： 配置繁琐：需要手动创建 tools.go 文件，并添加特定的构建标签来排除它； 使用不便：运行工具时可能需要额外的脚本或配置(每次手敲go run golang.org/x/tools/cmd/stringer的确有些不便)。 Go开发者期望工具依赖也能够无缝地与其他项目依赖(包依赖)统一管理，并纳入go.mod的版本控制体系。 为此，该提案设计并实现了下面几点以满足开发者的上述述求： go.mod引入tool directive，用于显式声明项目所需的工具。 tool directive与其他依赖项统一纳入go.mod文件，方便管理和版本控制。 扩展go install和go get命令，支持安装、更新和卸载工具。 我们来看一个示例，首先我们初始化一个module： $ gotip mod [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1-24-foresight-part2-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/17/go-1-24-foresight-part2">本文永久链接</a> &#8211; https://tonybai.com/2024/12/17/go-1-24-foresight-part2</p>
<p>在<a href="https://tonybai.com/2024/12/16/go-1-24-foresight-part1/">上一篇文章</a>中，我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文，重点介绍Go 1.24在工具链和标准库方面的重要更新，供大家参考。</p>
<h2>1. 工具链</h2>
<h3>1.1 <a href="https://github.com/golang/go/issues/48429">go.mod新增tool指示符，支持对tool的依赖管理(#48429)</a></h3>
<p>我们日常编写Go项目代码时常常会依赖一些使用Go编写的工具，比如golang.org/x/tools/cmd/stringer或github.com/kyleconroy/sqlc。我们希望所有项目合作者都使用相同版本的工具，以避免在不同时间、不同环境中的输出不同的结果。因此，Go社区希望通过go.mod将工具的版本以及依赖管理起来。</p>
<p>在Go 1.24版本之前，Go Wiki推荐tools.go的一种来自社区的最佳实践，阐述这种实践的最好的一个示例来自Go modules by example中的一个文档：”<a href="https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md">Tools as dependencies</a>“，其大致思路是将项目依赖的Go工具以“项目依赖”的方式存放到tools.go文件(放到go module根目录下)中，以golang.org/x/tools/cmd/stringer为例，tools.go的内容大致如下：</p>
<pre><code>//go:build tools

package tools

import (
    _ "golang.org/x/tools/cmd/stringer"
)
</code></pre>
<p>然后在同一目录下安装stringer或直接go run：</p>
<pre><code>$go install golang.org/x/tools/cmd/stringer
</code></pre>
<p>在安装stringer时，go.mod会记录下对stringer的依赖以及对应的版本，后续go.mod提交到项目repo中，所有项目成员就都可以使用相同版本的Stringer了。</p>
<p>tools.go实践虽然能解决问题，但这种方式还是存在一些不便：</p>
<ul>
<li>配置繁琐：需要手动创建 tools.go 文件，并添加特定的<a href="https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package">构建标签</a>来排除它；</li>
<li>使用不便：运行工具时可能需要额外的脚本或配置(每次手敲go run golang.org/x/tools/cmd/stringer的确有些不便)。</li>
</ul>
<p>Go开发者期望<strong>工具依赖</strong>也能够无缝地与其他项目依赖(包依赖)统一管理，并纳入go.mod的版本控制体系。</p>
<p>为此，该提案设计并实现了下面几点以满足开发者的上述述求：</p>
<ul>
<li>go.mod引入tool directive，用于显式声明项目所需的工具。</li>
<li>tool directive与其他依赖项统一纳入go.mod文件，方便管理和版本控制。</li>
<li>扩展go install和go get命令，支持安装、更新和卸载工具。</li>
</ul>
<p>我们来看一个示例，首先我们初始化一个module：</p>
<pre><code>$ gotip mod init demo
go: creating new go.mod: module demo
$ cat go.mod
module demo

go 1.24
</code></pre>
<p>编辑go.mod，加入下面内容：</p>
<pre><code>$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer
</code></pre>
<p>安装tool前需要go get它的依赖，否则go install会报错：</p>
<pre><code>$gotip install tool
no required module provides package golang.org/x/tools/cmd/stringer; to add it:
    go get golang.org/x/tools/cmd/stringer

$gotip get golang.org/x/tools/cmd/stringer
go: downloading golang.org/x/tools v0.28.0
go: downloading golang.org/x/sync v0.10.0
go: downloading golang.org/x/mod v0.22.0
go: added golang.org/x/mod v0.22.0
go: added golang.org/x/sync v0.10.0
go: added golang.org/x/tools v0.28.0

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/tools v0.28.0 // indirect
)
</code></pre>
<p>我们看到：go.mod中require了stringer的依赖。</p>
<p>接下来，我们便可以用go install安装stringer了：</p>
<pre><code>$ ls -l `which stringer` // old版本的stringer
-rwxr-xr-x 1 root root 6500561 1月  23 2024 /root/go/bin/stringer

$ gotip install tool
$ ls -l `which stringer`
-rwxr-xr-x 1 root root 7303970 12月  9 21:41 /root/go/bin/stringer
</code></pre>
<p>后续要更新stringer版本，可以直接使用go get -u：</p>
<pre><code>$gotip get -u golang.org/x/tools/cmd/stringer
</code></pre>
<p>此外，除了手工编辑go.mod，添加依赖的tool外，我们也可以直接使用go get -tool像go.mod中添加依赖的tool，它们在效果上是等价的：</p>
<pre><code>// 重置go.mod到最初状态
# cat go.mod
module demo

go 1.24

// 执行go get -tool
$gotip get -tool golang.org/x/tools/cmd/stringer
go: added golang.org/x/mod v0.22.0
go: added golang.org/x/sync v0.10.0
go: added golang.org/x/tools v0.28.0

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/tools v0.28.0 // indirect
)
</code></pre>
<p>使用stringer时也无需手工敲入那么长的命令(go run golang.org/x/tools/cmd/stringer)，只需使用gotip tool stringer即可：</p>
<pre><code>$ gotip tool stringer
Usage of stringer:
    stringer [flags] -type T [directory]
    stringer [flags] -type T files... # Must be a single package
For more information, see:

https://pkg.go.dev/golang.org/x/tools/cmd/stringer

Flags:
  -linecomment
        use line comment text as printed text when present
  -output string
        output file name; default srcdir/&lt;type&gt;_string.go
  -tags string
        comma-separated list of build tags to apply
  -trimprefix prefix
        trim the prefix from the generated constant names
  -type string
        comma-separated list of type names; must be set
</code></pre>
<p>go tool stringer就相当于go run golang.org/x/tools/cmd/stringer@v0.28.0了(注：v0.28.0是当前golang.org/x/tools的版本)。</p>
<p>tool directive和go工具链做了很好的融合，除了上面的命令外，还支持：</p>
<ul>
<li>go build tool构建module依赖的tool，并将构建出可执行文件放在当前目录下；</li>
<li>go build -o bin/ tool将构建module依赖的tool，并将构建出可执行文件放在项目自己的bin目录下。</li>
</ul>
<p>到这里，屏幕前的你可能会问一个问题：如果本地多个项目依赖同一个工具的不同版本，比如golangci-lint的v1.62.2和v1.62.0时，那么两个项目安装的golangci-lint是否会相互覆盖和影响呢？我们来验证一下，下面建立两个项目：tool-directive1和tool-directive2。</p>
<pre><code>.
├── tool-directive1/
│   ├── go.mod
│   └── go.sum
└── tool-directive2/
    ├── go.mod
    └── go.sum
</code></pre>
<p>我们先在tool-directive1下面执行下面命令添加对golangci-lint的依赖：</p>
<pre><code>$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint
go: downloading github.com/golangci/golangci-lint v1.62.2
go: downloading github.com/gofrs/flock v0.12.1
go: downloading github.com/fatih/color v1.18.0
... ...
</code></pre>
<p>然后在同一个目录下，使用gotip tool golangci-lint执行该工具，查看其版本：</p>
<pre><code>$ gotip tool golangci-lint --version
golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=") on (unknown)
</code></pre>
<p>我们看到tool-directive1依赖了v1.62.2版本的golangci-lint。不过你在执行上述命令时可能会注意到，这个命令的执行非常耗时，可能需要10~20s才能出结果。如果你再执行一次，它就可以瞬间输出结果，为什么会这样的？稍后我们给出答案。</p>
<p>现在我们切换到tool-directive2目录下，执行下面命令添加对golangci-lint v1.62.0版本的依赖：</p>
<pre><code>$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0
</code></pre>
<p>然后在同一个目录下，使用gotip tool golangci-lint执行该工具，查看其版本：</p>
<pre><code>$gotip tool golangci-lint --version
golangci-lint has version v1.62.0 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:/G0g+bi1BhmGJqLdNQkKBWjcim8HjOPc4tsKuHDOhcI=") on (unknown)
</code></pre>
<p>我们看到tool-directive2下得到的是v1.62.0版本的golangci-lint。并且我们会遇到同样的现象：第一次执行很慢，第二次执行就会瞬间出结果。</p>
<p>再回到tool-directive1下，看看它依赖的golangci-lint是否被覆盖了：</p>
<pre><code>$gotip tool golangci-lint --version
golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=") on (unknown)
</code></pre>
<p>我们发现：两个项目下依赖的版本各自独立，并不会相互覆盖。</p>
<p>这其中的缘由又是什么呢？为什么使用go tool golangci-lint第一次执行会慢，而后续的执行就会飞快呢？下面的issue将回答这个问题。</p>
<h3>1.2 <a href="https://github.com/golang/go/issues/69290">Go run生成的可执行文件支持缓存(#69290)</a></h3>
<p>Go 1.24 之前，cmd/go仅缓存编译后的包文件（build actions），而不缓存链接后的二进制文件（link actions）。不缓存二进制文件很大原因在于二进制文件比单个包对象文件大得多，并且它们不像包文件那样被经常重用。</p>
<p>不过上述1.1中，让go支持对依赖工具的管理以及让go tool支持自定义工具执行的issue让这个issue最终被纳入Go 1.24。该issue实现后，go run以及像上面那种go tool golangci-lint(本质上也是go run github.com/golangci/golangci-lint/cmd/golangci-lint@vx.y.z)的编译链接的结果会被缓存到go build cache中。这也是上面不同项目依赖同一工具不同版本时不会相互覆盖以及首次使用go tool执行依赖工具较慢的原因，第一次go tool执行会执行编译链接过程，之后的运行就会从缓存中直接找到缓存的文件并执行了。</p>
<p>由于这个issue会显著增大go build cache的磁盘空间占用，该issue也规定了，在<a href="https://github.com/golang/go/issues/68872">缓存执行定期清理</a>的时候，<strong>可执行文件缓存会优先于包缓存被优先清理掉</strong>。</p>
<h3>1.3 <a href="https://github.com/golang/go/issues/50603">Go build支持生成伪版本号(#50603)</a></h3>
<p>在Go 1.18及之后的版本中，cmd/go工具链在构建二进制文件时会嵌入依赖版本信息和VCS（版本控制系统）信息，这使得开发者可以更容易地追踪二进制文件的来源。然而，当使用go build命令构建主模块时，主模块的版本信息并不会被记录，而是显示为(devel)，这导致开发者需要使用外部构建脚本或-ldflags来手动设置版本信息。相比之下，go install命令会正确记录主模块的版本信息。</p>
<p>该issue就旨在让go build命令也能像go install一样，自动嵌入主模块的版本信息，从而避免开发者依赖外部构建脚本。</p>
<p>落地后，Go 1.24的go build命令会在编译后的二进制文件中包含版本信息。如果本地VCS（版本控制系统）标签可用，主模块的版本将从该标签中设置。如果没有本地VCS标签可用，则会生成一个伪版本（pseudo-version），通常包含时间戳和提交哈希。 此外，为了避免与已发布的版本混淆，go build还会在伪版本中添加一些特殊的标识符，例如devel，以表明这是一个本地构建的版本。如果有未提交的VCS更改，则会附加一个+dirty后缀。</p>
<p>使用-buildvcs=false标志可以省略二进制文件中的版本控制信息。</p>
<p>下面对比一下Go 1.24版本之前与Go 1.24版本在go build时生成的版本信息的差异：</p>
<p>以Go 1.23为例，其构建和安装的stringer的版本信息如下：</p>
<pre><code>$go version  -m `which stringer`
/root/go/bin/stringer: go1.23.0
... ...
</code></pre>
<p>而使用go1.24的build构建的stringer的版本信息如下：</p>
<pre><code>$go version  -m tool-directive1/bin/stringer
tool-directive1/bin/stringer: devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000
... ...
</code></pre>
<h3>1.4 <a href="https://github.com/golang/go/issues/64876">默认使能GOCACHEPROG以支持外部缓存</a></h3>
<p>估计Go社区很少有人用过GOCACHEPROG，即便在Go 1.21版本之后，它是以实验特性的形式提供的，通过GOEXPERIMENT=cacheprog启用。这个特性是由Go语言元老<a href="https://github.com/bradfitz">Brad Fitzpatrick</a>提出的，其主issue编号是<a href="https://github.com/golang/go/issues/59719">59719</a>。</p>
<p>我们知道：Go语言的cmd/go工具已经具备了强大的缓存支持，但其缓存机制仅限于基于文件系统的缓存。这种缓存方式在某些场景下效率不高，尤其是在CI（持续集成）环境中，用户通常需要将GOCACHE目录打包和解压缩，这往往比CI操作本身还要慢。此外，用户可能希望利用位于网络上的共享缓存(比如S3)或公司内部的P2P缓存协议来提高缓存效率，但这些功能并不适合直接集成到cmd/go工具中。</p>
<p>为了解决上述问题，Brad Fitzpatrick提出了一个新的环境变量GOCACHEPROG，类似于现有的GOCACHE变量。通过设置GOCACHEPROG，用户可以指定一个外部程序，该程序将作为子进程运行，并通过标准输入/输出来与cmd/go工具进行通信。cmd/go工具将通过这个接口<strong>与外部缓存程序交互</strong>，外部程序可以根据需要实现任意的缓存机制和策略。</p>
<p>为此，Bradfitz在issue 59719中给出了交互的协议设计。cmd/go工具与外部缓存程序之间的通信基于JSON格式的消息。消息分为请求（ProgRequest）和响应（ProgResponse）。请求包括命令类型、操作ID（ActionID）、对象ID（ObjectID）等。响应则包括缓存命中与否、对象的磁盘路径等信息。</p>
<p>其中请求的命令类型有如下几种：</p>
<ul>
<li>get：从缓存中获取对象。</li>
<li>put：将对象存入缓存。</li>
<li>close：关闭缓存连接。</li>
</ul>
<p>对于put请求，cmd/go工具会将对象的二进制数据通过base64编码后发送给外部程序。对于get请求，外部程序返回对象的磁盘路径。</p>
<p>在\$GOROOT/src/cmd/go/internal/cache/prog.go文件中可以看到具体协议相关的结构。</p>
<p>Bradfitz还给出了一个<a href="https://github.com/bradfitz/go-tool-cache">外部cache的样例程序go-tool-cache</a>，还有开发者fork了该样例程序，将它改造为<a href="https://github.com/or-shachar/go-tool-cache/">以S3为后端cache的外部缓存程序</a>。感兴趣的童鞋，可以按照这些样例程序的说明试验一下外部缓存功能。</p>
<h3>1.5 <a href="https://github.com/golang/go/issues/26232">go工具链支持HTTP扩展认证：GOAUTH(#26232)</a></h3>
<p>在Go语言中，go get命令用于从远程代码仓库获取依赖包。通常，这些依赖包的导入路径是通过HTTP请求获取的，服务器会返回一个包含元标签（meta tag）的HTML页面，指示如何获取该包的源代码。然而，对于需要身份验证的私有仓库，go get无法直接工作，因为go get使用的是net/http.DefaultClient，它不知道如何处理需要身份验证的URL。具体来说，当go get尝试获取一个私有仓库的URL时，由于没有提供身份验证信息，服务器会返回401或403错误，导致go get无法继续执行。这个问题在企业环境中尤为常见，因为许多公司使用私有代码托管服务，而这些服务通常需要身份验证。</p>
<p>issue 26232为上述情况提供了一种方案，让go get能够支持需要身份验证的私有仓库，使得用户可以通过go get命令获取私有仓库中的代码：</p>
<pre><code>$go get git.mycompany.com/private-repo
</code></pre>
<p>即使https://git.mycompany.com/private-repo需要身份验证，go get也能够正常工作。</p>
<p>方案采用了一种类似于Git凭证助手的机制，并通过新增的Go环境变量GOAUTH来指定一个或多个认证命令。go get在执行时会调用这些命令，获取身份验证信息，并在后续的HTTP请求中使用这些信息。</p>
<p>GOAUTH环境变量可以包含一个或多个认证命令，每个命令由空格分隔的参数列表组成，命令之间用分号分隔。go get会在每次需要进行HTTP请求时，首先检查缓存中的认证信息，如果没有匹配的认证信息，则会调用GOAUTH命令来获取新的认证信息。</p>
<p>通过go help goauth可以查看GOAUTH的详细用法，在Go 1.24中它支持如下认证命令：</p>
<ul>
<li>off：禁用GOAUTH功能</li>
<li>netrc：从NETRC或用户主目录中的.netrc文件中获取访问凭证，这也是<strong>GOAUTH的默认值</strong>。</li>
<li>git dir：在指定目录dir中运行git credential fill并使用其凭证。go命令将运行git credential approve/reject来更新凭证助手的缓存。</li>
<li>command：执行给定的命令（以空格分隔的参数列表），并将提供的头信息附加到 HTTPS 请求中。该命令必须按照以下格式生成输出：</li>
</ul>
<pre><code>Response      = { CredentialSet } .
CredentialSet = URLLine { URLLine } BlankLine { HeaderLine } BlankLine .
URLLine       = /* URL that starts with "https://" */ '\n' .
HeaderLine    = /* HTTP Request header */ '\n' .
BlankLine     = '\n' .
</code></pre>
<h3>1.6 <a href="https://github.com/golang/go/issues/62067">go build支持-json(#62067)</a></h3>
<p>Go 1.24版本之前，Go已经支持了go test -json命令，旨在为测试过程提供结构化的JSON输出，便于工具解析和处理测试结果。然而，当测试或导入的包在构建过程中失败时，构建错误信息会与测试的JSON输出交织在一起，导致工具难以准确地将构建错误与受影响的测试包关联起来。这增加了工具处理go test -json输出的复杂性。</p>
<p>为了解决这个问题，issue 62067提出了为go build命令(包括go install)添加-json标志的建议，以便生成与go test -json兼容的结构化JSON输出。go test -json也得到了优化，现在在test时出现构建错误时，go test -json也会以json格式输出构建错误信息，与test结果的json内容可以很好的融合在一起。当然，你也可以通过GODEBUG=gotestjsonbuildtext=1继续让go test -json输出文本格式的构建错误信息，以保持与Go 1.24之前的情况一致。</p>
<h2>2. 标准库</h2>
<p>Go标准库向来是添加新特性的大户，不过鉴于变化太多，下面我们仅列举一些主要的变化点。</p>
<h3>2.1 <a href="https://github.com/golang/go/issues/45669">json包支持omitzero选项</a></h3>
<p>关于这个变化点，我在《<a href="https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/">JSON包新提案：用“omitzero”解决编码中的空值困局</a>》一文中有详细说明，请移步阅读，这里不赘述了。</p>
<h3>2.2 <a href="https://github.com/golang/go/issues/67552">新增weak包和weak指针</a></h3>
<p>weak包和weak指针是Go团队在设计和实现unique包时的“副产物”，Go团队认为weak指针可以给大家带来更灵活的内存管理机制，于是将其从internal中提到标准库中。我之前的《<a href="https://tonybai.com/2024/09/23/go-weak-package-preview/">Go weak包前瞻：弱指针为内存管理带来新选择</a>》一文对weak包有详细说明，请移步阅读。</p>
<h3>2.3 crypto: FIPS 140-3认证</h3>
<p>在Go 1.24开发周期中，Go密码学小组与Russ Cox根据开发者日益增多的密码学合规性(满足FIPS 140)的需求反馈，决定对Go的加密库进行改造，以符合申请进行FIPS 140标准认证的要求。有关这个认证的issue和改动点(cl)都很多，大家可以阅读我的《<a href="https://tonybai.com/2024/11/16/go-crypto-and-fips-140/">走向合规：Go加密库对FIPS 140的支持</a>》一文了解详情。</p>
<h3>2.4 crypto：增加hkdf、pbkdf2、sha3等密码学包</h3>
<p>读过我的《<a href="https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive">Go开发者的密码学导航：crypto库使用指南</a>》一文的读者都知道：Go密码学团队维护的密码学包分布在Go标准库crypto目录和golang.org/x/crypto下面。Go密码学小组负责人<a href="https://github.com/golang/go/issues/65269">Roland Shoemaker认为当前这种”分割”的状态会带来一些问题</a>：</p>
<ul>
<li>用户困惑：用户经常对为什么某些加密库在x/crypto模块中，而另一些在标准库中感到困惑。这种困惑可能导致用户不愿意依赖x/crypto模块中的代码，因为他们误以为x/crypto中的代码是“实验性”的，质量或API稳定性不如标准库。</li>
<li>复杂的安全补丁流程：标准库依赖于x/crypto模块中的多个包（目前有7个），这些包需要被vendored。这种依赖关系增加了安全补丁的复杂性，因为需要一个特殊的第三方流程来处理这些包的补丁，而不是像标准库或x/crypto模块那样直接处理。</li>
<li>开发周期不一致：理论上，x/crypto模块是一个可以快速开发新加密算法或协议的地方，因为这些算法或协议的规范可能还在变化中。然而，实际上，x/crypto模块并没有被这样使用。如果开始这样做，反而会强化用户对x/crypto模块的误解。</li>
<li>特定包的快速开发需求：例如x/crypto/ssh包最近经历了非常快速的开发，许多用户希望立即使用新引入的功能和修复。如果将这个包移入标准库，可能会因为标准库的发布周期较慢而产生摩擦。</li>
</ul>
<p>为此Shoemaker提议了一个将x/crypto下的包到标准库crypto目录下的方案，以简化Go语言加密库的管理和维护，提高用户对这些库的信任和使用率，方案的大致思路和步骤如下：</p>
<ul>
<li>将x/crypto模块中的大部分包直接迁移到标准库的crypto/目录下，迁移过程应在单个标准库发布周期内完成，尽量接近发布周期的末尾，以避免需要同步两个版本的包。</li>
<li>迁移后，冻结x/crypto模块和标准库中的对应包，直到标准库重新开放，只接受标准库版本的更改。</li>
<li>使用构建标签（build tags）来区分迁移前后的版本，允许用户在不更新到最新Go版本的情况下继续使用x/crypto模块。</li>
<li>在迁移后的两个主要版本中（例如，假设在Go 1.24中完成迁移，则在Go 1.26中），移除旧的构建标签实现，只保留转发到标准库版本的包装器。</li>
<li>一些包由于其更新周期与标准库不一致，或者已经冻结/弃用，将不会迁移到标准库中。例如，x/crypto/x509roots包需要根据任意时间表进行更新，因此应移至独立的模块golang.org/x/x509roots。</li>
<li>一些已经弃用或冻结的包（如twofish、cast5、tea等）将保留在x/crypto模块中，并在v1版本中标记为冻结。</li>
<li>x/crypto/ssh包由于其快速的开发周期，可能会在迁移时带来一些麻烦。虽然可以考虑将其推迟迁移，但最终仍建议将其移入标准库。</li>
</ul>
<p>基于上述方案，Go 1.24版本中，Go密码学团队完成了hkdf、pbkdf2、sha3和mlkem等包的迁移。当然这次迁移与<a href="https://github.com/golang/go/issues/69536">Go密码学包要进行FIPS 140-3认证</a>也有着直接的联系。</p>
<p>这里面值得一提的是mklem包，它实现了<a href="https://doi.org/10.6028/NIST.FIPS.203">NIST FIPS 203</a>中指定的抗量子密钥封装方法ML-KEM（以前称为Kyber），也是Go密码学包中第一个<a href="https://en.wikipedia.org/wiki/Post-quantum_cryptography">后量子密码学</a>包。</p>
<h3>2.5 <a href="https://github.com/golang/go/issues/67002">支持限制目录的文件系统访问(#67002)</a></h3>
<p>目录遍历漏洞（Directory Traversal Vulnerabilities）和符号链接遍历漏洞（Symlink Traversal Vulnerabilities）是常见的安全漏洞。攻击者通过提供相对路径（如”../../../etc/passwd”）或创建符号链接，诱使程序访问其本不应访问的文件，从而导致安全问题。例如，<a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3400">CVE-2024-3400 </a>是一个最近的真实案例，展示了目录遍历漏洞如何导致远程代码执行。</p>
<p>在Go中，虽然可以通过 filepath.IsLocal等函数来验证文件名，但防御符号链接遍历攻击较为困难。现有的os.Open和os.Create等函数在处理不受信任的文件名时，容易受到这些攻击的影响。</p>
<p>为了解决这些问题，issue 67002提出了在os包中添加几个新的函数和方法，以安全地打开文件并防止目录遍历和符号链接遍历攻击。</p>
<p>最初该提案提出新增一些安全访问文件系统的API函数，在讨论过程中，Russ Cox 提出了一个更为简洁的方案，避免了引入大量新的 API，而是通过引入一个新的类型 Dir 来表示受限的文件系统根目录。这个方案最终奠定了该提案的最终实现。</p>
<p>最终Go在os包中引入了一个新的Root类型，并基于该类型提供了在特定目录内执行文件系统操作的能力。os.OpenRoot函数打开一个目录并返回一个os.Root。os.Root上的方法仅限于在该目录内操作，并且不允许路径引用目录外的位置，包括跟随符号链接指向目录外的路径。下面是一些Root类型的常用方法：</p>
<ul>
<li>os.Root.Open 打开一个文件以供读取。</li>
<li>os.Root.Create 创建一个文件。</li>
<li>os.Root.OpenFile 是通用的打开调用。</li>
<li>os.Root.Mkdir 创建一个目录。</li>
</ul>
<p>下面我们用一个示例对比一下通过os.Root进行的文件系统操作与传统文件系统操作的差异：</p>
<pre><code>// go1.24-foresight/stdlib/osroot/main.go
package main

import (
    "fmt"
    "os"
)

func main() {
    // 使用 os.Root 访问相对路径
    root, err := os.OpenRoot(".") // 打开当前目录作为根目录
    if err != nil {
        fmt.Println("Error opening root:", err)
        return
    }
    defer root.Close()

    // 尝试访问相对路径 "../passwd"
    file, err := root.Open("../passwd")
    if err != nil {
        fmt.Println("Error opening file with os.Root:", err)
    } else {
        fmt.Println("Successfully opened file with os.Root")
        file.Close()
    }

    // 传统的 os.OpenFile 方式
    // 尝试访问相对路径 "../passwd"
    file2, err := os.OpenFile("../passwd", os.O_RDONLY, 0644)
    if err != nil {
        fmt.Println("Error opening file with os.OpenFile:", err)
    } else {
        fmt.Println("Successfully opened file with os.OpenFile")
        file2.Close()
    }
}
</code></pre>
<p>运行上述代码，我们得到：</p>
<pre><code>$gotip run main.go
Error opening file with os.Root: openat ../passwd: path escapes from parent
Successfully opened file with os.OpenFile
</code></pre>
<p>我们看到：当代码通过os.Root返回的目录来尝试访问相对路径”../passwd”时，由于os.Root限制了操作仅限于根目录内，因此会返回错误。</p>
<p>从安全角度来看，Go 1.24之后，建议搭建多多使用这种安全操作文件系统的方式，如果你的文件操作都局限在一个目录下。</p>
<h3>2.6 <a href="https://github.com/golang/go/issues/67535">使用runtime.AddCleanup替代SetFinalizer(#67535)</a></h3>
<p>Go 1.24版本之前，Go提供了runtime.SetFinalizer函数用于对象的终结处理。然而，SetFinalizer的使用存在许多问题和限制，Michael Knyszek总结了下面几点：</p>
<ul>
<li>必须引用分配的第一个字：SetFinalizer必须引用分配的第一个字，这要求程序员了解什么是“分配”，而这一概念在语言中通常不暴露。</li>
<li>每个对象只能有一个终结器：不能为同一个对象设置多个终结器。</li>
<li>引用循环问题：如果对象参与了引用循环，且该对象有终结器，那么该对象将不会被释放，终结器也不会运行。</li>
<li>GC周期问题：有终结器的对象至少需要两个GC周期才能被释放。</li>
</ul>
<p>后面两个问题主要源于SetFinalizer允许对象复活（object resurrection），这使得对象的清理变得复杂且不可靠。</p>
<p>为了解决上述问题，，Michael Knyszek提出了一个新的API runtime.AddCleanup，并建议正式弃用runtime.SetFinalizer。AddCleanup的设计目标是解决SetFinalizer的诸多问题，特别是避免对象复活，从而允许对象的及时清理，并支持对象的循环清理。</p>
<p>AddCleanup函数的原型如下：</p>
<pre><code>func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup
</code></pre>
<p>AddCleanup函数将一个清理函数附加到ptr。当ptr不再可达时，运行时会在一个单独的goroutine中调用 cleanup(arg)。</p>
<p>AddCleanup的一个典型的用法如下：</p>
<pre><code>f, _ := Open(...)
runtime.AddCleanup(f, func(fd uintptr) { syscall.Close(fd) }, f.Fd())
</code></pre>
<p>通常，ptr是一个包装底层资源的对象（例如上面典型用法中的那个包装操作系统文件描述符的File对象），arg是底层资源（例如操作系统文件描述符），而清理函数释放底层资源（例如，通过调用close系统调用）。</p>
<p>AddCleanup对ptr的约束很少，支持为同一个指针附加多个清理函数。不过，如果ptr可以从cleanup或arg中可达，ptr将永远不会被回收，清理函数也永远不会运行。作为一种简单的保护措施，如果arg等于ptr，AddCleanup会引发panic。清理函数的运行顺序没有指定。特别是，如果几个对象相互指向并且同时变得不可达，它们的清理函数都可以运行，并且可以以任何顺序运行。即使对象形成一个循环也是如此。</p>
<p>cleanup(arg)调用并不总是保证运行，特别是它不保证在程序退出之前能运行。</p>
<p>清理函数可能在对象变得不可达时立即运行。为了正确使用清理函数，程序必须确保对象在清理函数安全运行之前保持可达。存储在全局变量中的对象，或者可以通过从全局变量跟踪指针找到的对象，是可达的。函数参数或方法接收者可能在函数最后一次提到它的地方变得不可达。为了确保清理函数不会过早调用，我们可以将对象传递给KeepAlive函数，以保证对象在保持可达的最后一个点之后依然可达。</p>
<p>到这里，也许一些读者想到了RAII(Resource Acquisition Is Initialization），RAII的核心思想是将资源的获取和释放与对象的生命周期绑定在一起，从而确保资源在对象不再使用时能够被正确释放。似乎AddCleanup可以用于实现Go版本的RAII，下面是一个示例：</p>
<pre><code>// go1.24-foresight/stdlib/addcleanup/main.go

package main

import (
    "fmt"
    "os"
    "runtime"
    "syscall"
    "time"
)

type FileResource struct {
    file *os.File
}

func NewFileResource(filename string) (*FileResource, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    // 使用 AddCleanup 注册清理函数
    fd := file.Fd()
    runtime.AddCleanup(file, func(fd uintptr) {
        fmt.Println("Closing file descriptor:", fd)
        syscall.Close(int(fd))
    }, fd)

    return &amp;FileResource{file: file}, nil
}

func main() {
    fileResource, err := NewFileResource("example.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }

    // 模拟使用 fileResource
    _ = fileResource
    fmt.Println("File opened successfully")

    // 当 fileResource 不再被引用时，AddCleanup 会自动关闭文件
    fileResource = nil
    runtime.GC() // 强制触发 GC，以便清理 fileResource
    time.Sleep(time.Second * 5)
}
</code></pre>
<p>运行上述代码得到如下结果：</p>
<pre><code>$gotip run main.go
File opened successfully
Closing file descriptor: 3
</code></pre>
<p>的确，在Go中，runtime.AddCleanup可以用来模拟RAII机制，但与传统的RAII有一些不同，在Go中，资源获取通常是通过显式的函数调用来完成的，例如打开文件等，而不是像C++那样在构造函数中隐式完成。并且，资源的释放由Go GC回收对象时触发。如果要实现C++那样的RAII，需要我们自行做一些封装。</p>
<h3>2.7 <a href="https://github.com/golang/go/issues/61515">不易出错的新Benchmark函数(#61515)</a></h3>
<p>在Go语言中，基准测试（benchmarking）是通过testing.B类型的b.N来实现的。b.N表示基准测试需要执行的迭代次数。然而，这种设计存在一些问题：</p>
<ul>
<li>容易忘记使用b.N：在某些情况下，开发者可能会忘记使用b.N，导致基准测试无法正确执行。</li>
<li>误用b.N：开发者可能会错误地将b.N用于其他目的，例如调整算法输入的大小，而不是作为迭代次数。</li>
<li>复杂的计时器管理：基准测试框架无法知道b.N循环何时开始，因此如果基准测试有复杂的设置（setup），开发者需要手动调用ResetTimer来重置计时器，这提高了开发人员使用benchmark函数的门槛，还非常容易出错。</li>
</ul>
<p>为了解决上述问题，Austin Clements提议在testing.B中添加一个新的方法Loop，并鼓励开发者使用Loop而不是b.N：</p>
<pre><code>func (b *B) Loop() bool

func Benchmark(b *testing.B) {
    ...(setup)
    for b.Loop() {
        // … benchmark body …
    }
    ...(cleanup)
}
</code></pre>
<p>显然新Loop方法以及基于新Loopfang方法的“新Benchmark”函数有如下优点：</p>
<ul>
<li>避免误用b.N：Loop方法明确地用于基准测试的迭代，开发者无法将其用于其他目的。</li>
<li>自动计时器管理：基准测试框架可以仅记录发生在基准测试操作期间(即for循环内部)的时间和其他指标，因此开发者不再需要手动调用ResetTimer或担心setup的复杂性了。</li>
<li>减少重复设置：Loop方法可以在内部处理迭代启动（ramp-up），这意味着基准测试之前的setup只会执行一次，而不是在每次启动步骤中重复执行。这对于具有复杂设置的基准测试来说，可以节省大量时间。</li>
<li>防止编译器优化：对go编译器来说，Loop方法本身就是一个的明显信号，可阻止某些优化（如内联），以确保基准测试结果的有效性。</li>
<li>支持更丰富的统计分析：将来，Loop方法可以收集值分布而不是仅仅平均值，从而提供更深入的基准测试结果分析。</li>
</ul>
<p>这里也<strong>强烈建议大家在Go 1.24及以后版本中，使用基于B.Loop的新基准测试函数</strong>。</p>
<h3>2.8 <a href="https://github.com/golang/go/issues/69687">增加实验包testing/synctest(#69687)</a></h3>
<p>在Go语言中，测试并发代码一直是一个具有挑战性的任务。传统的测试方法通常依赖于真实的系统时钟和同步机制，这会导致测试变得缓慢且容易出现不确定性（即“flaky”测试）。例如，测试一个带有超时机制的并发缓存时，测试代码可能需要等待几秒钟来验证缓存条目是否在预期时间内过期。这种等待不仅增加了测试的执行时间，还可能导致测试在某些情况下失败，尤其是在CI系统负载较高或执行环境不稳定的情况下。</p>
<p>为了解决这些问题，Go社区提出了一个<a href="https://github.com/golang/go/issues/67434">新的testing/synctest包</a>，旨在简化并发代码的测试。该包的核心思想是通过使用虚拟时钟和goroutine组(也称为气泡(bubble)来控制并发代码的执行，从而使测试既快速又可靠。下面是synctest包的API：</p>
<pre><code>func Run(f func()) {
    synctest.Run(f)
}

func Wait() {
    synctest.Wait()
}
</code></pre>
<p>我们看到synctest包对外仅暴露两个公开函数。</p>
<p>Run函数在一个新的goroutine中执行f函数，并创建一个独立的goroutine组（气泡），确保所有相关的goroutine都在虚拟时钟的控制下执行。气泡内的goroutine不能与气泡外的goroutine直接交互，否则会引发panic。如果所有goroutine都被阻塞且没有定时器被调度，Run会引发panic。Run 会在气泡中的所有goroutine退出后返回。</p>
<p>Wait函数调用后将阻塞，直到当前气泡中的所有其他goroutine都处于持久阻塞状态。该函数用于确保在虚拟时间推进后，所有相关的goroutine都已经完成其工作。即确保在测试继续之前所有后台goroutine都已空闲或退出。如果从非气泡的goroutine调用Wait，或者同一气泡中的两个goroutine同时调用Wait，会引发panic。阻塞在系统调用或外部事件（如网络操作）的goroutine不是持久阻塞的，Wait不会等待这些goroutine。</p>
<p>这里再明确一下上面API说明中提到的各种概念：</p>
<ul>
<li>goroutine组（气泡）</li>
</ul>
<p>Run函数创建的goroutine及其间接启动的所有goroutine形成一个独立的“气泡”。气泡内的goroutine使用虚拟时钟，并且气泡内的所有操作（如通道、定时器等）都与该气泡关联。气泡内的goroutine不能与气泡外的goroutine直接交互。</p>
<ul>
<li>虚拟时钟</li>
</ul>
<p>虚拟时钟的初始时间为2000-01-01 00:00:00 UTC。每个气泡有一个虚拟时钟，它只有在所有goroutine都处于阻塞状态时才会推进。这意味着测试代码可以精确控制时间的流逝，而不会受到真实系统时钟的限制。</p>
<ul>
<li>持久阻塞</li>
</ul>
<p>一个goroutine如果只能被气泡内的另一个goroutine解除阻塞，则称其为持久阻塞。以下操作会使goroutine持久阻塞：</p>
<pre><code>- 在气泡内向通道发送或接收数据
- 在select语句中，每个case都是气泡内的通道
- sync.Cond.Wait
- time.Sleep
</code></pre>
<p>下面是一个使用testing/synctest进行测试的简单示例，我们有一个Cache结构：</p>
<pre><code>// go1.24-foresight/stdlib/synctest/cache.go

package main

import (
    "sync"
    "time"
)

// Cache 是一个泛型并发缓存，支持任意类型的键和值。
type Cache[K comparable, V any] struct {
    mu      sync.Mutex
    items   map[K]cacheItem[V]
    expiry  time.Duration
    creator func(K) V
}

// cacheItem 是缓存中的单个条目，包含值和过期时间。
type cacheItem[V any] struct {
    value     V
    expiresAt time.Time
}

// NewCache 创建一个新的缓存，带有指定的过期时间和创建新条目的函数。
func NewCache[K comparable, V any](expiry time.Duration, f func(K) V) *Cache[K, V] {
    return &amp;Cache[K, V]{
        items:   make(map[K]cacheItem[V]),
        expiry:  expiry,
        creator: f,
    }
}

// Get 返回缓存中指定键的值，如果键不存在或已过期，则创建新条目。
func (c *Cache[K, V]) Get(key K) V {
    c.mu.Lock()
    defer c.mu.Unlock()

    // 检查缓存中是否存在该键
    item, exists := c.items[key]

    // 如果键存在且未过期，返回缓存的值
    if exists &amp;&amp; time.Now().Before(item.expiresAt) {
        return item.value
    }

    // 如果键不存在或已过期，创建新条目
    value := c.creator(key)
    c.items[key] = cacheItem[V]{
        value:     value,
        expiresAt: time.Now().Add(c.expiry),
    }

    return value
}
</code></pre>
<p>上述代码实现了一个简单的并发缓存，支持泛型键和值，并且具有过期机制。通过使用sync.Mutex来保护对缓存条目的并发访问，确保了线程安全。Get方法在键不存在或已过期时，会调用creator函数创建新条目，并更新缓存。</p>
<p>下面是对上面Cache结构进行并发测试的代码：</p>
<pre><code>// go1.24-foresight/stdlib/synctest/cache_test.go

package main

import (
    "testing"
    "testing/synctest"
    "time"
)

func TestCacheEntryExpires(t *testing.T) {
    synctest.Run(func() {
        count := 0
        c := NewCache(2*time.Second, func(key string) int {
            count++
            return count
        })

        // Get an entry from the cache.
        if got, want := c.Get("k"), 1; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }

        // Verify that we get the same entry when accessing it before the expiry.
        time.Sleep(1 * time.Second)
        synctest.Wait()
        if got, want := c.Get("k"), 1; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }

        // Wait for the entry to expire and verify that we now get a new one.
        time.Sleep(3 * time.Second)
        synctest.Wait()
        if got, want := c.Get("k"), 2; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }
    })
}
</code></pre>
<p>通过使用synctest.Run和synctest.Wait，上述测试代码能够在虚拟时钟的控制下<strong>验证Cache的过期机制</strong>。synctest.Run创建了一个独立的goroutine组，确保所有相关的goroutine都在虚拟时钟的控制下执行。synctest.Wait确保在虚拟时间推进后，所有相关的goroutine都已经完成其工作。</p>
<p>使用gotip执行该测试：</p>
<pre><code>$GOEXPERIMENT=synctest  gotip test -v
=== RUN   TestCacheEntryExpires
--- PASS: TestCacheEntryExpires (0.00s)
PASS
ok      demo    0.002s
</code></pre>
<p>我们可以瞬间得到结果，而<strong>无需等待代码中的Sleep秒数</strong>。</p>
<h3>2.9 其他一些变化</h3>
<ul>
<li><a href="https://github.com/golang/go/issues/62005">log/slog: 增加slog.DiscardHandler(#62005)</a></li>
</ul>
<p>slog包添加包级变量slog.DiscardHandler （类型为slog.Handler ），它将丢弃所有日志输出。</p>
<ul>
<li><a href="https://github.com/golang/go/issues/61901">bytes和strings增加一些iterator(#61901)</a></li>
</ul>
<p>下面是五个返回迭代器的新增函数，以strings包为例：</p>
<pre><code>- func Lines(s string) iter.Seq[string] 

返回一个迭代器，遍历字符串s中以换行符结尾的行。

- func SplitSeq(s, sep string) iter.Seq[string] 

返回一个迭代器，遍历s中由sep分隔的所有子字符串。  

- func SplitAfterSeq(s, sep string) iter.Seq[string] 

返回一个迭代器，遍历s中在每个sep实例之后分割的子字符串。 

- func FieldsSeq(s string) iter.Seq[string] 

返回一个迭代器，遍历s中由空白字符（由unicode.IsSpace定义）分隔的子字符串。

- func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string] 

返回一个迭代器，遍历s中由满足f(c)的Unicode码点分隔的子字符串。
</code></pre>
<ul>
<li><a href="https://github.com/golang/go/issues/70683">sync.Map的底层实现换成了HashTrieMap(#70683)</a></li>
</ul>
<p>和weak包一样，HashTrieMap同样是实现unique包的副产品，但它的性能很好，在很多情况下都要比sync.Map快很多。于是Michael Knyszek使用HashTrieMap替换了sync.Map的底层实现。</p>
<p>当然，如果你不满意HashTrieMap的表现，你也可以使用GOEXPERIMENT=nosynchashtriemap恢复到sync.Map之前的实现。</p>
<ul>
<li><a href="https://github.com/golang/go/issues/67816">net/http: 支持非加密的http/2(#67816)</a></li>
</ul>
<p>在Go语言的net/http包中，HTTP/2的支持默认是通过TLS加密的连接来实现的，通常称为”h2&#8243;。然而，HTTP/2也可以在不加密的TCP连接上运行，这种模式被称为”h2c”（HTTP/2 Clear Text）。尽管golang.org/x/net/http2/h2c包提供了对h2c的支持，但这种支持并不直接集成到net/http包中，导致用户在使用h2c时需要进行复杂的配置和处理。因此，社区提出了将h2c支持直接集成到net/http包中的issue，以简化用户的使用体验。</p>
<p>直接集成h2c支持后，将使得Go语言的HTTP/2功能更加完整，用户可以更方便地在未加密的连接上使用HTTP/2。</p>
<h2>3. 其它</h2>
<h3>3.1 <a href="https://github.com/golang/go/issues/65199">支持go:wasmexport指示符(#65199)</a></h3>
<p>Go语言在WebAssembly（Wasm）的支持方面已经有了一定的进展，特别是在<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">Go 1.21版本</a>引入了<a href="https://github.com/golang/go/issues/59149">go:wasmimport指示符</a>，使得<a href="https://go.dev/blog/wasi">Go代码可以调用Wasm宿主定义的函数</a>。然而，目前仍然无法从Wasm宿主调用Go代码。这对于一些需要扩展功能的应用来说是一个限制，例如Envoy、Istio、VS Code等应用，它们允许通过调用Wasm编译的代码来扩展功能。但Go目前无法支持这些应用，因为Go编译的Wasm模块中唯一导出的函数是&#95;start，对应于main包中的main函数。</p>
<p>但Go社区对导出Go函数为wasm有着迫切的需求，同时，导出函数到Wasm宿主也是实现GOOS=wasip2的必要条件(<a href="https://github.com/WebAssembly/WASI/blob/main/preview2/README.md">wasip2是WASI规范的预览2版本</a>)。</p>
<p>于是issue 65199给出了导出Go函数到Wasm的落地方案。该issue提议在库模式下(即导出的Go函数供其他基于wasm运行时库开发的应用使用)，重用-buildmode构建标志值c-shared，用于wasip1。它现在向编译器发出信号，要求用&#95;initialize函数替换&#95;start函数，该函数执行运行时和包的初始化：</p>
<pre><code>$gotip help buildmode
... ...
    -buildmode=c-shared
        Build the listed main package, plus all packages it imports,
        into a C shared library. The only callable symbols will
        be those functions exported using a cgo //export comment.
        On wasip1, this mode builds it to a WASI reactor/library,
        of which the callable symbols are those functions exported
        using a //go:wasmexport directive. Requires exactly one
        main package to be listed.
... ...
</code></pre>
<p>新增一个编译器指示符go:wasmexport，用于向编译器发出信号，表明某个函数应该使用Wasm导出（Wasm export），在生成的Wasm二进制文件中导出。该指示符只能在GOOS=wasip1时使用，否则会导致编译失败。</p>
<pre><code>//go:wasmexport name
</code></pre>
<p>其中name是导出函数的名称，该参数是必需的。<strong>该指示符只能用于函数，不能用于方法</strong>。</p>
<p>该issue由Johan Brandhorst提出，但最终是由CherryMui给出了最终实现，并且CherryMui还给出了一个<a href="https://go.googlesource.com/scratch/+/refs/heads/master/cherry/wasmtest/">应用go:wasmexport的example</a>，这个example演示了go:wasmexport在库模式下的应用方法。例子代码较多，这里我做了一个裁剪，下面是裁剪后的代码和使用方法，大家可以参考一下。</p>
<p>示例的结构如下：</p>
<pre><code>$tree -F ./wasmtest
./wasmtest
├── Makefile
├── go.mod
├── go.sum
├── testprog/
│   └── x.go
└── w.go
</code></pre>
<p>其中testprog/x.go中导出了一个Add函数：</p>
<pre><code>// go1.24-foresight/wasmtest/testprog/x.go

package main

func init() {
    println("init function called")
}

//go:wasmexport Add
func Add(a, b int64) int64 {
    return a+b
}

func main() {
        println("hello")
}
</code></pre>
<p>我们将x.go编译为x.wasm文件：</p>
<pre><code>$GOARCH=wasm GOOS=wasip1 gotip build -buildmode=c-shared -o x.wasm ./testprog
</code></pre>
<p>然后在w.go中使用x.wasm中的Add函数：</p>
<pre><code>// go1.24-foresight/wasmtest/w.go

package main
import (
    "context"
    "fmt"
    "os"
    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/api"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func main() {
    ctx := context.Background()
    r := wazero.NewRuntime(ctx)
    defer r.Close(ctx)
    buf, err := os.ReadFile(os.Args[1])
    if err != nil {
        panic(err)
    }
    config := wazero.NewModuleConfig().
        WithStdout(os.Stdout).WithStderr(os.Stderr).
        WithStartFunctions() // don't call _start
    wasi_snapshot_preview1.MustInstantiate(ctx, r)
    m, err := r.InstantiateWithConfig(ctx, buf, config)
    if err != nil {
        panic(err)
    }

    // get export functions from the module
    F := func(a int64, b int64) int64 {
        exp := m.ExportedFunction("Add")
        r, err := exp.Call(ctx, api.EncodeI64(a), api.EncodeI64(b))
        if err != nil {
            panic(err)
        }
            rr := int64(r[0])
                fmt.Printf("host: Add %d + %d = %d\n", a,b,rr)
                return rr
    }

    // Library mode.
    entry := m.ExportedFunction("_initialize")
    fmt.Println("Library mode: initialize")
    _, err = entry.Call(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println("\nLibrary mode: call export functions")
    println(F(5,6))
}
</code></pre>
<p>运行上述w.go，我们将得到以下预期结果：</p>
<pre><code>$gotip run w.go ./x.wasm
Library mode: initialize
init function called

Library mode: call export functions
host: Add 5 + 6 = 11
11
</code></pre>
<h3>3.2 移植(porting)</h3>
<ul>
<li>Linux：要求内核版本不低于3.2。</li>
<li>macOS：Go 1.24是支持macOS 11 Big Sur的最后一个版本。</li>
<li>Windows：提升对Nano Server和内置服务帐户的支持，并修复域环境中的性能问题。</li>
<li>支持的Unicode版本升级到15.1.0。</li>
</ul>
<h2>4. 小结</h2>
<p>本文详细介绍了即将发布的Go 1.24版本在工具链和标准库方面的重要新特性。这些新特性不仅简化了工具的使用，提升了开发体验，还增强了标准库的功能和安全性，特别是在加密、并发测试等方面。通过这些改进，Go语言将继续朝着更高效、更安全、更易用的方向发展。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.24-foresight">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/milestone/322">Go 1.24 milestone</a> &#8211; https://github.com/golang/go/milestone/322</li>
<li><a href="https://tip.golang.org/doc/go1.24">Go 1.24 Release Notes Draft</a> &#8211; https://tip.golang.org/doc/go1.24</li>
<li><a href="https://dev.golang.org/release">Go Release Dashboard</a> &#8211; https://dev.golang.org/release</li>
<li><a href="https://tip.golang.org/ref/spec">Go spec tip</a> &#8211; https://tip.golang.org/ref/spec</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/17/go-1-24-foresight-part2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
