• 深入理解Golang 闭包,直通面试


    大家好

    今天为大家讲解的面试专题是: 闭包。

    定义

    闭包在计算机科学中的定义是:在函数内部引用了函数内部变量的函数。

    看完定义后,我陷入了沉思...确实,如果之前没有接触过闭包或者对闭包不理解的话,这个定义着实有点让人上头。

    下面让我们先看几个示例,在了解闭包的实际应用后,再去理解这个定义,就不会那么晦涩难懂了。

    示例

    Go 语言是通过匿名函数实现闭包的。

    func increase() func(int) int {
     sum := 0
     return func(i int) int {
      sum += i
      return sum
     }
    }
    
    func main() {
     incr := increase()
     fmt.Println(incr(1))
     fmt.Println(incr(2))
    }
    

    输出结果:

    $go run main.go
    1
    3
    

    不难看出,sum变量在increase函数执行完成后并没有被销毁,而是始终保持在了内存,下面我们可以通过go中的相关命令查看变量是否发生逃逸。

    $go build -gcflags=-m main.go
    # command-line-arguments
    ./main.go:112:9: can inline increase.func1
    ./main.go:106:13: inlining call to fmt.Println
    ./main.go:107:13: inlining call to fmt.Println
    ./main.go:111:2: moved to heap: sum
    ./main.go:112:9: func literal escapes to heap
    ./main.go:106:18: incr(1) escapes to heap
    ./main.go:106:13: []interface {}{...} does not escape
    ./main.go:107:18: incr(2) escapes to heap
    ./main.go:107:13: []interface {}{...} does not escape
    <autogenerated>:1: .this does not escape
    

    可以发现 sum变量发生了内存逃逸,从increase()函数的内部变量逃逸到了堆上,保证了其离开increase()函数作用域后始终保持在内存不被销毁。

    再看定义

    下面我们结合定义:在函数内部引用了函数内部变量的函数,拆解一下上面的这个示例:

    //函数内部是指:increase()函数内部
    //函数内部变量:sum变量
    //函数是指:
    //func(i int)int{ 
    //    sum += i
    //    return sum
    //}
    换成本示例的语言即为在increase()函数内部定义了一个引用increase()函数内部sum变量的匿名函数func(i int)int{}有点套娃道友们多读几遍揣摩揣摩其义自见

    从上面的示例中我们可以看出闭包的两个核心作用:

    • 在函数外部访问函数内部变量成为可能
    • 函数内部变量离开其作用域后始终保持在内存中而不被销毁

    其他场景下的闭包应用

    弄清楚定义后,为了加深我们对闭包的理解,再看两个关于闭包的示例

    defer延迟调用与闭包

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

    func func1() {
     a := 1
     defer func(r int) {
      fmt.Println(r)
     }(a)
     a = a + 100
     fmt.Println(a)
    }
    
    func func2() {
     a := 1
     defer func() {
      fmt.Println(a)
     }()
     a = a + 100
     fmt.Println(a)
    }
    

    func1输出结果:

    $go run main.go
    101
    1
    

    func2输出结果

    $go run main.go
    101
    101
    

    两个函数的差异在于,func1中的defer定义时就将a=1赋值给了defer,在执行defer函数时执行时用的a是在定义时对a的拷贝并非当前环境变量中的a值,即defer执行的是:

    func (r int) {
      fmt.Println(r)
    }(1)
    

    而在func2中,在defer定义时并没有完成任何赋值动作,只是注册了在执行完成后调用的函数,使用的a变量是当前环境的变量。

    goroutine和闭包

    func func2() {
     for i := 0; i < 5; i++ {
      go func() {
       fmt.Println(i)
      }()
     }
    }
    

    针对上面的函数输出,很多人会误以为是0,1,2,3,4,5。但实际上多次运行结果并不一致:

    $go run main.go
    5 3 5 5 5
    //或
    5 5 5 5 5
    //或
    3 3 5 5 5
    

    这是因为go在调度协程的时候,时机不定,可能在i等于0-5任一时间点发生调度,输出的结果,取决于此goroutine执行时外部 i 的值为多少。

    如果我们在goroutine中加上sleep后,输出结果会怎样呢?

    func func2() {
     for i := 0; i < 5; i++ {
      go func() {
       time.Sleep(time.Second)
       fmt.Println(i)
      }()
     }
    }
    

    输出结果:

    $go run main.go
    5 5 5 5 5
    

    这是因为我们在加上sleep后,确保了外部循环执行完成,此时i=5,然后在执行goroutine时,输出结果也为5。

    面试点总结

    • 谈谈你对闭包的理解。 PS:对闭包的考察往往是会出几个类似于本文提到的示例,然后让你说出输出结果及原因。

    后语

    如果大家对本文提到的技术点有任何问题,都可以在评论区进行回复哈,我们共同学习,一起进步! 

  • 相关阅读:
    FastDFS+Nginx部署详细教程
    简单的区别记录
    linux搜索命令之find和grep
    [转载]redis持久化的两种操作RDB和AOF
    多线程的一点点整理
    利用spring-mail模块发送带附件邮件dome
    java集合类总结
    微信支付 遇到的问题
    dubbo监控工具
    Maven配置dubbo环境简单例子
  • 原文地址:https://www.cnblogs.com/gongxianjin/p/16255614.html
Copyright © 2020-2023  润新知