Go面试题(二):聊聊Go语言中的闭包理解

15001004756 · · 981 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

**大家好,我是小道哥。** 今天为大家讲解的面试专题是: **闭包**。 ## 定义 闭包在计算机科学中的定义是:**在函数内部引用了函数内部变量的函数**。 看完定义后,我陷入了沉思...确实,如果之前没有接触过闭包或者对闭包不理解的话,这个定义着实有点让人上头。 下面让我们先看几个示例,在了解闭包的实际应用后,再去理解这个定义,就不会那么晦涩难懂了。 ## 示例 Go 语言是通过匿名函数实现闭包的。 ```golang 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)) } ``` 输出结果: ```shell $go run main.go 1 3 ``` 不难看出,sum变量在increase函数执行完成后并没有被销毁,而是始终保持在了内存,下面我们可以通过go中的相关命令查看变量是否发生逃逸。 ```shell $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()函数作用域后始终保持在内存不被销毁。 ## 再看定义 下面我们结合定义:**在函数内部引用了函数内部变量的函数**,拆解一下上面的这个示例: ```golang //函数内部是指:increase()函数内部 //函数内部变量:sum变量 //函数是指: //func(i int)int{ // sum += i // return sum //} 换成本示例的语言即为: 在increase()函数内部定义了一个引用increase()函数内部sum变量的匿名函数func(i int)int{}【有点套娃,道友们多读几遍,揣摩揣摩,其义自见】 ``` 从上面的示例中我们可以看出闭包的两个核心作用: * **在函数外部访问函数内部变量成为可能** * **函数内部变量离开其作用域后始终保持在内存中而不被销毁** ## 其他场景下的闭包应用 弄清楚定义后,为了加深我们对闭包的理解,再看两个关于闭包的示例 ### defer延迟调用与闭包 defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用。defer 中使用的匿名函数也是一个闭包。 ```golang 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输出结果: ```shell $go run main.go 101 1 ``` func2输出结果 ```shell $go run main.go 101 101 ``` 两个函数的差异在于,func1中的defer定义时就将a=1赋值给了defer,在执行defer函数时执行时用的a是在定义时对a的拷贝并非当前环境变量中的a值,即defer执行的是: ```golang func (r int) { fmt.Println(r) }(1) ``` 而在func2中,在defer定义时并没有完成任何赋值动作,只是注册了在执行完成后调用的函数,使用的a变量是当前环境的变量。 ### goroutine和闭包 ```golang func func2() { for i := 0; i < 5; i++ { go func() { fmt.Println(i) }() } } ``` 针对上面的函数输出,很多人会误以为是0,1,2,3,4,5。但实际上多次运行结果并不一致: ```shell $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后,输出结果会怎样呢? ```go func func2() { for i := 0; i < 5; i++ { go func() { time.Sleep(time.Second) fmt.Println(i) }() } } ``` 输出结果: ```shell $go run main.go 5 5 5 5 5 ``` 这是因为我们在加上sleep后,确保了外部循环执行完成,此时i=5,然后在执行goroutine时,输出结果也为5。 ## 面试点总结 * 谈谈你对闭包的理解。 PS:对闭包的考察往往是会出几个类似于本文提到的示例,然后让你说出输出结果及原因。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

981 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传