【Golang之路】——匿名函数和闭包

简介: 【Golang之路】——匿名函数和闭包

引言


对golang有些了解的读者,都知道函数在golang中是一等公民,对于函数的一些基本定义和使用,在本文中就不在赘述,我们主要介绍下匿名函数和闭包,这两个概念在很多的框架底层源码中还是非常常见的。


匿名函数


顾名思义,匿名函数就是我们没有函数名称的函数,匿名函数只包括 参数列表、返回值列表:


1. func(参数列表)(返回参数列表){
2.     函数体
3. }

举个栗子:

func main() {
  sq:=func (f float64) float64{
    return  math.Sqrt(f)
  }
  fmt.Println(sq(4))
}

匿名函数还可以在声明后直接调用:

func main() {
  sq:=func (f float64) float64{
    return  math.Sqrt(f)
  }(4)
  fmt.Println(sq)
}

匿名函数做回调函数


匿名函数做回调函数在go语言的设计中非常的常见:

func main() {
  var arrs = []int{1,2,3}
  lists(arrs, func(i int) {
    fmt.Printf("i:=%+v\n",i )
  })
}
func lists(arr []int, f func(int)) {
  for _, i := range arr {
    f(i)
  }
}

上面几个小例子应该已经很清楚的为我们介绍了匿名函数的一些基本用法, 匿名函数是闭包的一个基础,我们对匿名函数有了一定的了解以后,下面我们开始介绍闭包。


闭包


基本定义


所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。闭包=函数+引用环境。

上面的介绍看着非常的专业,但是又好像啥也没有介绍,因为完全没有看懂,所以呢,如果想更加清楚的介绍闭包,我们还是上例子:

func incr() func() int {
  var x int
  return func() int {
    x++
    return x
  }
}

调用这个函数会返回一个函数变量。


in:=incr()


通过把这个函数变量赋值给in,in变量就成了一个闭包。


所以,in中就保存着对x的引用,可以想象成 in中有着一个指针指向x或者说in中有x的地址  


由于in中有着指向x的指针,所以可以修改x,并且可以保持状态。

func main() {
  in:=incr()
  println(in()) // 1
  println(in()) // 2
  println(in()) // 3
}

也就说,x变量逃逸了,他生命周期没有随着它的作用域结束而结束。


通过对上面这个小例子的分析,我们现在在反过来琢磨一下,官方对闭包的定义:闭包=函数+引用环境。  


接下来,我们看下这段代码:


println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1


这个代码返回值都是1,没有进行递增,这是因为我们这里调用了三次incr(),返回了三个闭包,这三个闭包持有了三个不同的对x的引用,他们的状态是各自独立的。


闭包的引用

x := 1
f := func() {
    println(x)
}
x = 2
x = 3
f() // 3

这段代码输出3,如果对上面的分析理解到位了,这里应该比较好理解。因为,f中保存了x的引用,它使用的时候直接解引用,所以x的值随着程序的执行而改变,最后的值为3,所以f中对x的解引用的结果也是3.

x := 1
func() {
    println(x) // 1
}()
x = 2
x = 3


上面输出结果1,因为在调用f的时候就进行解引用了,后面的值修改对f没有影响了。其实上面这段代码就等价于下面代码:

x := 1
f := func() {
    println(x)
}
f() // 1
x = 2
x = 3

循环闭包引用

func main() {                
    s := []string{"a", "b", "c"}                             
    for _, v := range s { 
        go func() {
            fmt.Println(v)
        }()                 
    }                        
    time.Sleep(time.Second * 1)                                                       
} 

结果会是什么呢? a, b, c? 错了,结果是 c, c, c。为什么呢?

这是因为for语句里面中闭包使用的v是外部的v变量,当执行完循环之后,v最终是c,所以输出了 c, c, c。 如果你去执行,有可能也不是这个结果。 输出这个结果的前提是“在主协程执行完for之后,定义的子协程 才开始执行,如果for过程中,子协程执行了,结果就可能不是c, c,c”。 输出的结果依赖于子协程执行时的那一刻,v是什么。


如果我们想输入a b c 怎么做呢?


 func main() {        
      s := []string{"a", "b", "c"}
    for _, v := range s {
        go func() {
            fmt.Println(v)
        }()
        time.Sleep(time.Second * 3)
    }
    fmt.Println("main routine")
    time.Sleep(time.Second * 1)    // 阻塞模式
}

