小雷蕾 發表於 2025-12-22 10:03:01

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. 通俗定义:自带&ldquo;背包&rdquo;的函数</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">情况一:调用两次&ldquo;工厂函数&rdquo; (你的&nbsp;main&nbsp;函数里的情况)</a></li><li><a href="#_lab2_5_4">情况二:调用同一个&ldquo;闭包实例&rdquo;多次</a></li><li><a href="#_lab2_5_5">用那个计数器的例子看最清楚</a></li><li><a href="#_lab2_5_6">总结你的&nbsp;makeHandler&nbsp;代码</a></li></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>前言</h2>
<p>&ldquo;闭包&rdquo;(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. 通俗定义:自带&ldquo;背包&rdquo;的函数</h2>
<p>通常情况下,一个函数执行完,它内部定义的变量就会被销毁(从内存栈中弹出)。</p>
<p>但是闭包不一样。闭包是一个特殊的函数,它被创建的时候,会偷偷把当时它能看到的变量抓取过来,装进自己的&ldquo;背包&rdquo;里带走。</p>
<p><strong>公式:</strong></p>
<blockquote><p><strong>闭包 = 函数代码 + 捕获的外部变量</strong></p></blockquote>
<p>哪怕创造它的那个外部函数已经执行结束了,闭包依然能通过&ldquo;背包&rdquo;访问和修改那些变量。</p>
<p class="maodian"><a name="_label2"></a></p><h2>2. 代码演示:最经典的计数器</h2>
<p>这是理解闭包的&ldquo;Hello World&rdquo;例子。</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>&nbsp;注意看&nbsp;<code>i</code>。它定义在&nbsp;<code>seq</code>&nbsp;里,但在&nbsp;<code>main</code>&nbsp;里通过&nbsp;<code>nextNum()</code>&nbsp;依然能一直修改它。<code>nextNum</code>&nbsp;这个函数把&nbsp;<code>i</code>&nbsp;&ldquo;<strong>封闭</strong>&rdquo;在自己的作用域里了,这就叫闭包。</p>
<p class="maodian"><a name="_label3"></a></p><h2>3. 底层原理:变量去哪了?(CS 专业向)</h2>
<p>作为一个计算机专业的学生,你可能会问:&ldquo;栈上的局部变量不是函数返回就销毁了吗?为什么&nbsp;<code>i</code>&nbsp;还能活下来?&rdquo;</p>
<p>这里涉及到了 Go 编译器的<strong>逃逸分析 (Escape Analysis)</strong>。</p>
<ul><li><p><strong>正常情况</strong>:函数内部的变量分配在<strong>栈 (Stack)</strong>&nbsp;上,函数结束,栈帧弹出,数据销毁。</p></li><li><p><strong>闭包情况</strong>:编译器扫描代码,发现变量&nbsp;<code>i</code>&nbsp;被内部的匿名函数引用了,而且这个匿名函数被返回到了外部。</p></li><li><p><strong>逃逸</strong>:编译器判定&nbsp;<code>i</code>&nbsp;不能死在栈上,于是把它分配到了<strong>堆 (Heap)</strong>&nbsp;上。</p></li><li><p><strong>引用</strong>:返回的闭包函数(<code>nextNum</code>)内部持有一个指针,指向堆上的这个&nbsp;<code>i</code>。</p></li></ul>
<p>所以,当你调用&nbsp;<code>makeHandler(viewHandler)</code>&nbsp;时,那层&ldquo;中间件&rdquo;之所以能记住&nbsp;<code>viewHandler</code>&nbsp;是谁,就是因为&nbsp;<code>fn</code>&nbsp;这个变量逃逸到了堆上,被返回的那个匿名函数死死抓在手里。</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>不需要专门写一个&nbsp;<code>struct</code>&nbsp;或者&nbsp;<code>class</code>&nbsp;来存状态,直接用闭包。上面的计数器就是例子。</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>&nbsp;你想给原本的函数&nbsp;<code>viewHandler</code>&nbsp;加一层&ldquo;安检&rdquo;。</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 &lt; 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>每次调用&nbsp;makeHandler(工厂),都会产生一个新的、独立的闭包环境。它们互不干扰,不引用同一个变量。</strong></p>
<p>为了让你彻底明白,我们要分两种情况来看:</p>
<p class="maodian"><a name="_lab2_5_3"></a></p><h3>情况一:调用两次&ldquo;工厂函数&rdquo; (你的&nbsp;main&nbsp;函数里的情况)</h3>
<p>这是你在&nbsp;<code>main</code>&nbsp;函数里写的代码逻辑:</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>&nbsp;<strong>它们不引用同一个变量。</strong></p>
<p><strong>原理:</strong>&nbsp;每次你调用&nbsp;<code>makeHandler</code>&nbsp;时,Go 都会在内存中开辟一个新的<strong>栈帧 (Stack Frame)</strong>。</p>
<ul><li><p>第一次调用时,参数&nbsp;<code>fn</code>&nbsp;是&nbsp;<code>viewHandler</code>。这个&nbsp;<code>fn</code>&nbsp;也就是闭包 A &ldquo;背包&rdquo;里的东西。</p></li><li><p>第二次调用时,参数&nbsp;<code>fn</code>&nbsp;是&nbsp;<code>editHandler</code>。这个&nbsp;<code>fn</code>&nbsp;是闭包 B &ldquo;背包&rdquo;里的东西。</p></li></ul>
<p>虽然变量名都叫&nbsp;<code>fn</code>,但它们在内存里是<strong>两块完全不同的地址</strong>。就好比你去了两次肯德基,第一次买了汉堡,第二次买了薯条。虽然都装在&ldquo;打包袋&rdquo;里,但这是两个不同的袋子,装的东西也不一样。</p>
<p class="maodian"><a name="_lab2_5_4"></a></p><h3>情况二:调用同一个&ldquo;闭包实例&rdquo;多次</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>&nbsp;<strong>它们引用的是同一个外部变量。</strong></p>
<p><strong>原理:</strong>&nbsp;因为&nbsp;<code>viewHandlerWrapper</code>&nbsp;是<strong>同一个</strong>闭包实例。 在这个例子里,它们引用的那个&ldquo;外部变量&rdquo;就是被捕获的&nbsp;<code>fn</code>&nbsp;(也就是&nbsp;<code>viewHandler</code>)。 无论用户调用多少次&nbsp;<code>viewHandlerWrapper</code>,它都会去自己的背包里找那个&nbsp;<code>fn</code>。这也是正确的,因为我们希望大家用的处理逻辑是一样的。</p>
<p class="maodian"><a name="_lab2_5_5"></a></p><h3>用那个计数器的例子看最清楚</h3>
<p>回到刚才的计数器&nbsp;<code>seq</code>,这能最直观地展示&ldquo;变量隔离&rdquo;:</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>总结你的&nbsp;makeHandler&nbsp;代码</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>&nbsp;创建了一个闭包,它<strong>独占</strong>&nbsp;<code>viewHandler</code>。</p></li><li><p><code>makeHandler(editHandler)</code>&nbsp;创建了另一个闭包,它<strong>独占</strong>&nbsp;<code>editHandler</code>。</p></li></ul>
<p><strong>它们互不干扰,绝对安全。</strong>&nbsp;这也是为什么闭包在 Go 的并发编程和中间件设计中如此好用的原因&mdash;&mdash;它天然地实现了状态的隔离。</p>
頁: [1]
查看完整版本: Go中的闭包函数Closure示例详解