前言

入坑 Go 语言已经大半年了,却没有写过一篇像样的技术文章,每次写一半就搁笔,然后就烂尾了。

几经思考,痛定思痛,决定金盆洗手,重新做人,哦不,重新开始写技术博文。

这段时间在研究Go语言闭包的过程中,发现了很多有意思的东西,也学到了不少内容,于是便以次为契机,重新开始技术文章的输出。

什么是闭包

闭包Go 语言中一个重要特性,也是 函数式编程 中必不可少的角色。那么什么是 闭包 呢?

A closure is a function value that references variables from outside its body.

这是 A Tour of Go 上的定义,闭包 是一种引用了外部变量的函数。但我觉得这个定义还不够准确,闭包 应该是引用了外部变量的 匿名函数

看了很多文章,大多把 闭包匿名函数混淆在了一起,也有很多人说,闭包 其实就是匿名函数,但其实两者是不能直接划等号的。

闭包 是一种特殊的匿名函数,是匿名函数的子集。所以在说 闭包 之前,我们先来看看 匿名函数 吧。

匿名函数

匿名函数 顾名思义,就是没有名字的函数。在Go语言中,函数是一等公民,也就是说,函数可以被赋值或者当作返回值和参数进行传递,在很多时候我们并不需要一个有名字的函数(而且命名确实是一项相当费劲的事),所以我们在某些场景下可以选择使用 匿名函数

举个例子:

func main(){
hello := func(){
fmt.Println("Hello World")
}
hello()
}

这是一个简单的例子,我们声明了一个 匿名函数 ,然后把它赋值给一个叫 hello 的变量,然后我们就能像调用函数那样使用它了。

这跟下面的代码效果是一样的:

func main(){
hello()
} func hello(){
fmt.Println("Hello World")
}

我们还可以把 匿名函数 当作函数参数进行传递:

func main(){
doPrint("Hello World", func(s string){
fmt.Println(s)
})
} type Printer func(string) func doPrint(s string, printer Printer){
printer(s)
}

或者当作函数返回值进行返回:

func main(){
getPrinter()("Hello World")
} type Printer func(string) func getPrinter()Printer{
return func(s string){
fmt.Println(s)
}
}

匿名函数 跟普通函数在绝大多数场景下没什么区别,普通函数的函数名可以当作是与该函数绑定的函数常量。

一个函数主要包含两个信息:函数签名和函数体,函数的签名包括参数类型,返回值的类型,函数签名可以看做是函数的类型,函数的函数体即函数的值。所以一个接收匿名函数的变量的类型便是由函数的签名决定的,一个匿名函数被赋值给一个变量后,这个变量便只能接收同样签名的函数。

func main(){
hello := func(){
fmt.Println("Hello World")
} // 给 hello 变量赋值一个匿名函数
hello() hello = func(){
fmt.Println("Hello World2")
} // 重新赋值新的匿名函数
hello() hello = hi // 将一个普通函数赋值给 hello
hello() hello = func(int){
fmt.Println("Hello World3")
} // 这里编译器会报错
hello()
} func hi(){
fmt.Println("Hi")
}

匿名函数 跟普通函数的微小区别在于 匿名函数 赋值的变量可以重新设置新的 匿名函数,但普通函数的函数名是与特定函数绑定的,无法再将其它函数赋值给它。这就类似于变量与常量之间的区别。

闭包的特性

说完了 匿名函数,我们再回过头来看看 闭包

闭包 是指由一个拥有许多变量和绑定了这些变量的环境的 匿名函数

闭包 = 函数 + 引用环境

听起来有点绕,什么是 引用环境呢?

引用环境 是指在程序执行中的某个点所有处于活跃状态的变量所组成的集合。

由于闭包把函数和运行时的引用环境打包成为一个新的整体,所以就解决了函数编程中的嵌套所引发的问题。

当每次调用包含闭包的函数时都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。不同于函数,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

简单来说,闭包 就是引用了外部变量的匿名函数。不太明白?没关系,让我们先来看一个栗子:

func adder() func() int {
var i = 0
return func() int {
i++
return i
}
}

这是用闭包实现的简单累加器,这一部分便是闭包,它引用在其作用域范围之外的变量i。

func() int {
i++
return i
}

可以这样使用:

func main() {
a := adder()
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
b := adder()
fmt.Println(b())
fmt.Println(b())
}

输出如下:

1
2
3
4
1
2

上述例子中,adder 是一个函数,没有入参,返回值是一个返回 int 类型的无参函数,也就是说调用 adder 函数会返回一个函数,这个函数的返回值是 int 类型,且不接收参数。

main 方法中:

a := adder()

这里是将调用后得到的函数赋值给了变量 a ,随后进行了四次函数调用和输出:

fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())

也许你还是会感到困惑,iadder 函数里的变量,调用完成之后变量的生命周期不久结束了吗?为什么还能不断累加?

这就涉及到闭包的另一个重要话题了:闭包 会让被引用的局部变量从栈逃逸到堆上,从而使其能在其作用域范围之外存活。闭包 “捕获”了和它在同一作用域的其它常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,只要闭包还在使用它们,这些变量就还会存在。