此时输出的就是 a, b, c , main routine

为什么这次有正常了呢? 这是因为在for循环中执行了sleep, 让每次for循环中新定义的子协程有时间执行,子协程执行时获取环境中的变量v, 那么每次就会是本次循环执行时变量v的实际值。


另外一种方法,也是我们最为常用的一种,只需要每次将变量v的拷贝传进函数即可,但此时就不是使用的上下文环境中的变量了。


func main() {                
    s := []string{"a", "b", "c"}                             
    for _, v := range s { 
        go func(v string) {
            fmt.Println(v)
        }(v)   //每次将变量 v 的拷贝传进函数                 
    }                        
    select {}                                                      
}  


延迟调用与闭包


defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用 。defer 中使用匿名函数依然是一个闭包。


package main
import "fmt"
func main() {
    x, y := 1, 2
    defer func(a int) { 
        fmt.Printf("x:%d,y:%d\n", a, y)  // y 为闭包引用
    }(x)      // 复制 x 的值
    x += 100
    y += 100
    fmt.Println(x, y)
}
输出结果:
101 102
x:1,y:102


为什么在defer中的x是1而不是101呢?


其实是原因是 在defer定义时 已经将x的拷贝 1 复制给了defer, defer执行时使用的是当时defer定义时x的拷贝,而不是当前环境中x的值。


为什么要用闭包?


匿名自执行函数:我们知道所有的变量,如果不加上 var 关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用 var 关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。


结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。


总结


本文主要是介绍了匿名函数和闭包,我们在平常使用的是有的一些诡异的问题,尤其是我们如果不注意会引起一些逻辑bug,如果对这些概念和使用不是很熟悉,排查起来也比较费时间,希望通过本文给您带来一些帮助。

目录
相关文章
|
Go
Go 语言使用 goroutine 运行闭包的“坑”
Go 语言使用 goroutine 运行闭包的“坑”
57 0
|
Serverless Go
Go语言闭包不打烊,让你长见识!
Go语言闭包不打烊,让你长见识!
47 0
|
3月前
|
存储 运维 安全
go语言中闭包与匿名函数是什么?
本文探讨了Go语言中的匿名函数与闭包。首先介绍了匿名函数的定义与使用方式,包括直接调用、赋值给变量以及作为全局变量的应用。接着深入解析了闭包的概念及其本质,强调闭包能实现状态保持,但也警告其不当使用可能导致复杂的内存管理和运维问题。通过示例展示了如何利用闭包实现累加器功能,并对比了使用结构体字段的方法。最后,通过一个并发场景的示例说明了闭包在Go中处理多协程安全访问共享数据的应用,展示了闭包结合锁机制确保数据一致性的方式。
|
缓存 Java Serverless
Golang中的闭包详解
Golang中的闭包详解
|
3月前
|
编译器 Go
Go语言中的闭包:封装数据与功能的强大工具
Go语言中的闭包:封装数据与功能的强大工具
|
5月前
|
Go
go的函数定义、递归、延迟、匿名、高阶、闭包
go的函数定义、递归、延迟、匿名、高阶、闭包
|
5月前
|
Go
Go语言进阶篇——浅谈函数中的闭包
Go语言进阶篇——浅谈函数中的闭包
|
6月前
|
Go C++
go 语言回调函数和闭包
go 语言回调函数和闭包
|
6月前
|
存储 编译器 Go
GO闭包实现原理(汇编级讲解)
函数闭包一点也不神秘,它就是函数和引用环境而组合的实体。在Go中,闭包在底层是一个结构体对象,它包含了函数指针与自由变量。Go编译器的逃逸分析机制,会将闭包对象分配至堆中,这样自由变量就不会随着函数栈的销毁而消失,它能依附着闭包实体而一直存在。因此,闭包使用的优缺点是很明显的:闭包能够避免使用全局变量,转而维持自由变量长期存储在内存之中;但是,这种隐式地持有自由变量,在使用不当时,会很容易造成内存浪费与泄露。附着闭包实体而一直存在。
79 0
GO闭包实现原理(汇编级讲解)
|
算法 Go 调度