Golang基础 函数详解 匿名函数与闭包

匿名函数是指不需要定义函数名的一种函数实现方式(即没有名字的函数)。匿名函数多用于实现回调函数和闭包。


01 匿名函数

Golang 支持匿名函数,即在需要使用函数时,再定义函数;匿名函数没有函数名,只有函数体。

在 Golang 里面,函数可以被作为一种类型被赋值给函数类型的变量进行传递或使用,匿名函数也往往以变量方式被传递。

1.1 定义匿名函数

相较于 C++ 11 中提供的 Lambda 表达式形式来定义匿名函数,Golang 中匿名函数的定义格式除了没有函数名,其他部分与普通函数定义格式一致。换言之,匿名函数的定义就是没有名字的普通函数定义,其格式如下所示:

// 声明格式
func(参数列表)(返回值列表){
    函数体
}

// example:
func main() {
    func1 := func(m, n int) bool {
        return m < n
    }

    a := 1
    b := 2
    if func1(a, b) {
        fmt.Printf("%d<%d\n", a, b)
    } else {
        fmt.Printf("%d>=%d\n", a, b)
    }
}

/* output:
1<2
*/

🅰️将匿名函数赋值给变量:如上例所示,将匿名函数赋值给变量,然后以变量的形式进行传递是较为常见的方式,类似给没有函数名的匿名函数起了个名字。

🅱️在定义时调用匿名函数:匿名函数也可以在定义时直接进行调用执行,不需要外部调用,这样这个函数仅会被调用一次,如下例子所示:

func main() {
    a := 2
    b := 1
    if func(m, n int) bool { return m < n }(a, b) {
        fmt.Printf("%d<%d\n", a, b)
    } else {
        fmt.Printf("%d>=%d\n", a, b)
    }
}

/* output:
2>=1
*/

1.2 匿名函数使用场景

1️⃣匿名函数作为值使用:由于匿名函数没有函数名,这时它就相当于是一条表达式,可以把它赋值给一个变量,在需要使用的地方进行按值调用即可。

func main() {
    func2 := func(s []int) int {
        sum := 0
        for _, v := range s {
            sum += v
        }
        return sum
    }

    fmt.Printf("sqrt 0f func2 is : %f\n", math.Sqrt(float64(func2([]int{1, 2, 3, 3}))))
}

/* output:
sqrt 0f func2 is : 3.000000
*/

2️⃣匿名函数作为回调函数使用

回调函数就是📣通过函数引用调用的函数,例如函数A的参数列表中函数B的引用作为参数之一,并且在函数A中使用该函数引用调用执行函数B,那么函数B就是回调函数。

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用,用于对该事件或条件进行响应(回调函数是一种事件驱动机制)。

匿名函数也可以以函数引用的形式作为其他函数的参数使用,📌使用匿名函数作为回调函数的优点在于不需要耗费额外的空间去专门定义处理回调事件的函数📌,因为当函数被执行时才会调用回调函数。

匿名函数作为回调函数需要注意如下两点:

  • 回调函数所必须的值由外部调用函数传入
  • 外部调用函数的参数列表中必须正确声明回调函数
func scanSlice(s []int, f func(int)) {
    for _, v := range s {
        f(v)
    }
}

func main() {
    func3 := func(v int) {
        fmt.Printf(" %d ", v)
    }
    // func3 作为回调函数打印切片中的元素
    scanSlice([]int{1, 2, 3, 4, 5}, func3)
}

/* output:
 1  2  3  4  5 
*/

3️⃣匿名函数用来获取父作用域中的变量:通过匿名函数访问父作用域中的变量是创建闭包的一种常见方式,就是在一个函数内部声明一个匿名函数,通过该匿名函数访问这个函数的局部变量。通过这种方式可以把局部变量留在内存中,避免使用全局变量引发潜在的全部变量污染情况。

02 闭包

闭包的形式化定义是:📌一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。📌

通俗来讲,📣闭包就是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。

👉 函数 + 引用环境 = 闭包 👈

函数和相关引用环境的组合形成了闭包实例,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

其中,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”。🔴函数是编译期的静态概念,🟢而闭包是运行期的动态概念。(可以与程序和进程的概念进行类比)

在这里插入图片描述

闭包机制的主要用途在于如下两点:

  • 可以获取函数内部的局部变量,实现公有变量
  • 可以让自由变量的值始终保存在内存中,实现缓存效果

