golang闭包、defer

  • 闭包
    • 闭包以引用的方式捕捉外部变量
    • 闭包变量与作用域
      • 传统函数局部变量
      • 闭包的变量
  • defer
    • 延迟:在函数执行完毕之前调用
    • 多个defer执行顺序

闭包

闭包以引用的方式捕捉外部变量

package mainimport "fmt"func main() {a := 10str := "狂歌痛饮空度日"func() {a = 14str = "飞扬跋扈为谁雄"fmt.Printf("闭包:a= %d, str = %s\n", a, str)}()// 函数调用fmt.Printf("外部:a= %d, str = %s\n", a, str)}

控制台打印

闭包:a= 14, str = 飞扬跋扈为谁雄
外部:a= 14, str = 飞扬跋扈为谁雄
PS D:\vscode\code\demo1>

以上例子说明,闭包以引用的方式捕捉外部变量,闭包里更改了变量,实则是引用的方式,所以外面的变量也跟着改变了。

闭包变量与作用域

所谓闭包就是一个函数“捕获”了和它在同一作用域的其它常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。

下面我们来看两个例子去理解一下上面这句话

传统函数局部变量

package mainimport "fmt"func test1() int {// 函数被调用时,x才分配空间,才初始化为0var x int // int 类型没有被初始化,值为0x++return x * x // 函数调用完毕,x 自动释放
}func main() {fmt.Println(test1())fmt.Println(test1())fmt.Println(test1())fmt.Println(test1())fmt.Println(test1())
}

控制台

PS D:\vscode\code\demo1> go run bibao2.go
1
1
1
1
1

以上代码没有什么好说的,就是我们常见的函数,其中要注意一点是,在golang里,函数的局部变量是在函数被调用时才,变量才初始化,比如int类型的变量,然后初始化为0,当函数调用完毕之后,这些局部变量就会被释放。如上面例子,调用多少次,那么x就初始化,然后释放,下次调用的时候仍然是初始化,然后再释放。
下面我们来看看闭包的变量

闭包的变量

package mainimport "fmt"// 函数的返回值是一个匿名函数,返回一个函数类型
func test2() func() int {var x int// 对于下面的代码,匿名函数就形成了一个闭包了return func() int {x++return x * x}
}func main() {// 返回值为一个匿名函数,返回一个函数类型,通过f来调用返回的匿名函数,f来调用闭包函数f := test2()// 它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包(这里指的是f)还在使用它,这些变量就还会存在fmt.Println(f()) // 1fmt.Println(f()) // 4fmt.Println(f()) // 9fmt.Println(f()) // 16fmt.Println(f()) // 25}

上面代码,在闭包第一次被调用完之后,一下代码就形成了一个独立的空间

 return func() int {x++return x * x}

x 的生命周期还在,也没有被释放。那x还是1,当第一次调用之后,x=1,第二次调用的时候x++ 就是2,然后第三次x++ = 3…所以返回结果就是他们的平方,1,4,9,16,25.

函数test2返回另一个类型为func() int 的函数。对test2的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用test2 时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
通过这个例子,我们看到变量的生命周期不由它的作用域决定:test2返回后,变量x仍然隐式的存在于f中。
所以对于开始的那句话:它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只要闭包函数(这里指main方法的f)还在使用它,这些变量就还会存在。

defer

官方定义:关键字 defer用于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行。注意,defer语句只能出现在函数或方法的内部。

说人话:defer主要用于函数在调用结束前做一些清理工作,比如我们读取文件,文件打开了,然后我要关闭文件,什么时候关闭呢,那就是函数结束前去关闭。

延迟:在函数执行完毕之前调用

defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下

package mainimport "fmt"func main() {// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下defer fmt.Println("我自横刀向天笑")fmt.Println("去留肝胆两昆仑")
}

控制台:

PS D:\vscode\code\demo1> go run defer1.go
去留肝胆两昆仑
我自横刀向天笑

多个defer执行顺序

如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
其实可以理解为和栈差不多,先进后出,就是按照代码顺序,先defer的后被执行。

下面我们来看一段代码

package mainimport "fmt"func test(x int) {result := 10 / xfmt.Println("result = %d", result)
}func main() {// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下fmt.Println("不尽长江滚滚来")fmt.Println("无边落木萧萧下")test(0)fmt.Println("驻青沙白鸟飞回")fmt.Println("风急天高猿啸哀")
}

下面我们通过代码来解释一下这句话:
如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
demo1:普通代码,没有defer

PS D:\vscode\code\demo1> go run defer1.go
不尽长江滚滚来
无边落木萧萧下
panic: runtime error: integer divide by zerogoroutine 1 [running]:
main.test(0xf366c0)D:/vscode/code/demo1/defer1.go:6 +0xad
main.main()D:/vscode/code/demo1/defer1.go:15 +0x9b
exit status 2

demo分析:以上代码没有defer我们可以看到,以上代码遇到panic的时候就停止运行了。

demo2:其他代码加defer,但是panic的那行代码不加defer

package mainimport "fmt"func test(x int) {result := 10 / xfmt.Println("result = %d", result)
}func main() {// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下defer fmt.Println("不尽长江滚滚来")defer fmt.Println("无边落木萧萧下")test(0)defer fmt.Println("驻青沙白鸟飞回")defer fmt.Println("风急天高猿啸哀")
}

控制台

PS D:\vscode\code\demo1> go run defer1.go
无边落木萧萧下
不尽长江滚滚来
panic: runtime error: integer divide by zerogoroutine 1 [running]:
main.test(0x101018c6cbd0108)D:/vscode/code/demo1/defer1.go:6 +0xad
main.main()D:/vscode/code/demo1/defer1.go:15 +0xc5
exit status 2
PS D:\vscode\code\demo1>

demo2分析:代码里除了panic的那行代码没有defer,其他的都加了defer。我们可以看到两个现象。现象1:前两句诗确实是按照 先进后出,即多个defer修饰的代码先定义后打印。
现象2,panic之后的代码并没有被打印,也就是在panic之后的代码就没运行。
注意:以上代码其实最先调用的是test(0)这行代码,因为被defer修饰代码在函数运行结束之前才会被调用。其次运行到代码test(0)的时候代码崩了,然后函数就要结束了,所以此时要运行被defer修饰的代码,因为test(0)没有被defer修饰,所以test(0)之后的代码就不会被运行了。于是打印结果是只打印了,前两个derfer。所以我们在写代码的时候,尽量将defer写在函数最前面。

demo3:所有代码都加defer

package mainimport "fmt"func test(x int) {result := 10 / xfmt.Println("result = %d", result)
}func main() {// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下defer fmt.Println("不尽长江滚滚来")defer fmt.Println("无边落木萧萧下")defer test(0)defer fmt.Println("驻青沙白鸟飞回")defer fmt.Println("风急天高猿啸哀")
}

控制台

PS D:\vscode\code\demo1> go run defer1.go
风急天高猿啸哀
驻青沙白鸟飞回
无边落木萧萧下
不尽长江滚滚来
panic: runtime error: integer divide by zerogoroutine 1 [running]:
main.test(0xab6220)D:/vscode/code/demo1/defer1.go:6 +0xad
main.main()D:/vscode/code/demo1/defer1.go:18 +0x19a
exit status 2
PS D:\vscode\code\demo1>```demo3分析:
现象1:多个defer修饰的代码全部是,【先进后出】即:先定义后运行的顺序。
现象2:哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。所以以上3个demo就解释了开头的那句话:
如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。# defer和匿名函数联合使用
无奖精彩,爱猜不猜dem1:你知道运行结果吗?```go
package mainimport "fmt"func test(x int) {result := 10 / xfmt.Println("result = %d", result)
}func main() {a := 10b := 20defer func() {fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)}() // 调用函数a = 111b = 222fmt.Printf("外部 a = %d, b =%d\n", a, b)
}

控制台:

PS D:\vscode\code\demo1> go run defer2.go
外部 a = 111, b =222
匿名函数 a=111, b=222
PS D:\vscode\code\demo1>

demo1分析,首先defer是最后执行的,所以先打印外部函数,再打印匿名函数。又因为两个变量已经被赋予了新的值,于是defer打印的就是新的值了。

有人说demo1很简单,那我们来看看demo2
demo2:

package mainimport "fmt"func test(x int) {result := 10 / xfmt.Println("result = %d", result)
}func main() {a := 10b := 20defer func(a, b int) {fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)}(a, b) // 调用函数a = 111b = 222fmt.Printf("外部 a = %d, b =%d\n", a, b)
}

