1. Go函数闭包

Go语言原生提供了对闭包(closure)的支持。在Go语言中,闭包就是函数字面值[2]。Go规范中是这样诠释闭包的:

函数字面值(function literals)是闭包:它们可以引用其包裹函数(surrounding function)中定义的变量。然后,这些变量在包裹函数和函数字面值之间共享,只要它们可以被访问,它们就会继续存在。

闭包在Go语言中有着广泛的应用,最常见的就是与go关键字一起联合使用创建一个新goroutine,比如下面标准库中net/http包中的一段代码:

// $GOROOT/src/net/http/fileTransport.go00 func (t fileTransport) RoundTrip(req *Request) (resp *Response, err error) {
01     rw, resc := newPopulateResponseWriter()
02     go func() {
03         t.fh.ServeHTTP(rw, req)
04         rw.finish()
05     }()
06     return <-resc, nil
07 }

上面这段代码中的RoundTrip方法就是使用go关键字结合闭包创建了一个新的goroutine,并且在这个goroutine中运行的函数还引用了本属于其外部包裹函数的变量:t、rw和req,或者说两者共享这些变量。

原本仅在RoundTrip方法内部使用的变量一旦被“共享”给了其他函数,那么它就无法在栈上分配了,逃逸到堆上是确定性事件。

那么问题来了!这些被引用或叫被闭包捕获的分配在堆上的外部变量何时能被回收呢?也许上面的例子还十分容易理解,当新创建的goroutine执行完毕后,这些变量就可以回收了。那么下面的闭包函数呢?

func foo() func(int) int {i := []int{0: 10, 1: 11, 15: 128}return func(n int) int {n+=i[0]return n }
}

在这个foo函数中,被闭包函数捕获的长度为16的切片变量i何时可以被回收呢?

注:我们定义闭包时,喜欢用引用外部包裹函数的变量这种说法,但在Go编译器的实现代码[3]中,使用的是capture var,翻译过来就是“被捕获的变量”,所以这里也用了“捕获”一词来表示那些被闭包共享使用的外部包裹函数甚至是更外层函数中的变量。

foo函数的返回值类型是一个函数,也就是说foo函数的本地变量i被foo返回的新创建的闭包函数所捕获,i不会被回收。通常一个堆上的内存对象有明确的引用它的对象或指向它的地址的指针,该对象才会继续存活,当其不可达(unreachable)时,即再没有引用它的对象或指向它的指针时才会被GC回收。

那么,变量i究竟是被谁引用了呢?变量i将在何时被回收呢?

我们先回头看一个非闭包的一般函数:

func f1() []int {i := []int{0: 10, 1: 11, 15: 128}return i
}func f2() {sl := f1()sl[0] = sl[0] + 10fmt.Println(sl)
}func main() {f2()
}

我们看到f1将自己的局部切片变量i返回后,该变量被f2函数中的sl所引用,f2函数执行完成后,切片变量i将变成unreachable,GC将回收该变量对应的堆内存。

如果换成闭包函数,比如前面的foo函数,我们很大可能是这么来用的:

// https://github.com/bigwhite/experiments/tree/master/closure/closure1.go1 package main2 3 import "fmt"4 5 func foo() func(int) int {6     i := []int{0: 10, 1: 11, 15: 128}7     return func(n int) int {8         n += i[0]9         return n
10     }
11 }
12
13 func bar() {
14     f := foo()
15     a := f(5)
16     fmt.Println(a)
17 }
18
19 func main() {
20     bar()
21     g := foo()
22     b := g(6)
23     fmt.Println(b)
24 }

在这里例子中,只要闭包函数中引用了foo函数的本地变量。这突然让我想起了“在Go中,函数也是一等公民的特性[4]”。难道是闭包函数这一对象引用了foo函数的本地变量? 那么闭包函数在内存布局上是如何引用到foo函数的本地整型切片变量i的呢?闭包函数在内存布局中被映射为什么了呢?

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。

2. Go闭包函数对象

要解答这个问题,我们只能寻求Go汇编[5]的帮助。我们生成上面的closure1.go的汇编代码(我们使用go 1.16.5版本Go编译器):

$go tool compile -S closure1.go > closure1.s

