深入理解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:对闭包的考察往往是会出几个类似于本文提到的示例,然后让你说出输出结果及原因。
后语
如果大家对本文提到的技术点有任何问题,都可以在评论区进行回复哈,我们共同学习,一起进步!
深入理解Golang 闭包,直通面试的更多相关文章
- 深入贯彻闭包思想,全面理解JS闭包形成过程
谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至可以说它们是衡量js工程师实力的一个重要指标.下面我们就罗列闭包的几个常见问题,从回答 ...
- golang 闭包
说起golang闭包,在官方手册里面看过一次,没怎么用过,还是因为6哥经常用,阅读他的代码好多闭包,emmm,今天就学习一下. 在过去近十年时间里,面向对象编程大行其道,以至于在大学的教育里,老师也只 ...
- 深入理解JavaScript闭包【译】
在<高级程序设计>中,对于闭包一直没有很好的解释,在stackoverflow上翻出了一篇很老的<JavaScript closure for dummies>(2016)~ ...
- 轻松理解JavaScript闭包
摘要 闭包机制是JavaScript的重点和难点,本文希望能帮助大家轻松的学习闭包 一.什么是闭包? 闭包就是可以访问另一个函数作用域中变量的函数. 下面列举出常见的闭包实现方式,以例子讲解闭包概念 ...
- 理解Golang哈希表Map的元素
目录 概述 哈希函数 冲突解决 初始化 结构体 字面量 运行时 操作 访问 写入 扩容 删除 总结 在上一节中我们介绍了 数组和切片的实现原理,这一节会介绍 Golang 中的另一个集合元素 - 哈希 ...
- 理解Python闭包概念
闭包并不只是一个python中的概念,在函数式编程语言中应用较为广泛.理解python中的闭包一方面是能够正确的使用闭包,另一方面可以好好体会和思考闭包的设计思想. 1.概念介绍 首先看一下维基上对闭 ...
- 我从来不理解JavaScript闭包,直到有人这样向我解释它...
摘要: 理解JS闭包. 原文:我从来不理解JavaScript闭包,直到有人这样向我解释它... 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 正如标题所述,JavaScript闭包 ...
- javascript深入理解js闭包(转)
javascript深入理解js闭包 转载 2010-07-03 作者: 我要评论 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. ...
- 理解JS闭包的几个小实验
学了JavaScript有一段时间了,但是对闭包还是不太理解,于是怀着心中的疑问做了几个小实验,终于有点明白了. 首先看一下MDN上的定义:闭包是函数和声明该函数的词法环境的组合. 简单来说,闭包是一 ...
- 理解 JavaScript 闭包
这是本系列的第 4 篇文章. 作为 JS 初学者,第一次接触闭包的概念是因为写出了类似下面的代码: for (var i = 0; i < helpText.length; i++) { var ...
随机推荐
- Java 多线程:线程池
Java 多线程:线程池 作者:Grey 原文地址: 博客园:Java 多线程:线程池 CSDN:Java 多线程:线程池 工作原理 线程池内部是通过队列结合线程实现的,当我们利用线程池执行任务时: ...
- Python Web开发主流框架
Web 开发是Python 语言应用领域的重要部分,也是工作岗位最多的领域.如果你对基于Python的Web 开发有兴趣,正打算使用Python 做Web 开发,或者已经是一个Web 开发者有工作需要 ...
- Prometheus 监控外部 Kubernetes 集群
转载自:https://www.qikqiak.com/post/monitor-external-k8s-on-prometheus/ 在实际环境中很多企业是将 Prometheus 单独部署在集群 ...
- 6.第五篇 安装keepalived与Nginx
文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247483796&idx=1&sn=347664de ...
- Elasticsearch:Elasticsearch中的refresh和flush操作指南
在今天的文章里,我们来主要介绍一下Elasticsearch的refresh及flush两种操作的区别.如果我们从字面的意思上讲,好像都是刷新的意思.但是在Elasticsearch中,这两种操作是有 ...
- LeetCode - 统计数组中的元素
1. 统计数组中元素总结 1.1 统计元素出现的次数 为了统计元素出现的次数,我们肯定需要一个map来记录每个数组以及对应数字出现的频次.这里map的选择比较有讲究: 如果数据的范围有限制,如:只有小 ...
- 【前端必会】走进webpack生命周期,另类的学习方法
背景 webpack构建过程中的hooks都有什么呢?除了在网上看一些文章,还可以通过更直接的办法,结合官方文档快速让你进入webpack的hook世界 写一个入口文件 //index.js cons ...
- 关于pwd命令小技巧-确认当前工作目录的绝对路径中是否包含软链接目录名
Linux中任何一个命令,当你用心研究到深处时,也许总能有着新的发现或者有趣的用途,如下方的pwd命令 对于pwd命令,大家都知道是用于打印当前的工作目录路径,而且是绝对路径 pwd命令两个选项的,默 ...
- P1073 [NOIP2009 提高组] 最优贸易 (最短路spfa)
本题就是在一条1-n的路径上找p,q(先经过p),使得q-p最大. 考虑建正反图,正图上求出d[x],表示1-x的路径经过的节点最小值,反图上则从n开始求出f[x],x-n的最大值,最后枚举断点i,取 ...
- POJ3107 Godfather (树的重心)
又是一道模板题...... 1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 using ...