控制台

PS D:\vscode\code\demo1> go run defer2.go
外部 a = 111, b =222
匿名函数 a=10, b=20
PS D:\vscode\code\demo1>

WTF?

demo2分析:首先先打印外部的,并且外部打印的为111,222这个没啥争议。
但是匿名函数内呢?其实代码相当于是先把参数10,20传进去了,但是并没有马上运行,因为它被defer修饰了,所以它最后运行,但是当运行的时候,它拿到的还是传递过去的10,20,所以打印结果是10,20。
以上代码就等价于

defer func(a, b int) {fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)}(10, 20) // 调用函数
defer func(a, b int) {fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)}(a, b) // 调用函数

demo3

package mainimport "fmt"func f1() (result int) { defer func() {result++}()return 1
}func f2() (r int) { t := 1defer func() {t++}()return t
}func f3() (r int) { defer func(t int) {t++}(r)return 1
}func main() {fmt.Println(f1())fmt.Println(f2())fmt.Println(f3())
}

demo3
控制台

PS D:\vscode\code\demo1> go run defer3.go
2
1
1

demo3解析:

package mainimport "fmt"// 首先函数的返回值为result,在return 1 的时候,其实是将1 赋值给了result这个变量,然后defer是最后运行的,
// result++ ,所以返回值为2
func f1() (result int) {defer func() {result++}()return 1
}// 首先函数的返回值为r,在return的时候,其实是将t的值赋值给了r,于是r = 1,然后defer是最后运行的
// 但是返回值为r,defer里面对t进行++,t = 2,但是返回值是r,所以函数返回值为1
func f2() (r int) {t := 1defer func() {t++}()return t
}// 首先函数返回值为r, return 1的时候其实是将 1赋值给了r,然后函数传参,将r 传递了进去,此时t = 1,然后t++ = 2
// 但是函数的返回值为r,与t无关,所以返回1,可以把defer函数里的r参数看成一个固定值1,就行了,不要被干扰
// 还可以这样理解,go语言中所有的函数传递都是传值的,那么所有函数都是有一个参数副本的,在函数里面计算是计算的副本,
// 不会修改原先的值,也就是这里虽然传递的是r,但是它算的是r的副本,并不是r原先的值,如果这里传递的是r的引用,那结果就是2了
func f3() (r int) { defer func(t int) {t++}(r)return 1
}
// 如果传递的是引用,则defer里计算的就是r的值了,那么函数返回的就是r了。
// 注意:*类型,如*int代表该变量为指针类型,用来存储变量的地址。*指针类型的*代表对指针取值,取出指针指向的地址的内存。&代表取一个变量的指针地址
func f3() (r int) {defer func(t *int) {*t++}(&r)return 1
}func main() {fmt.Println(f1())fmt.Println(f2())fmt.Println(f3())
}