在汇编代码中,我们找到closure1.go中第7行创建一个闭包函数所对应的汇编代码:

// https://github.com/bigwhite/experiments/tree/master/closure/closure1.s0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX0x0059 00089 (closure1.go:7)    MOVQ    CX, (SP)0x005d 00093 (closure1.go:7)    PCDATA  $1, $10x005d 00093 (closure1.go:7)    NOP0x0060 00096 (closure1.go:7)    CALL    runtime.newobject(SB)0x0065 00101 (closure1.go:7)    MOVQ    8(SP), AX0x006a 00106 (closure1.go:7)    LEAQ    "".foo.func1(SB), CX0x0071 00113 (closure1.go:7)    MOVQ    CX, (AX)0x0074 00116 (closure1.go:7)    MOVQ    $16, 16(AX)0x007c 00124 (closure1.go:7)    MOVQ    $16, 24(AX)0x0084 00132 (closure1.go:7)    PCDATA  $0, $-20x0084 00132 (closure1.go:7)    CMPL    runtime.writeBarrier(SB), $00x008b 00139 (closure1.go:7)    JNE 1650x008d 00141 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX0x0092 00146 (closure1.go:7)    MOVQ    CX, 8(AX)0x0096 00150 (closure1.go:7)    PCDATA  $0, $-10x0096 00150 (closure1.go:7)    MOVQ    AX, "".~r0+40(SP)0x009b 00155 (closure1.go:7)    MOVQ    24(SP), BP0x00a0 00160 (closure1.go:7)    ADDQ    $32, SP0x00a4 00164 (closure1.go:7)    RET0x00a5 00165 (closure1.go:7)    PCDATA  $0, $-20x00a5 00165 (closure1.go:7)    LEAQ    8(AX), DI0x00a9 00169 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX0x00ae 00174 (closure1.go:7)    CALL    runtime.gcWriteBarrierCX(SB)0x00b3 00179 (closure1.go:7)    JMP 1500x00b5 00181 (closure1.go:7)    NOP

汇编总是晦涩难懂。我们重点看第一行:

    0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX

我们看到对应到Go源码中创建闭包函数的第7行,这行汇编代码大致意思是将一个结构体对象的地址放入CX。我们把这个结构体对象摘录出来:

struct {F uintptri []int
}

这个结构体对象是哪里来的呢?显然是Go编译器根据闭包函数的“特征”创建出来的。其中的F就是闭包函数自身的地址,毕竟是函数,这个地址与一般函数的地址应该是在一个内存区域(比如rodata的只读数据区),那么整型切片变量i呢?难道这就是闭包函数所捕获的那个Foo函数本地变量i。没错!正是它。如果不信,我们可以再定义一个捕获更多变量的闭包函数来验证一下。

下面是一个捕获3个整型变量的闭包函数的生成函数:

// https://github.com/bigwhite/experiments/tree/master/closure/closure2.gofunc foo() func(int) int {var a, b, c int = 11, 12, 13return func(n int) int {a += nb += nc += nreturn a + b + c}
}

其对应的汇编代码中那个闭包函数结构为:

0x0084 00132 (closure2.go:10)   LEAQ    type.noalg.struct { F uintptr; "".a *int; "".b *int; "".c *int }(SB), CX

将该结构体提取出来,即:

struct {F uintptra *intb *intc *int
}

到这里,我们证实了引用了包裹函数本地变量的正是闭包函数自身,即编译器为其在内存中建立的闭包函数结构体对象。通过unsafe包,我们甚至可以输出这个闭包函数对象。以closure2.go为例,我们来尝试一下,如下面代码所示。

// https://github.com/bigwhite/experiments/tree/master/closure/closure2.gofunc foo() func(int) int {var a, b, c int = 11, 12, 13return func(n int) int {a += nb += nc += nreturn a + b + c}
}type closure struct {f uintptra *intb *intc *int
}func bar() {f := foo()f(5)pc := *(**closure)(unsafe.Pointer(&f))fmt.Printf("%#v\n", *pc)fmt.Printf("a=%d, b=%d,c=%d\n", *pc.a, *pc.b, *pc.c)f(6)fmt.Printf("a=%d, b=%d,c=%d\n", *pc.a, *pc.b, *pc.c)
}

