返回顶部
分享到

Go中的闭包函数Closure示例

Golang 来源:互联网 作者:佚名 发布时间:2025-12-22 20:56:57 人浏览
摘要

闭包(Closure)是编程中一个非常重要、但初学者容易晕的概念。它在函数式编程(Functional Programming)中无处不在,Go 语言对它的支持非常强大。 简单来说,闭包就是:一个函数和它周围环境

“闭包”(Closure)是编程中一个非常重要、但初学者容易晕的概念。它在函数式编程(Functional Programming)中无处不在,Go 语言对它的支持非常强大。

简单来说,闭包就是:一个函数和它周围环境的绑定。

为了让你彻底理解,我们从三个层面来拆解:通俗定义、代码演示、以及底层原理。

1. 通俗定义:自带“背包”的函数

通常情况下,一个函数执行完,它内部定义的变量就会被销毁(从内存栈中弹出)。

但是闭包不一样。闭包是一个特殊的函数,它被创建的时候,会偷偷把当时它能看到的变量抓取过来,装进自己的“背包”里带走。

公式:

闭包 = 函数代码 + 捕获的外部变量

哪怕创造它的那个外部函数已经执行结束了,闭包依然能通过“背包”访问和修改那些变量。

2. 代码演示:最经典的计数器

这是理解闭包的“Hello World”例子。

Go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

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

}

为什么叫闭包? 注意看 i。它定义在 seq 里,但在 main 里通过 nextNum() 依然能一直修改它。nextNum 这个函数把 i “封闭”在自己的作用域里了,这就叫闭包。

3. 底层原理:变量去哪了?(CS 专业向)

作为一个计算机专业的学生,你可能会问:“栈上的局部变量不是函数返回就销毁了吗?为什么 i 还能活下来?”

这里涉及到了 Go 编译器的逃逸分析 (Escape Analysis)。

  • 正常情况:函数内部的变量分配在栈 (Stack) 上,函数结束,栈帧弹出,数据销毁。

  • 闭包情况:编译器扫描代码,发现变量 i 被内部的匿名函数引用了,而且这个匿名函数被返回到了外部。

  • 逃逸:编译器判定 i 不能死在栈上,于是把它分配到了堆 (Heap) 上。

  • 引用:返回的闭包函数(nextNum)内部持有一个指针,指向堆上的这个 i。

所以,当你调用 makeHandler(viewHandler) 时,那层“中间件”之所以能记住 viewHandler 是谁,就是因为 fn 这个变量逃逸到了堆上,被返回的那个匿名函数死死抓在手里。

4. 闭包在实战中的三大用途

回到你的 HTTP 题目,以及以后的开发中,闭包主要有这就三种用法:

A. 封装状态(像简化的类)

不需要专门写一个 struct 或者 class 来存状态,直接用闭包。上面的计数器就是例子。

B. 延迟执行 / 回调 (Callback)

当你把一个函数 A 传给另一个函数 B,B 在未来某个时间点调用 A,A 还能记得当初传给它的数据。

C. 中间件 / 装饰器 (Middleware / Decorator)

这就是你那段代码的本质。 你想给原本的函数 viewHandler 加一层“安检”。

  • 输入:一个旧函数。

  • 输出:一个新函数(新函数 = 安检逻辑 + 旧函数)。

Go

1

2

3

4

5

6

7

8

9

10

11

12

// 这就是一个典型的装饰器模式(Decorator Pattern)

func makeHandler(fn ...) http.HandlerFunc {

    return func(...) {

        // 1. 前置逻辑 (Pre-processing)

        // ... 验证 title ...

         

        // 2. 调用原函数 (Original logic)

        fn(...)

         

        // 3. 后置逻辑 (Post-processing) - 可选

    }

}

总结

  • 闭包就是能记住它出生环境的函数。

  • 在 Go 里,闭包让局部变量逃逸到堆上,从而延长了变量的生命周期。

  • 在很多场景下(比如你的 Web 路由处理),用闭包比定义一个 Interface 或者 Struct 更轻量、更优雅。

看一段代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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),

        )

    }

}

输出结果:

0 0
1 -2
3 -6
6 -12
10 -20
15 -30

每次调用 makeHandler(工厂),都会产生一个新的、独立的闭包环境。它们互不干扰,不引用同一个变量。

为了让你彻底明白,我们要分两种情况来看:

情况一:调用两次“工厂函数” (你的 main 函数里的情况)

这是你在 main 函数里写的代码逻辑:

Go

1

2

3

4

5

6

7

// 第 1 次调用 makeHandler

// 产生了一个闭包 A,它的背包里装的是 viewHandler

h1 := makeHandler(viewHandler)

 

// 第 2 次调用 makeHandler

