大家好

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

定义

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

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

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

示例

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:对闭包的考察往往是会出几个类似于本文提到的示例,然后让你说出输出结果及原因。

后语

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

最新文章

  1. stm32GPIO的速度是什么意思
  2. sqlite里执行查询提示未启用约束、主键冲突之——数据竟能超字段长度存储
  3. java中自动装箱的问题
  4. springmvc restful配置有一个小小的坑坑
  5. 2013年最新流行的响应式 WordPress 主题【上篇】
  6. php的SAPI,CLI SAPI,CGI SAPI
  7. Java并发编程:阻塞队列(转载)
  8. 实现从sql server存取二进制图片
  9. longest common str
  10. Spring AOP Example – Pointcut , Advisor
  11. iOS 小知识-tips
  12. spark 监控--WebUi、Metrics System
  13. 【Cocos2d-X开发学习笔记】第19期:动作管理类(CCActionManager)的使用
  14. MySQL无法重启问题解决Warning: World-writable config file ‘/etc/my.cnf’ is ignored
  15. Ubuntu上搭建Git服务器
  16. 解决thymeleaf layout布局不生效
  17. 【经验分享】Hydra(爆破神器)使用方法
  18. CSS 的三种样式 内联 内部 外部
  19. 【LOJ#6277】数列分块1
  20. build script和all projects作用和区别

热门文章

  1. KingbaseES Query Mapping 查询映射功能
  2. 【读书笔记】C#高级编程 第二十章 诊断
  3. 玩转Configmap配置应用的各种姿势
  4. 2020年12月-第01阶段-前端基础-HTML CSS 项目阶段(二)
  5. Openstack neutron:目录
  6. Job for redis-server.service failed because the control process exited with error code(Centos 7 设置Redis开机自启报错)
  7. 谈谈对K8S CNI、CRI和CSI插件的理解
  8. Containerd和Docker的关系
  9. 使用Logstash把MySQL数据导入到Elasticsearch中
  10. 3_肯德基餐厅信息查询_动态加载_post请求