在上面代码中,我们参考汇编的输出定义了closure这个结构体来对应内存中的闭包函数对象(每种闭包对象都是不同的,一个技巧就是参考汇编输出的对象来定义),通过unsafe的地址转换,我们将内存中的闭包对象映射到closure结构体实例上。运行上面程序,我们可以得到如下输出:

$go run closure2.go
main.closure{f:0x10a4d80, a:(*int)(0xc000118000), b:(*int)(0xc000118008), c:(*int)(0xc000118010)}
a=16, b=17,c=18
a=22, b=23,c=24

在上面的例子中,闭包函数捕获了外部变量a、b和c,这些变量实质上被编译器创建的闭包内存对象所引用。当我们调用foo函数时,闭包函数对象创建(其地址赋值给变量f)。这样,f对象一直引用着变量a、b和c。只有当f被回收,a、b和c才会因unreachable而被回收。

如果我们在闭包函数中仅仅是对捕获的外部变量进行只读操作,那么闭包函数对象不会存储这些变量的指针,而仅会做一份值拷贝。当然,如果某个变量被一个函数中创建的多个闭包所捕获,并且有的只读,有的修改,那么闭包函数对象还是会存储该变量的地址的。

了解了闭包函数的本质,我们再来看本文标题中的问题就容易多了。其答案就是在捕捉变量的闭包函数对象被回收后,如果这些被捕捉的变量没有其他引用,它们将变为unreachable的,后续就会被GC回收了

3. 小结

我们回顾一下文章开头引用的Go语言规范中对闭包诠释中提到的一句话:“只要它们可以被访问,它们就会继续存在”。现在看来,我们可以将其理解为:只要闭包函数对象存在,其捕获的那些变量就会存在,就不会被回收

闭包函数的这种机制决定了我们在日常使用过程中也要时刻考虑着闭包函数所捕获的变量可能的“延迟回收”。如果某个场景下,闭包引用的变量占用内存较大,且闭包函数对象被创建出的数量很多且因业务需要延迟很久才会被执行(比如定时器场景),这就会导致堆内存可能长期处于高水位,我们要考虑内存容量是否能承受这样的水位,如果不能,则要考虑更换实现方案了。

本文涉及的所有代码可以从这里下载[6]:https://github.com/bigwhite/experiments/tree/master/closure

4. 参考资料

  • 深入理解函数闭包 - https://zhuanlan.zhihu.com/p/56750616

  • Go语言高级编程 - https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-06-func-again.md#366-闭包函数


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列

  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践[7]”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订 阅!


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用[8]”在慕课网热卖中,欢迎小伙伴们订阅学习!


我爱发短信[9]:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展;短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[10]:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx

  • 微信公众号:iamtonybai

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

参考资料

[1]

本文永久链接: https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go

[2]

函数字面值: https://tip.golang.org/ref/spec#Function_literals

[3]

Go编译器的实现代码: https://github.com/golang/go/tree/master/src/cmd/compile/internal/gc/closure.go

[4]

在Go中,函数也是一等公民的特性: https://www.imooc.com/read/87/article/2420

[5]

Go汇编: https://tip.golang.org/doc/asm

[6]

这里下载: https://github.com/bigwhite/experiments/tree/master/closure

[7]

改善Go语⾔编程质量的50个有效实践: https://www.imooc.com/read/87

[8]

Kubernetes实战:高可用集群搭建、配置、运维与应用: https://coding.imooc.com/class/284.html

[9]

我爱发短信: https://51smspush.com/

[10]

链接地址: https://m.do.co/c/bff6eed92687

