【Go语言学习】匿名函数与闭包

admin 2个月前 (09-11) 科技 46 1

前言

入坑 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语言中,闭包的应用十分普遍,掌握了闭包的使用可以让你在写代码时能加倍游刃有余,也可以制止许多不必要的贫苦。以是是必须要掌握的一个知识点。

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

,

欧博亚洲客户端

欢迎进入欧博亚洲客户端(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。

皇冠体育声明:该文看法仅代表作者自己,与本平台无关。转载请注明:【Go语言学习】匿名函数与闭包

网友评论

  • (*)

最新评论

  • 卡利充值 2020-09-11 00:07:52 回复

    周口新兰商贸有限公司周口新兰商贸有限公司精准、实时报道:新闻频道、体育频道、财经频道、游戏频道、科技频道、健康养生频道等资讯。我也来看

    1

标签列表

    文章归档

    站点信息

    • 文章总数:640
    • 页面总数:0
    • 分类总数:8
    • 标签总数:960
    • 评论总数:272
    • 浏览总数:9380