注:*、&,指针和内存

golang学习之四:闭包、defer相关推荐

  1. golang学习笔记(基础篇)

    LCY~~Golang学习笔记 一.Go语言开发环境 ##安装Go开发包以及VsCode Go开发包与vscode配置安装教程网址:https://www.liwenzhou.com/posts/Go ...

  2. 118云原生编程语言Golang学习笔记

    Golang学习笔记 文章目录 1.Go简介 1.1 简介 1.2 设计初衷 1.3 Golang vs Java 1.4 应用领域 1.5 用go语言的公司 2.Go下载和安装 2.1 开发工具 2 ...

  3. golang反编译_【Golang】脱胎换骨的defer(一)

    Go语言的defer是一个很方便的机制,能够把某些函数调用推迟到当前函数返回前才实际执行.我们可以很方便的用defer关闭一个打开的文件.释放一个Redis连接,或者解锁一个Mutex.而且Go语言在 ...

  4. golang学习之negroni/gizp源码分析

    在 Go 语言里,Negroni 是一个很地道的 Web 中间件,它是一个具备微型.非嵌入式.鼓励使用原生 net/http 库特征的中间件.利用它地Use功能,我们可以很简单地自定义中间件并使用.其 ...

  5. Golang学习--TOML配置处理

    Golang学习–TOML配置处理 文章目录 Golang学习--TOML配置处理 配置工具的选择 toml的使用 配置的单例模式 配置的更新 POSIX信号 在POSIX.1-1990标准中定义的信 ...

  6. Golang学习-基础命令

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. . Golang学习-基础命令 一.go run 用于运行命令源码文件,只能接收一个命令源码文件以及若干个库源码文件作为 ...

  7. 通过示例学习JavaScript闭包

    译者按: 在上一篇博客,我们通过实现一个计数器,了解了如何使用闭包(Closure)**,这篇博客将提供一些代码示例,帮助大家理解闭包. 原文: JavaScript Closures for Dum ...

  8. [转]Docker学习之四:使用docker安装mysql

    本文转自:https://blog.csdn.net/qq_19348391/article/details/82998391 Docker学习之一:注册Docker Hub账号 Docker学习之二 ...

  9. ETL学习之四:SQL Server Integration Services入门

    ETL学习之四:SQL Server Integration Services入门 SSIS就是微软在SQL SERVER2005上对DTS的升级,不得不说,微软在BI上是花了很大功夫的,包括提供了S ...

