<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 6 章 函数、延迟与恐慌 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/</link><description>Recent content in 第 6 章 函数、延迟与恐慌 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/index.xml" rel="self" type="application/rss+xml"/><item><title>6.1 函数调用</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/func/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/func/</guid><description>&lt;h1 id="61-函数调用"&gt;6.1 函数调用&lt;/h1&gt;
&lt;p&gt;函数是 Go 的一等公民，可以赋值、传递、返回、捕获外部变量成为闭包。这背后是两件事的实现：
函数值（闭包）在内存里如何表示，以及一次函数调用在底层如何传参与返回。后者还藏着 Go 1.17
一次悄无声息却影响全局的变革：从栈传参改为寄存器传参。这一节把这两件事讲透，并在它们的接缝处
点出一个关键事实，正是这个事实把「闭包是什么」与「调用怎么发生」焊成同一件事。&lt;/p&gt;
&lt;h2 id="611-函数值与闭包"&gt;6.1.1 函数值与闭包&lt;/h2&gt;
&lt;p&gt;一个 &lt;code&gt;func&lt;/code&gt; 类型的值，本质是一个指向&lt;strong&gt;闭包对象&lt;/strong&gt;的指针。运行时给这个对象起的名字是 &lt;code&gt;funcval&lt;/code&gt;，
它在 &lt;code&gt;runtime/runtime2.go&lt;/code&gt; 里的定义短得出奇：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// funcval：函数值在运行时的表示（来自 runtime2.go，已加注释）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; funcval &lt;span style="color:#00f"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fn &lt;span style="color:#2b91af"&gt;uintptr&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 后面紧跟变长的、与具体函数相关的数据，即被捕获的环境&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;只有一个固定字段 &lt;code&gt;fn&lt;/code&gt;,函数代码的入口地址。注释里那句「variable-size, fn-specific data here」
才是闭包的灵魂：紧随 &lt;code&gt;fn&lt;/code&gt; 之后，编译器排布着这个闭包捕获的外部变量。一个不捕获任何变量的
普通函数，它的 &lt;code&gt;funcval&lt;/code&gt; 只有 &lt;code&gt;fn&lt;/code&gt; 一字，全局唯一、只读；一旦它捕获了外部变量，编译器就要为每次
求值在堆上构造一个带捕获数据的 &lt;code&gt;funcval&lt;/code&gt;。可以把它画成这样：&lt;/p&gt;