2.1 闭包实现公有变量

闭包机制使得函数的内部变量可以在函数外部被重复使用,并且不会造成变量污染。

这相较于全局变量和普通局部变量具有明显优势,虽然全局变量可以重复使用,但是容易造成变量污染;而局部变量仅在局部作用域内有效,虽然不会造成变量污染,但是不可以重复使用。

如下例子中声明了一个累加函数 accumulate,他的返回值是函数类型 func() int,每次调用这个累加函数时,其内部的匿名函数都会对累加函数中的局部变量 x 进行 +1 修改,并打印该变量的地址。在 main 函数中分别声明两个初始值不同的累加函数 func4,func5,并分别调用两次。

func accumulate(n int) func() int {
    var x int = n
    //返回一个自己定义的函数类型 (返回一个闭包:匿名函数 + accumulate引用环境)
    return func() int {
        x++             // 修改accumulate的变量 x
        fmt.Println(&x) // 打印这个变量的地址
        return x        // 返回累加值
    }
}

func main() {
    // 初始值为 1 累加
    func4 := accumulate(1)
    fmt.Printf("%p\n", func4)
    fmt.Println(func4())
    fmt.Println(func4())

    // 初始值为 10 累加
    func5 := accumulate(10)
    fmt.Printf("%p\n", func5)
    fmt.Println(func5())
    fmt.Println(func5())
}

/* output:
0x8df1c0
0xc000012300
2
0xc000012300
3
0x8df1c0
0xc000012308
11
0xc000012308
12
*/

从输出结果可以看出,闭包与函数与引用环境的关系,声明的两个函数他们的地址是一致的,函数中操作的变量地址在同一个闭包中是一致的,然而在不同闭包中变量地址是不一致的。

这表示被捕获到闭包中的变量让闭包本身拥有了📣记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应,其中的变量在闭包的声明周期内变成了“公有变量”。函数在不同引用环境下会形成不同的闭包,所以闭包是一个动态概念。

2.2 闭包实现缓存效果

闭包会把父作用域(函数)中的变量保存在内存中,直到闭包的生命周期结束,所以利用这一特点可以将被闭包捕获的变量作为缓存空间来使用。

如下例子中声明了一个 dish 函数,其自由变量名称为 food 作为缓存空间,其中声明了两个匿名函数,分别实现重置 foodpop 操作和修改 foodpush 操作,用来模拟消费和生产两种行为。🙋‍♂️(由于pop 和 push 的参数列表不一样不好抽象统一,例子中的实现并不优雅,暂时没有想到比较好的实现方式,欢迎指正!)

func dish(s string) map[string]func(string) {
    var food = s
    var function = map[string]func(string){
        "pop": func(newFood string) {
            if food != "" {
                fmt.Printf("pop: %s, pos: %p\n", food, &food)
                food = ""
            } else {
                fmt.Println("the dish is empty!")
            }
        },
        "push": func(newFood string) {
            food = newFood
            fmt.Printf("push: %s, pos: %p\n", food, &food)
        },
    }

    return function
}

func main() {
    func6 := dish("apple")
    func6["pop"]("")
    func6["pop"]("")
    func6["push"]("banana")
    func6["pop"]("")
}

/* output:
pop: apple, pos: 0xc0000465e0
the dish is empty!
push: banana, pos: 0xc0000465e0
pop: banana, pos: 0xc0000465e0
*/

可以看到输出结果中,不管是 pop 还是 push 操作,food 的地址都没有改变过,所以自由变量 food 一直被保存在内存中,并没有在 dish 调用结束后被自动清除,通过这种方式可以实现缓存效果。

但是这种闭包实现缓存的也存在着明显的缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。一种解决方法是在退出函数之前,将不使用的局部变量全部删除。

参考资料

匿名函数 · Go语言中文文档

C++11 Lambda表达式(匿名函数)

匿名函数的应用&命名空间的应用&类与对象的关系与使用方式–2019年9月29日

回调函数(callback)是什么? - 简书

闭包、递归 · Go语言中文文档

闭包的作用 - 简书


如果文章对你有帮助,欢迎一键三连 👍 ⭐️ 💬 。如果还能够点击关注,那真的是对我最大的鼓励 🔥🔥🔥 。


  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王清欢Randy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值