Go中的闭包函数Closure示例详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">前言</a></li><li><a href="#_label1">1. 通俗定义:自带“背包”的函数</a></li><li><a href="#_label2">2. 代码演示:最经典的计数器</a></li><li><a href="#_label3">3. 底层原理:变量去哪了?(CS 专业向)</a></li><li><a href="#_label4">4. 闭包在实战中的三大用途</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_0">A. 封装状态(像简化的类)</a></li><li><a href="#_lab2_4_1">B. 延迟执行 / 回调 (Callback)</a></li><li><a href="#_lab2_4_2">C. 中间件 / 装饰器 (Middleware / Decorator)</a></li></ul><li><a href="#_label5">总结</a></li><ul class="second_class_ul"><li><a href="#_lab2_5_3">情况一:调用两次“工厂函数” (你的 main 函数里的情况)</a></li><li><a href="#_lab2_5_4">情况二:调用同一个“闭包实例”多次</a></li><li><a href="#_lab2_5_5">用那个计数器的例子看最清楚</a></li><li><a href="#_lab2_5_6">总结你的 makeHandler 代码</a></li></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>前言</h2><p>“闭包”(Closure)是编程中一个非常重要、但初学者容易晕的概念。它在函数式编程(Functional Programming)中无处不在,Go 语言对它的支持非常强大。</p>
<p>简单来说,闭包就是:<strong>一个函数和它周围环境的绑定。</strong></p>
<p>为了让你彻底理解,我们从三个层面来拆解:<strong>通俗定义</strong>、<strong>代码演示</strong>、以及<strong>底层原理</strong>。</p>
<p class="maodian"><a name="_label1"></a></p><h2>1. 通俗定义:自带“背包”的函数</h2>
<p>通常情况下,一个函数执行完,它内部定义的变量就会被销毁(从内存栈中弹出)。</p>
<p>但是闭包不一样。闭包是一个特殊的函数,它被创建的时候,会偷偷把当时它能看到的变量抓取过来,装进自己的“背包”里带走。</p>
<p><strong>公式:</strong></p>
<blockquote><p><strong>闭包 = 函数代码 + 捕获的外部变量</strong></p></blockquote>
<p>哪怕创造它的那个外部函数已经执行结束了,闭包依然能通过“背包”访问和修改那些变量。</p>
<p class="maodian"><a name="_label2"></a></p><h2>2. 代码演示:最经典的计数器</h2>
<p>这是理解闭包的“Hello World”例子。</p>
<p>Go</p>
<div class="jb51code"><pre class="brush:go;">package main
import "fmt"
// seq 此时是一个“工厂”,它返回一个函数
// 这个返回的函数返回 int
func seq() func() int {
i := 0 // 这是一个局部变量,按理说 seq 执行完它就该销毁了
// 返回一个匿名函数
return func() int {
i++ // 这个匿名函数引用了外部的 i
return i
}
}
func main() {
// 1. 创建第一个闭包实例 nextNum
// 此时 seq() 执行结束,但变量 i 被 nextNum 的背包抓走了
nextNum := seq()
fmt.Println(nextNum()) // 输出 1
fmt.Println(nextNum()) // 输出 2
fmt.Println(nextNum()) // 输出 3 (i 的状态被保留了!)
// 2. 创建第二个闭包实例 nextNum2
// 它会有自己全新的 i,和上面那个互不干扰
nextNum2 := seq()
fmt.Println(nextNum2()) // 输出 1
}
</pre></div>
<p><strong>为什么叫闭包?</strong> 注意看 <code>i</code>。它定义在 <code>seq</code> 里,但在 <code>main</code> 里通过 <code>nextNum()</code> 依然能一直修改它。<code>nextNum</code> 这个函数把 <code>i</code> “<strong>封闭</strong>”在自己的作用域里了,这就叫闭包。</p>
<p class="maodian"><a name="_label3"></a></p><h2>3. 底层原理:变量去哪了?(CS 专业向)</h2>
<p>作为一个计算机专业的学生,你可能会问:“栈上的局部变量不是函数返回就销毁了吗?为什么 <code>i</code> 还能活下来?”</p>
<p>这里涉及到了 Go 编译器的<strong>逃逸分析 (Escape Analysis)</strong>。</p>
<ul><li><p><strong>正常情况</strong>:函数内部的变量分配在<strong>栈 (Stack)</strong> 上,函数结束,栈帧弹出,数据销毁。</p></li><li><p><strong>闭包情况</strong>:编译器扫描代码,发现变量 <code>i</code> 被内部的匿名函数引用了,而且这个匿名函数被返回到了外部。</p></li><li><p><strong>逃逸</strong>:编译器判定 <code>i</code> 不能死在栈上,于是把它分配到了<strong>堆 (Heap)</strong> 上。</p></li><li><p><strong>引用</strong>:返回的闭包函数(<code>nextNum</code>)内部持有一个指针,指向堆上的这个 <code>i</code>。</p></li></ul>
<p>所以,当你调用 <code>makeHandler(viewHandler)</code> 时,那层“中间件”之所以能记住 <code>viewHandler</code> 是谁,就是因为 <code>fn</code> 这个变量逃逸到了堆上,被返回的那个匿名函数死死抓在手里。</p>
<p class="maodian"><a name="_label4"></a></p><h2>4. 闭包在实战中的三大用途</h2>
<p>回到你的 HTTP 题目,以及以后的开发中,闭包主要有这就三种用法:</p>
<p class="maodian"><a name="_lab2_4_0"></a></p><h3>A. 封装状态(像简化的类)</h3>
<p>不需要专门写一个 <code>struct</code> 或者 <code>class</code> 来存状态,直接用闭包。上面的计数器就是例子。</p>
<p class="maodian"><a name="_lab2_4_1"></a></p><h3>B. 延迟执行 / 回调 (Callback)</h3>
<p>当你把一个函数 A 传给另一个函数 B,B 在未来某个时间点调用 A,A 还能记得当初传给它的数据。</p>
<p class="maodian"><a name="_lab2_4_2"></a></p><h3>C. 中间件 / 装饰器 (Middleware / Decorator)</h3>
<p><strong>这就是你那段代码的本质。</strong> 你想给原本的函数 <code>viewHandler</code> 加一层“安检”。</p>
<ul><li><p><strong>输入</strong>:一个旧函数。</p></li><li><p><strong>输出</strong>:一个新函数(新函数 = 安检逻辑 + 旧函数)。</p></li></ul>
<p>Go</p>
<div class="jb51code"><pre class="brush:go;">// 这就是一个典型的装饰器模式(Decorator Pattern)
func makeHandler(fn ...) http.HandlerFunc {
return func(...) {
// 1. 前置逻辑 (Pre-processing)
// ... 验证 title ...
// 2. 调用原函数 (Original logic)
fn(...)
// 3. 后置逻辑 (Post-processing) - 可选
}
}
</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>总结</h2>
<ul><li><p><strong>闭包</strong>就是能记住它出生环境的函数。</p></li><li><p><strong>在 Go 里</strong>,闭包让局部变量逃逸到堆上,从而延长了变量的生命周期。</p></li><li><p><strong>在很多场景下</strong>(比如你的 Web 路由处理),用闭包比定义一个 Interface 或者 Struct 更轻量、更优雅。</p></li></ul>
<p>看一段代码:</p>
<div class="jb51code"><pre class="brush:go;">package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
</pre></div>
<p>输出结果:</p>
<blockquote><p>0 0<br />1 -2<br />3 -6<br />6 -12<br />10 -20<br />15 -30</p></blockquote>
<p><strong>每次调用 makeHandler(工厂),都会产生一个新的、独立的闭包环境。它们互不干扰,不引用同一个变量。</strong></p>
<p>为了让你彻底明白,我们要分两种情况来看:</p>
<p class="maodian"><a name="_lab2_5_3"></a></p><h3>情况一:调用两次“工厂函数” (你的 main 函数里的情况)</h3>
<p>这是你在 <code>main</code> 函数里写的代码逻辑:</p>
<p>Go</p>
<div class="jb51code"><pre class="brush:go;">// 第 1 次调用 makeHandler
// 产生了一个闭包 A,它的背包里装的是 viewHandler
h1 := makeHandler(viewHandler)
// 第 2 次调用 makeHandler
// 产生了一个闭包 B,它的背包里装的是 editHandler
h2 := makeHandler(editHandler)
</pre></div>
<p><strong>结论:</strong> <strong>它们不引用同一个变量。</strong></p>
<p><strong>原理:</strong> 每次你调用 <code>makeHandler</code> 时,Go 都会在内存中开辟一个新的<strong>栈帧 (Stack Frame)</strong>。</p>
<ul><li><p>第一次调用时,参数 <code>fn</code> 是 <code>viewHandler</code>。这个 <code>fn</code> 也就是闭包 A “背包”里的东西。</p></li><li><p>第二次调用时,参数 <code>fn</code> 是 <code>editHandler</code>。这个 <code>fn</code> 是闭包 B “背包”里的东西。</p></li></ul>
<p>虽然变量名都叫 <code>fn</code>,但它们在内存里是<strong>两块完全不同的地址</strong>。就好比你去了两次肯德基,第一次买了汉堡,第二次买了薯条。虽然都装在“打包袋”里,但这是两个不同的袋子,装的东西也不一样。</p>
<p class="maodian"><a name="_lab2_5_4"></a></p><h3>情况二:调用同一个“闭包实例”多次</h3>
<p>这是当服务器运行起来,多个用户访问同一个 URL 时的情况:</p>
<p>Go</p>
<div class="jb51code"><pre class="brush:go;">// 假设这是 main 函数里注册好的那个 handler
viewHandlerWrapper := makeHandler(viewHandler)
// 用户甲访问 /view/1
viewHandlerWrapper(w1, r1)
// 用户乙访问 /view/2
viewHandlerWrapper(w2, r2)
</pre></div>
<p><strong>结论:</strong> <strong>它们引用的是同一个外部变量。</strong></p>
<p><strong>原理:</strong> 因为 <code>viewHandlerWrapper</code> 是<strong>同一个</strong>闭包实例。 在这个例子里,它们引用的那个“外部变量”就是被捕获的 <code>fn</code> (也就是 <code>viewHandler</code>)。 无论用户调用多少次 <code>viewHandlerWrapper</code>,它都会去自己的背包里找那个 <code>fn</code>。这也是正确的,因为我们希望大家用的处理逻辑是一样的。</p>
<p class="maodian"><a name="_lab2_5_5"></a></p><h3>用那个计数器的例子看最清楚</h3>
<p>回到刚才的计数器 <code>seq</code>,这能最直观地展示“变量隔离”:</p>
<p>Go</p>
<div class="jb51code"><pre class="brush:go;">func seq() func() int {
i := 0 // 这就是那个“外部变量”
return func() int {
i++
return i
}
}
func main() {
// 【工厂调用两次】:创造了两个独立的世界
counterA := seq()
counterB := seq()
// counterA 的背包里有一个 i (地址 0x1111)
// counterB 的背包里有一个 i (地址 0x2222)
fmt.Println(counterA()) // 输出 1 (修改的是 0x1111)
fmt.Println(counterA()) // 输出 2 (修改的是 0x1111)
// 关键点来了:
fmt.Println(counterB()) // 输出 1 (修改的是 0x2222)
// 发现了吗?counterB 从头开始计数,完全没受 counterA 影响!
}
</pre></div>
<p class="maodian"><a name="_lab2_5_6"></a></p><h3>总结你的 makeHandler 代码</h3>
<p>在你的代码中:</p>
<p>Go</p>
<div class="jb51code"><pre class="brush:go;">func makeHandler(fn func(...)) http.HandlerFunc {
return func(w, r) {
// ...
fn(w, r, title) // 这里的 fn 是被捕获的
}
}
</pre></div>
<ul><li><p><code>makeHandler(viewHandler)</code> 创建了一个闭包,它<strong>独占</strong> <code>viewHandler</code>。</p></li><li><p><code>makeHandler(editHandler)</code> 创建了另一个闭包,它<strong>独占</strong> <code>editHandler</code>。</p></li></ul>
<p><strong>它们互不干扰,绝对安全。</strong> 这也是为什么闭包在 Go 的并发编程和中间件设计中如此好用的原因——它天然地实现了状态的隔离。</p>
頁:
[1]