最新文章

  1. 杨国福或夺“麻辣烫第一股”,是否名副其实?
  2. hprof文件中导出图片
  3. LeetCode每日一题:回文数(No.9)
  4. 设置mysql表名不区分大小写
  5. Unity 3D 正交相机(Orthographic)
  6. 如何用编程得出泰坦尼克号生还者的年龄段?
  7. linux -- open /acess/ftruncate/lstat 函数
  8. 有Web认证情况下的路由器设置
  9. js的中文在网页中显示为乱码
  10. easybcd卸载linux系统,Windows 8.1和Ubuntu 14.04双系统卸载Ubuntu参考教程
  11. C#服务端如何获取外网IP
  12. 扩张的矩阵三要素——时间、空间和事件
  13. 项目xx方案文档格式规范模板
  14. 阿里发布内部(面试官)题库:2022年Java社招岗(正式版)面试题
  15. 【diskgenius】【Error on partition resizing.(2000011a)Out of disk space.】【The partition(or volume)“PART
  16. 五指山(nefu 84)
  17. JVM、JDK、JRE分别表示什么含义
  18. 如何在Windows上使用PSCP命令?
  19. matlab水力学工具箱,水工设计工具箱免费版
  20. Java、JSP农产品销售系统的设计与实现

热门文章

  1. Java 面试常见项目问题回答
  2. 集美大学计算机课程考试系统,计算机系统结构-集美大学考试内容.docx
  3. 关于3G手机USIM卡的电话簿
  4. 发票丢失了该怎么处理......
  5. cs七龙珠怎么添加机器人_怎么调能让CS龙珠版2.1的机器人笨一点?
  6. 关于“幽灵架构”的总结:适用场景与方法重载
  7. 【RPA入门教程】UIBot命令的基本操作
  8. U3 精通U盘分区与启动-孙宇彤-专题视频课程
  9. 团队目标由一致到分歧的案例
  10. MMORPG大型游戏设计与开发(服务器 游戏场景 地图和区域)