&lt;pre class="mermaid"&gt;flowchart LR
 FV[&amp;#34;func 值&amp;lt;br/&amp;gt;（一个指针）&amp;#34;] --&amp;gt; CO
 subgraph CO[&amp;#34;闭包对象 funcval&amp;#34;]
 direction TB
 FN[&amp;#34;fn：函数代码入口地址&amp;#34;]
 CAP[&amp;#34;captured：被捕获的变量&amp;lt;br/&amp;gt;（如 &amp;amp;x, &amp;amp;y，按引用）&amp;#34;]
 FN --- CAP
 end
 CALL[&amp;#34;调用 f()&amp;#34;] -.-&amp;gt;|取 fn 字段间接跳转| FN&lt;/pre&gt;
&lt;p&gt;调用一个函数值，就是取出 &lt;code&gt;fn&lt;/code&gt; 字段间接跳转过去，与接口的方法分发
（&lt;a href="../../ch04type/interface"&gt;4.2&lt;/a&gt;）异曲同工。关键细节是&lt;strong&gt;捕获按引用&lt;/strong&gt;：闭包捕获的是变量本身，
不是它当时的值。下面这段代码里，&lt;code&gt;counter&lt;/code&gt; 返回的两个闭包共享同一个 &lt;code&gt;n&lt;/code&gt;，因为它们捕获的是
&lt;code&gt;n&lt;/code&gt; 这个变量的地址，而非某个快照：&lt;/p&gt;</description></item><item><title>6.2 延迟语句</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/defer/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/defer/</guid><description>&lt;h1 id="62-延迟语句"&gt;6.2 延迟语句&lt;/h1&gt;
&lt;p&gt;延迟语句 &lt;code&gt;defer&lt;/code&gt; 在最早期的 Go 设计中并不存在，是后来才单独增补的特性，由 Robert Griesemer
完成语言规范的编写 [Griesemer, 2009]，Ken Thompson 完成最早期的实现 [Thompson, 2009]。
它的语义读起来很短：被 &lt;code&gt;defer&lt;/code&gt; 的调用会在外层函数返回、发生 panic、或调用 &lt;code&gt;runtime.Goexit&lt;/code&gt;
时执行。直觉上这像是一个纯编译期特性，类似 C++ 的 RAII（离开作用域自动析构），编译器似乎只需把
延迟的调用「搬」到函数末尾即可，不该有运行时开销。&lt;/p&gt;
&lt;p&gt;真实情况要复杂一层。Go 的 &lt;code&gt;defer&lt;/code&gt; 没有与某个资源的作用域绑定，还允许出现在条件、循环里，于是
它不再是一个静态的作用域概念。一段执行次数取决于运行时的循环，能产生多少个延迟调用在编译期无法
确定：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; randomDefers() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#00f"&gt;for&lt;/span&gt; rand.Intn(100) &amp;gt; 42 {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#00f"&gt;defer&lt;/span&gt; println(&lt;span style="color:#a31515"&gt;&amp;#34;golang-design/under-the-hood&amp;#34;&lt;/span&gt;) &lt;span style="color:#008000"&gt;// 次数运行时才定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;正因为存在这种不确定性，&lt;code&gt;defer&lt;/code&gt; 不是免费的午餐。本节先讲清它的语义，再沿着一条贯穿十余年的
性能演进主线，看它如何从「最慢时被劝退出热路径」一路优化到「几乎零成本」。这条主线的终点，也是
理解 &lt;a href=".././panic"&gt;6.3 恐慌与恢复&lt;/a&gt; 的钥匙：在今天的运行时里，正常返回时跑 &lt;code&gt;defer&lt;/code&gt; 与 panic 展开
时跑 &lt;code&gt;defer&lt;/code&gt;，走的是同一套机器。&lt;/p&gt;</description></item><item><title>6.3 恐慌与恢复内建函数</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/panic/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part2lang/ch06func/panic/</guid><description>&lt;h1 id="63-恐慌与恢复内建函数"&gt;6.3 恐慌与恢复内建函数&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;defer&lt;/code&gt;（&lt;a href=".././defer"&gt;6.2&lt;/a&gt;）已经把「函数退出时要做什么」交代清楚，留下的悬念是「函数被
异常地打断时又会发生什么」。&lt;code&gt;panic&lt;/code&gt; 与 &lt;code&gt;recover&lt;/code&gt; 这对内建函数回答的正是这个问题：前者中断
当前的正常控制流并开始沿调用栈向上展开，后者在展开途中把它截住。这一节先讲清它们的语义，
再落到 go1.26 运行时里 &lt;code&gt;gopanic&lt;/code&gt;、&lt;code&gt;gorecover&lt;/code&gt; 的实现，最后回到一个更要紧的问题：panic
究竟该被当作什么，以及为什么 Go 没有把它做成 C++ 或 Java 那样的异常机制。&lt;/p&gt;
&lt;h2 id="631-语义展开拦截与进程终止"&gt;6.3.1 语义：展开、拦截与进程终止&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;panic&lt;/code&gt; 做三件事，顺序固定。它先停下当前函数余下的语句，转而按后进先出的次序执行当前
goroutine 已经登记的 &lt;code&gt;defer&lt;/code&gt; 链；每执行一个延迟函数，控制权都短暂回到用户代码，给它一次
拦截的机会；若整条链走完仍无人拦截，panic 会逐层弹出调用栈，到达 goroutine 栈顶后终止
&lt;strong&gt;整个进程&lt;/strong&gt;，并打印 panic 值与栈回溯。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;recover&lt;/code&gt; 是拦截的唯一手段，但它的生效条件很窄：只有&lt;strong&gt;直接&lt;/strong&gt;被某个延迟函数调用时才有效。
直接的意思是，调用 &lt;code&gt;recover&lt;/code&gt; 的那一帧，必须正是发生 panic 的那一帧用 &lt;code&gt;defer&lt;/code&gt; 登记的延迟
函数本身，多一层嵌套或少一层都不算。一次成功的 &lt;code&gt;recover&lt;/code&gt; 会让 panic 停止展开，返回当时
传给 &lt;code&gt;panic&lt;/code&gt; 的值，随后控制流如同那个延迟函数正常返回一般，继续往下走。&lt;/p&gt;
&lt;p&gt;这条「同一调用链、且直接位于延迟函数中」的约束，是后文一切实现细节的根。先用三段代码把它
落实。第一段，&lt;code&gt;recover&lt;/code&gt; 写错了位置，拦不住：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; A() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	B()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	C()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; B() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#00f"&gt;defer&lt;/span&gt; &lt;span style="color:#00f"&gt;func&lt;/span&gt;() { recover() }() &lt;span style="color:#008000"&gt;// 拦不住 C 的 panic：B 已经返回，不在展开路径上&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	println(&lt;span style="color:#a31515"&gt;&amp;#34;B&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; C() { panic(&lt;span style="color:#a31515"&gt;&amp;#34;boom&amp;#34;&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;A&lt;/code&gt; 先调用 &lt;code&gt;B&lt;/code&gt;，&lt;code&gt;B&lt;/code&gt; 正常返回、它的 &lt;code&gt;defer&lt;/code&gt; 也早已执行完毕；随后 &lt;code&gt;A&lt;/code&gt; 调用 &lt;code&gt;C&lt;/code&gt;，&lt;code&gt;C&lt;/code&gt; 发生
panic 时展开的是 &lt;code&gt;C → A&lt;/code&gt; 这条路径，&lt;code&gt;B&lt;/code&gt; 根本不在其上。第二段，把 &lt;code&gt;recover&lt;/code&gt; 放到调用链上游
的 &lt;code&gt;A&lt;/code&gt; 里，就拦得住：&lt;/p&gt;</description></item></channel></rss>