Go中被闭包捕获的变量何时会被回收相关推荐

  1. Python 中的闭包介绍

    引言 闭包是优雅的 Python 结构.在本文中,我们将了解它们,如何定义闭包,为什么以及何时使用它们. 但是在讨论什么是闭包之前,我们必须首先理解什么是嵌套函数,以及作用域规则是如何为它们工作的.那 ...

  2. 【Groovy】闭包 Closure ( 闭包类 Closure 简介 | this、owner、delegate 成员区别 | 静态闭包变量 | 闭包中定义闭包 )

    文章目录 总结 一.静态闭包变量 1.执行普通闭包变量 2.执行静态闭包变量 二. 在闭包中定义闭包 三. 完整代码示例 总结 在闭包中 , 打印 this , owner , delegate , ...

  3. JS闭包中未使用的引用变量回收机制浅探

    缘起与群里贴出的一段sizzle代码: 最后的那段指定为null是否有必要? ---------------------------------------忧郁的分割线------------ siz ...

  4. python中闭包函数_Python的闭包问题(关于内嵌函数引用闭包函数的变量问题)

    一.闭包: 记得:闭包的特性就是:内嵌函数会保存它引用的外围函数的变量值. 闭包概念:在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数和被引用的变量等 ...

  5. 在 Swift 中使用闭包实现懒加载

    本文讲的是在 Swift 中使用闭包实现懒加载, 原文地址:Swift Lazy Initialization with Closures 原文作者:Bob Lee 译文出自:掘金翻译计划 译者:ls ...

  6. golang中的闭包

    闭包 简介 作用:缩小变量作用域,减少对全局变量的污染 闭包又是什么?你可以想象一下,在一个函数中存在对外来标识符的引用.所谓的外来标识符,既不代表当前函数的任何参数或结果,也不是函数内部声明的,它是 ...

  7. c# 睡眠3秒_C#中的闭包和意想不到的坑

    转自:老胡写代码cnblogs.com/deatharthas/p/13166987.html 虽然闭包主要是函数式编程的玩意儿,而C#的最主要特征是面向对象,但是利用委托或lambda表达式,C#也 ...

  8. 从编译器层面理解C#中的闭包的这个坑!

    前言 在公众号上看到一篇文章<正确使用和理解C#中的闭包>,里面提到了闭包的一个坑: 当捕获的外部变量为for循环的迭代变量时,C#认为变量i是定义在循环体外的.所以,当添加委托集合的fo ...

  9. 正确使用和理解C#中的闭包

    定义 我们把在Lambda表达式(或匿名方法)中所引用的外部变量称为捕获变量.而捕获变量的表达式就称为闭包. 捕获变量 捕获的变量会在真正调用委托时"赋值",而不是在捕获时&quo ...

最新文章

  1. DFT实训教程笔记2(bibili版本)- Scan synthesis practice
  2. [leetcode]15.三数之和
  3. python实战,教你用微信每天给女朋友说晚安
  4. BatchNorm中forward未被调用原因
  5. mysql first value_开窗函数 First_Value 和 Last_Value
  6. mysql 导出bson格式_mongodb 导入导出GridFS【图片/文件/视频/音频等多媒体文件的导入导出】...
  7. CocoaLumberjack+XcodeColor(输出带有颜色的日志)在安装过程中遇到的问题
  8. linux session存储目录,Linux session(会话)
  9. Linux下安装Nginx与配置
  10. 录音文件怎么转换成文字呢?
  11. 对无序的边界点排序(顺时针绘制边界)
  12. Real格式的影片如何分离、合并音频视频?
  13. C++笔试题目大全(笔试宝典)(转)
  14. 有哪些常见的电脑硬盘故障?
  15. ios 拍照人像识别_Google相册为iOS用户添加了人像深度编辑和色彩弹出功能
  16. plotwidget横坐标日期_matlab中如何画以日期为横坐标的图?
  17. linux微信卡,在UOS个人版中运行Wine QQ/微信/TIM很慢,很卡的处理
  18. 基于深度学习的无人驾驶道路检测
  19. 基于YOLOv5的手势识别系统(含手势识别数据集+训练代码)
  20. 现代控制理论——李雅普诺夫第一方法

热门文章

  1. 菜刀webshell特征分析
  2. [PLC]ST语言一:LD_LDI_AND_ANI_OR_ORI
  3. 牛客网 Wannafly挑战赛20 A-染色
  4. 荒岛求生html5母狼攻,荒岛求生各资源作用及获取方法解析 荒岛求生资源怎么获得...
  5. Elasticsearch+Spring Boot集成实践
  6. 人民币大写在线转换工具
  7. python语言描述兰伯特pdf_数据结构PYTHON语言描述 [美] Kenneth A. Lambert 兰伯特
  8. CTF show 萌新区解题报告 (二)
  9. MFC: DeviceIoControl 通过API访问设备驱动程序
  10. Linux内核中断处理“下半部”机制(超详细~)