匿名函数和闭包的使用

可以利用匿名函数闭包可以实现很多有意思的功能,比如上面的累加器,便是利用了 闭包 的作用域隔离特性,每调用一次 adder 函数,就会生成一个新的累加器,使用新的变量 i,所以在调用 b() 时,仍旧会从1开始输出。

再来看几个匿名函数闭包应用的例子。

工厂函数

工厂函数即生产函数的函数,调用工厂函数可以得到其内嵌函数的引用,每次调用都可以得到一个新的函数引用。

func getFibGen() func() int {
f1 := 0
f2 := 1
return func() int {
f2, f1 = f1 + f2, f2
return f1
}
} func main() {
gen := getFibGen()
for i := 0; i < 10; i++ {
fmt.Println(gen())
}
}

上面是利用闭包实现的函数工厂来求解斐波那契数列问题,调用 getFibGen 函数之后,gen 便获得了内嵌函数的引用,且该函数引用里一直持有 f1f2 的引用,每执行一次 gen(),便会运算一次斐波那契的递推关系式:

func() int {
f2, f1 = f1 + f2, f2
return f1
}

输出如下:

1
1
2
3
5
8
13
21
34
55

由于闭包能构造出单独的变量环境,可以很好的实现环境隔离,所以很适合应用于函数工厂,在实现功能时保存某些状态变量。

装饰器/中间件

修饰器是指在不改变对象的内部结构情况下,动态地扩展对象的功能。通过创建一个装饰器,来包装真实的对象。使用闭包很容易实现装饰器模式

在 gin 中的 Middleware 便是使用装饰器模式来实现的。比如我们可以这样实现一个自定义的 Logger:

func Logger() gin.HandlerFunc {
return func(context *gin.Context) {
host := context.Request.Host
url := context.Request.URL
method := context.Request.Method
fmt.Printf("%s::%s \t %s \t %s \n", time.Now().Format("2006-01-02 15:04:05"), host, url, method)
context.Next()
fmt.Println("response status: ", context.Writer.Status())
}
}

这是在 gin 中利用 匿名函数 实现的自定义日志中间件,在 gin 中,类似的用法十分常见。

defer

这是匿名函数闭包最常用的地方,我们会经常在 defer 函数中使用匿名函数闭包来做释放锁,关闭连接,处理 panic 等函数善后工作。

func main() {
defer func() {
if ok := recover(); ok != nil {
fmt.Println("recover from panic")
}
}() panic("error")
}

gorutine

匿名函数闭包还有一个十分常用的场景,那便是在启动 gorutine 时使用。

func main(){
go func(){
fmt.Println("Hello World")
}()
time.Sleep(1 * time.Second)
}

重新声明一下,在函数内部引用了外部变量便是闭包,否则就是匿名函数

func main(){
hello := "Hello World"
go func(){
fmt.Println(hello)
}()
time.Sleep(1 * time.Second)
}

context

在cancelContext中也使用到了闭包:

// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func() // WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
} // newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}

闭包的陷阱

闭包很好用,但在某些场景下,也十分具有欺骗性,稍有不慎,就会掉入其陷阱里。

不如先来看一个例子:

for j := 0; j < 2; j++ {
defer func() {
fmt.Println(j)
}()
}

你猜会输出什么?

2
2

这是因为在 defer 中使用的闭包引用了外部变量 j

闭包 中持有的是外部变量的引用

这是很容易犯的错误,在循环体中使用 defer,来关闭连接,释放资源,但由于闭包内持有的是外部变量的引用,在这里持有的是变量 j 的引用,defer 会在函数执行完成前调用闭包,在开始执行闭包时,j 的值已经是2了。

那么这个问题应该如何修复呢?有两种方式,一种是重新定义变量:

for j := 0; j < 2; j++ {
k := j
defer func() {
fmt.Println(k)
}()
}

在循环体里,每次循环都定义了一个新的变量 k 来获取原变量 j 的值,因此每次调用闭包时,引用的是不同的变量 k,从而达到变量隔离的效果。

另一种方式是把变量当成参数传入:

for j := 0; j < 2; j++ {
defer func(k int) {
fmt.Println(k)
}(j)
}

这里每次调用闭包时,传入的都是变量 j 的值,虽然 defer 仍会在函数执行完成前调用,但传入闭包的参数值却是先计算好的,因而能够正确输出。

闭包返回的包装对象是一个复合结构,里面包含匿名函数的地址,以及环境变量的地址。

为了更好的理解这一点,我们再来看一个例子:

package main

import "fmt"

func main() {
x, y := 1, 2 defer func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y)
}(x) x += 1
y += 1
fmt.Println(x, y)
}

输出如下:

2 3
x:1,y:3

另外,由于闭包会使得其持有的外部变量逃逸出原有的作用域,所以使用不当可能会造成内存泄漏,这一点由于相当具有隐蔽性,所以也需要谨慎对待。

总结