// 产生了一个闭包 B,它的背包里装的是 editHandler

h2 := makeHandler(editHandler)

结论: 它们不引用同一个变量。

原理: 每次你调用 makeHandler 时,Go 都会在内存中开辟一个新的栈帧 (Stack Frame)。

  • 第一次调用时,参数 fn 是 viewHandler。这个 fn 也就是闭包 A “背包”里的东西。

  • 第二次调用时,参数 fn 是 editHandler。这个 fn 是闭包 B “背包”里的东西。

虽然变量名都叫 fn,但它们在内存里是两块完全不同的地址。就好比你去了两次肯德基,第一次买了汉堡,第二次买了薯条。虽然都装在“打包袋”里,但这是两个不同的袋子,装的东西也不一样。

情况二:调用同一个“闭包实例”多次

这是当服务器运行起来,多个用户访问同一个 URL 时的情况:

Go

1

2

3

4

5

6

7

8

// 假设这是 main 函数里注册好的那个 handler

viewHandlerWrapper := makeHandler(viewHandler)

 

// 用户甲访问 /view/1

viewHandlerWrapper(w1, r1)

 

// 用户乙访问 /view/2

viewHandlerWrapper(w2, r2)

结论: 它们引用的是同一个外部变量。

原理: 因为 viewHandlerWrapper 是同一个闭包实例。 在这个例子里,它们引用的那个“外部变量”就是被捕获的 fn (也就是 viewHandler)。 无论用户调用多少次 viewHandlerWrapper,它都会去自己的背包里找那个 fn。这也是正确的,因为我们希望大家用的处理逻辑是一样的。

用那个计数器的例子看最清楚

回到刚才的计数器 seq,这能最直观地展示“变量隔离”:

Go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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 影响!

}

总结你的 makeHandler 代码

在你的代码中:

Go

1

2

3

4

5

6

func makeHandler(fn func(...)) http.HandlerFunc {

    return func(w, r) {

        // ...

        fn(w, r, title) // 这里的 fn 是被捕获的

    }

}

  • makeHandler(viewHandler) 创建了一个闭包,它独占 viewHandler。

  • makeHandler(editHandler) 创建了另一个闭包,它独占 editHandler。

它们互不干扰,绝对安全。 这也是为什么闭包在 Go 的并发编程和中间件设计中如此好用的原因——它天然地实现了状态的隔离。


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • Go中的闭包函数Closure示例
    闭包(Closure)是编程中一个非常重要、但初学者容易晕的概念。它在函数式编程(Functional Programming)中无处不在,Go 语言对它的支持非常强
  • go语言中regexp正则表达式的操作
    Go 语言的regexp包提供了对正则表达式的支持。 正则表达式(regex)是一种字符串搜索模式,用来检查一个字符串是否符合某种特定的模式,
  • Go实现完全静态编译和交叉编译的代码
    Go 语言天生支持跨平台编译,并且其标准库几乎不依赖系统动态库,所以在大多数场景下,它编译出来的二进制文件几乎可以直接丢到任何
  • Go语言编译环境设置教程

    Go语言编译环境设置教程
    Go语言优势 天生支持高并发 可以自由的去控制其并发量,也就是携程,通过go routine关键字就行了。 自动垃圾回收机制 内存的清理 不需要环
  • Go fmt包中Scan获取标准输入方式
    Go fmt包下有三个函数 可以在程序运行过程中获取用户输入。 fmt.Scan:获取输入 fmt.Scanf:获取输入,但是可以指定格式,go会根据格式解析参
  • go中空接口的具体使用
    接口-空接口 1. 什么是空接口? 空接口是特殊形式的接口类型,普通的接口都有方法,而空接口没有定义任何方法口,也因此,我们可以说
  • 快速解除oracle dataguard的方法

    快速解除oracle dataguard的方法
    有些时候,我们为了使oracle dg的standby库另做他用,需要解除oracle dataguard数据同步。我本地因为standby库存储出现故障,导致dg存在问题,故需
  • Go 1.23中Timer无buffer的实现方式介绍
    在 Go 1.23 中,Timer 的实现通常是通过 time 包提供的 time.Timer 类型来实现的。Timer 是一个用于在指定时间后触发一次事件的计时器。Timer 的实
  • golang之 wire 库的使用介绍
    1. 写在最前面 之前 review 其他人的代码的时候,看到了关于 wire 库的使用。但当时主要是分析逻辑上的问题,没怎么太学习 wire 库的用法,
  • golang panic 函数用法介绍
    在 Go 语言中,panic和recover是用于处理运行时异常的关键字。以下是它们的用法总结和示例: 1. panic 的作用 触发条件:当程序遇到无法恢复
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计