闭包是一种特殊的匿名函数,是由函数体和引用的外部变量一起组成,可以看成类似如下结构:

type FF struct {
F unitptr
A *int
B *int
X *int // 如果X是string/[]int,那么这里应该为*string,*[]int
}

在Go语言中,闭包的应用十分广泛,掌握了闭包的使用可以让你在写代码时能更加游刃有余,也可以避免很多不必要的麻烦。所以是必须要掌握的一个知识点。

至此,关于闭包的内容就完结了,希望能对你有帮助。

【Go语言学习】匿名函数与闭包的更多相关文章

  1. Python学习---匿名函数和闭包的学习

    1.1. 匿名函数 匿名函数的命名规则:  用lamdba 关键字标识,冒号(:)左侧表示函数接收的参数(a,b) ,冒号(:)右侧表示函数的返回值(a+b). 因为lamdba在创建时不需要命名,所 ...

  2. Golang之匿名函数和闭包

    Go语言支持匿名函数,即函数可以像普通变量一样被传递或使用. 使用方法如下: main.go package main import ( "fmt" ) func main() { ...

  3. Golang中的匿名函数(闭包)

    GO语言的匿名函数就是闭包,以下是<GO语言编程>中对闭包的解释 基本概念闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代 ...

  4. Golang函数-匿名函数与闭包函数

    Golang函数-匿名函数与闭包函数 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.

  5. [日常] Go语言圣经-匿名函数习题

    Go语言圣经-匿名函数1.拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值2.通过这种方式定义的函数可以访 ...

  6. 第一百一十节,JavaScript匿名函数和闭包

    JavaScript匿名函数和闭包 学习要点: 1.匿名函数 2.闭包 匿名函数就是没有名字的函数,闭包是可访问一个函数作用域里变量的函数.声明:本节内容需要有面向对象和少量设计模式基础,否则无法听懂 ...

  7. JavaScript(第十五天)【匿名函数和闭包】

      学习要点: 1.匿名函数 2.闭包 匿名函数就是没有名字的函数,闭包是可访问一个函数作用域里变量的函数.声明:本节内容需要有面向对象和少量设计模式基础,否则无法听懂.(所需基础15章的时候已经声明 ...

  8. 从匿名函数(闭包特性)到 PHP 设计模式之容器模式

    匿名函数(匿名函数) 匿名函数,也叫闭包函数,它允许临时创建一个没有指定名称的函数,常用作回调函数参数的值,也可以作为变量的值来使用.具体的使用见以下示例代码: /* 示例一:声明一个简单匿名函数,并 ...

  9. php的匿名函数和闭包函数

    php的匿名函数和闭包函数 tags: 匿名函数 闭包函数 php闭包函数 php匿名函数 function use 引言:匿名函数和闭包函数都不是特别高深的知识,但是很多刚入门的朋友却总是很困惑,因 ...

随机推荐

  1. SpringBoot--日期格式化

    1.为了统一转转,可以使用日期格式化类 package com.example.demo.resource; import com.fasterxml.jackson.datatype.jsr310. ...

  2. Spring9——通过用Aware接口使用Spring底层组件、环境切换

    通过用Aware接口使用Spring底层组件 能够供我们使用的组件,都是Aware的子接口. ApplicationContextAware:实现步骤:             (1)实现Applic ...

  3. vue全家桶(3.1)

    4.数据请求 4.1.axios是什么? axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它有以下特征: 从浏览器中创建 XMLHttpRequest 从 no ...

  4. sql server 分组查询结合日期模糊查询

    分组查询: https://www.cnblogs.com/netserver/p/4518995.html 日期格式化格式: http://blog.csdn.net/qq_16769857/art ...

  5. redis 链接数满了

    服务器上可以设置timeout参数,这样可以将限制的连接自动释放掉.

  6. ORA-39257: Data cannot be remapped for more than 10 columns.

    ORA-39257: Data cannot be remapped for more than 10 columns. 前言 还是脱敏数据相关的事情. 使用expdp的remap_data参数对指定 ...

  7. 关于WebServices的调用

    1.使用soapui测试接口是否能通    具体操作步骤请查看:https://www.cnblogs.com/BINDAI/p/13201513.html Soapui请求结果集 2.获得soapu ...

  8. 央行数字货币(CBDCs)的互操作性至关重要

    CBDCs(央行数字货币)将在我们的有生之年产生重大的金融转变.然而,除非这些工具吸取了法定货币的教训,否则创新将毫无意义.互操作性一直是影响CBDC采用和功能的最重要障碍之一.因此,各国央行在这一理 ...

  9. 【学习】从.txt文件读取生成编译代码。

    string code = null; String projectName = Assembly.GetExecutingAssembly().GetName().Name; // 1. 生成要编译 ...

  10. ES6入门(一)

    目录 ES6入门 (一) let 和 const 命令 let 定义 注意事项 块级作用域 不存在变量提升 let的特点就是存在暂时性死区 特殊情况的暂时性死区 之 ES6函数存在默认值情况 不允许重 ...