标签 汇编 下的文章

Go究竟是否为空切片分配了底层数组

本文永久链接 – https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice

这周在“Go语言第一课”的留言区看到一位同学的这样一个问题:

切片是Go语言中的一个重要的语法元素,也是日常Go开发中使用最为频繁的语法元素。有过Go语言开发经验的童鞋估计大多都知道空切片(empty slice)与nil切片(nil slice)比较的梗,这也是Go面试中的一道高频题。

var sl1 = []int{} // sl1是空切片
var sl2 []int     // sl2是nil切片

要真正理解切片,离不开运行时的切片表示。在我的专栏《Go语言精进之路》一书中都有对切片在运行时表示的细致讲解。

切片在运行时由三个字段构成,reflect包中有切片在类型系统中表示的对应的定义:

// $GOROOT/src/reflect/value.go
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

基于这个定义我们来理解空切片和nil切片就容易多了。我们用一段代码来看看这两种切片的差别:

// dumpslice.go
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    var sl1 = []int{}
    ph1 := (*reflect.SliceHeader)(unsafe.Pointer(&sl1))
    fmt.Printf("empty slice's header is %#v\n", *ph1)
    var sl2 []int
    ph2 := (*reflect.SliceHeader)(unsafe.Pointer(&sl2))
    fmt.Printf("nil slice's header is %#v\n", *ph2)
}

在这段代码中,我们通过unsafe包以及reflect.SliceHeader输出了空切片与nil切片在内存中的表示,即SliceHeader各个字段的值。我们在Go 1.18beta2下运行一下上述代码(使用-gcflags ‘-l -N’可关闭Go编译器的优化):

$go run -gcflags '-l -N' dumpslice.go
empty slice's header is reflect.SliceHeader{Data:0xc000092eb0, Len:0, Cap:0}
nil slice's header is reflect.SliceHeader{Data:0x0, Len:0, Cap:0}

通过输出结果,我们看到nil切片在运行时表示的三个字段值都是0;而空切片的len、cap值为0,但data值不为零

好了,此时我们再回到本文开始处那个童鞋提出的那个问题:空切片到底分没分配底层数组

答案是肯定的:没有分配!那么上述代码中空切片在运行时表示中第一个字段data的值0xc000092eb0从何而来,难道不是底层数组的地址么?

要想回答这个问题,我们需要下沉到汇编层面去看。

Go使用plan9的汇编语法,目前市面上关于这种汇编的资料比较少,比较权威是Go官方的asm资料和Rob Pike编写的A Manual for the Plan 9 assembler。此外IBM工程师的 Dropping down Go functions in assembly language这份资料也十分不错。国内《Go语言高级编程》一书以及曹春辉的plan9 assembly 完全解析讲解的十分全面,值得大家参考。

我们以下面这段最简单的有关空切片的代码为例:

// layout6.go

1 package main
2
3 func main() {
4     var sl = []int{}
5     _ = sl
6 }

生成go源码对应汇编代码的主要方法有:go tool compile -S xxx.go和针对编译后的二进制文件使用go tool objdump -S exe_file。

我们看看这段代码对应的汇编代码,我们使用下面命令将上述go源码转换为汇编代码(Go 1.18beta2 on darwin amd64):

$go tool compile -S -N -l layout6.go > layout6.s // -N -l两个命令行选项用于关闭Go编译器的优化,优化后的代码会掩盖实现细节

(在MacOS上)生成的layout6.s汇编代码如下(汇编代码中的FUNCDATA和PCDATA是Go编译器插入的、给GC使用的指示符,这里将其滤掉了):

"".main STEXT nosplit size=48 args=0x0 locals=0x30 funcid=0x0 align=0x0
    0x0000 00000 (layout6.go:3) TEXT    "".main(SB), NOSPLIT|ABIInternal, $48-0 // 48是main函数的栈帧大小,0表示参数大小
    0x0000 00000 (layout6.go:3) SUBQ    $48, SP
    0x0004 00004 (layout6.go:3) MOVQ    BP, 40(SP)
    0x0009 00009 (layout6.go:3) LEAQ    40(SP), BP
    0x000e 00014 (layout6.go:4) LEAQ    ""..autotmp_2(SP), AX
    0x0012 00018 (layout6.go:4) MOVQ    AX, ""..autotmp_1+8(SP)
    0x0017 00023 (layout6.go:4) TESTB   AL, (AX)
    0x0019 00025 (layout6.go:4) JMP 27
    0x001b 00027 (layout6.go:4) MOVQ    AX, "".sl+16(SP)
    0x0020 00032 (layout6.go:4) MOVUPS  X15, "".sl+24(SP)
    0x0026 00038 (layout6.go:6) MOVQ    40(SP), BP
    0x002b 00043 (layout6.go:6) ADDQ    $48, SP
    0x002f 00047 (layout6.go:6) RET
    0x0000 48 83 ec 30 48 89 6c 24 28 48 8d 6c 24 28 48 8d  H..0H.l$(H.l$(H.
    0x0010 04 24 48 89 44 24 08 84 00 eb 00 48 89 44 24 10  .$H.D$.....H.D$.
    0x0020 44 0f 11 7c 24 18 48 8b 6c 24 28 48 83 c4 30 c3  D..|$.H.l$(H..0.
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
    0x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=24
    0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0x0010 00 00 00 00 00 00 00 00                          ........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
    0x0000 01 00 00 00 00 00 00 00                          ........
gclocals·ff19ed39bdde8a01a800918ac3ef0ec7 SRODATA dupok size=9
    0x0000 01 00 00 00 04 00 00 00 00                       .........

关于汇编语法的问题,大家可以参考前面提供的参考资料,这里不赘述。我们这里最关注的是对应Go源码第4行Go代码的汇编源码,这里我把这段汇编源码单独提出来放在下面:

    0x000e 00014 (layout6.go:4) LEAQ    ""..autotmp_2(SP), AX
    0x0012 00018 (layout6.go:4) MOVQ    AX, ""..autotmp_1+8(SP)
    0x0017 00023 (layout6.go:4) TESTB   AL, (AX)
    0x0019 00025 (layout6.go:4) JMP 27
    0x001b 00027 (layout6.go:4) MOVQ    AX, "".sl+16(SP)
    0x0020 00032 (layout6.go:4) MOVUPS  X15, "".sl+24(SP)

我们逐行看一下:

  • 00014行:将SP寄存器指向的内存单元(该内存单元被命名为autotmp_2)的地址存入AX寄存器中;
  • 00019行:将AX寄存器中存储的值写入地址为SP+8的内存单元中,这个内存单元被命名为autotmp_1;
  • 00023行:将AL寄存器中的值与AX寄存器指向的内存单元的值做逻辑与操作,设置相关标志位;
  • 00025行:无条件跳转至00027行执行;
  • 00027行:将AX寄存器中存储的值写入sl切片变量运行时表示的第一个字段data中,该字段的地址为SP+16;
  • 00032行:使用intel平台上的SIMD指令集SSE的MOVUPS指令通过X15代表的固定的零寄存器对起始地址为SP+24的连续128bit(16个字节)进行清零。即sl切片变量运行时的len和cap字段被清零。

关于X15寄存器的含义,在Go internal ABI specification中有说明。

我这里用一幅图展示一下上面操作后的main函数栈情况:

我们看到切片sl的指向底层数组的指针data的值实际上是一个栈上的内存单元的地址,Go编译器并没有在堆上额外分配新的内存空间作为切片sl的底层数组。只是上面汇编代码的第00019行、00023行的操作让人很迷,不知道这两部指令操作的意图为何。

我们再来看一个例子,以进一步证实我们上面的结论。这个例子的源码如下:

// layout7.go
1 package main
2
3 func main() {
4     var sl = []int{}
5     sl = append(sl, 1)
6 }

在这个例子中,我们先是声明了一个空切片sl,之后又通过append为sl追加了一个元素。append时,由于sl为空切片,Go势必会为sl新分配底层存储数组,我们通过对比一下第4行和第5行两个操作的异同来确认“空切片并未分配底层数组”的结论。我们同样通过go tool compile -S命令得到该源码对应的汇编代码:

$go tool compile -S -N -l layout7.go > layout7.s

layout7.s中main函数的汇编代码如下(过滤掉了PCDATA和FUNCDATA指示符行):

"".main STEXT size=114 args=0x0 locals=0x70 funcid=0x0 align=0x0
    0x0000 00000 (layout7.go:3) TEXT    "".main(SB), ABIInternal, $112-0
    0x0000 00000 (layout7.go:3) CMPQ    SP, 16(R14)
    0x0004 00004 (layout7.go:3) JLS 107
    0x0006 00006 (layout7.go:3) SUBQ    $112, SP
    0x000a 00010 (layout7.go:3) MOVQ    BP, 104(SP)
    0x000f 00015 (layout7.go:3) LEAQ    104(SP), BP
    0x0014 00020 (layout7.go:4) LEAQ    ""..autotmp_2+64(SP), BX
    0x0019 00025 (layout7.go:4) MOVQ    BX, ""..autotmp_1+72(SP)
    0x001e 00030 (layout7.go:4) TESTB   AL, (BX)
    0x0020 00032 (layout7.go:4) JMP 34
    0x0022 00034 (layout7.go:4) MOVQ    BX, "".sl+80(SP)
    0x0027 00039 (layout7.go:4) MOVUPS  X15, "".sl+88(SP)
    0x002d 00045 (layout7.go:5) JMP 47
    0x002f 00047 (layout7.go:5) LEAQ    type.int(SB), AX
    0x0036 00054 (layout7.go:5) XORL    CX, CX
    0x0038 00056 (layout7.go:5) MOVQ    CX, DI
    0x003b 00059 (layout7.go:5) MOVL    $1, SI
    0x0040 00064 (layout7.go:5) CALL    runtime.growslice(SB)
    0x0045 00069 (layout7.go:5) LEAQ    1(BX), DX
    0x0049 00073 (layout7.go:5) JMP 75
    0x004b 00075 (layout7.go:5) MOVQ    $1, (AX)
    0x0052 00082 (layout7.go:5) MOVQ    AX, "".sl+80(SP)
    0x0057 00087 (layout7.go:5) MOVQ    DX, "".sl+88(SP)
    0x005c 00092 (layout7.go:5) MOVQ    CX, "".sl+96(SP)
    0x0061 00097 (layout7.go:6) MOVQ    104(SP), BP
    0x0066 00102 (layout7.go:6) ADDQ    $112, SP
    0x006a 00106 (layout7.go:6) RET
    0x006b 00107 (layout7.go:6) NOP
    0x006b 00107 (layout7.go:3) CALL    runtime.morestack_noctxt(SB)
    0x0070 00112 (layout7.go:3) JMP 0
    ... ...

有了对layout6.s的汇编的分析的基础,再来看这段汇编似乎就好很多了。首先layout7.s中对应var sl = []int{}代码的第00020到00039的原理与layout6.s一致。sl的data字段被赋值为一个栈上内存单元(SP+64)的地址。

从第00047到00073实际上是为调用runtime.growslice函数做准备以及调用runtime.growslice函数。runtime.growslice函数负责在堆上分配新的底层数组用于存储切片sl的元素。runtime.growslice返回后,我们看到,第00075行,Go将一个立即数1写入AX寄存器指向的内存单元,即growslice新分配的底层数组的第一个元素的内存单元。

之后,sl的三个字段被重新做了赋值:

    0x0052 00082 (layout7.go:5) MOVQ    AX, "".sl+80(SP)
    0x0057 00087 (layout7.go:5) MOVQ    DX, "".sl+88(SP)
    0x005c 00092 (layout7.go:5) MOVQ    CX, "".sl+96(SP)

我们看到:00082行,sl的data字段(SP+80)被赋值为AX寄存器中的值,即堆上分配新的底层数组的地址。而后的len和cap字段也分配用DX和CX寄存器的值做了赋值,这两个寄存器分配存储了切片的len和cap。

我这里同样用一幅示意图展示append后main函数栈的情况:

通过这个例子,我们可以看到,如果Go在堆上为切片分配底层数组,我们会在汇编代码中看到growslice或newobject这样的调用。

如果一个非空切片没有逃逸到堆上,那么Go也可能在栈上为该切片分配底层数组空间,比如下面这段代码:

// layout10.go
package main

func main() {
    var sl = []int{11, 12, 13}
    _ = sl
}

它对应的汇编如下:

"".main STEXT nosplit size=103 args=0x0 locals=0x40 funcid=0x0 align=0x0
    0x0000 00000 (layout10.go:3)    TEXT    "".main(SB), NOSPLIT|ABIInternal, $64-0
    0x0000 00000 (layout10.go:3)    SUBQ    $64, SP
    0x0004 00004 (layout10.go:3)    MOVQ    BP, 56(SP)
    0x0009 00009 (layout10.go:3)    LEAQ    56(SP), BP
    0x000e 00014 (layout10.go:4)    MOVUPS  X15, ""..autotmp_2(SP)
    0x0013 00019 (layout10.go:4)    MOVUPS  X15, ""..autotmp_2+8(SP)
    0x0019 00025 (layout10.go:4)    LEAQ    ""..autotmp_2(SP), AX
    0x001d 00029 (layout10.go:4)    MOVQ    AX, ""..autotmp_1+24(SP)
    0x0022 00034 (layout10.go:4)    TESTB   AL, (AX)
    0x0024 00036 (layout10.go:4)    MOVQ    $11, ""..autotmp_2(SP)
    0x002c 00044 (layout10.go:4)    TESTB   AL, (AX)
    0x002e 00046 (layout10.go:4)    MOVQ    $12, ""..autotmp_2+8(SP)
    0x0037 00055 (layout10.go:4)    TESTB   AL, (AX)
    0x0039 00057 (layout10.go:4)    MOVQ    $13, ""..autotmp_2+16(SP)
    0x0042 00066 (layout10.go:4)    TESTB   AL, (AX)
    0x0044 00068 (layout10.go:4)    JMP 70
    0x0046 00070 (layout10.go:4)    MOVQ    AX, "".sl+32(SP)
    0x004b 00075 (layout10.go:4)    MOVQ    $3, "".sl+40(SP)
    0x0054 00084 (layout10.go:4)    MOVQ    $3, "".sl+48(SP)
    0x005d 00093 (layout10.go:6)    MOVQ    56(SP), BP
    0x0062 00098 (layout10.go:6)    ADDQ    $64, SP
    0x0066 00102 (layout10.go:6)    RET

这段汇编代码就留给大家自己阅读分析吧。


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

img{512x368}

img{512x368}
img{512x368}
img{512x368}

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

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址: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

微信赞赏:
img{512x368}

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

Go中被闭包捕获的变量何时会被回收

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

1. Go函数闭包

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

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

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

// $GOROOT/src/net/http/fileTransport.go

00 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编译器的实现代码中,使用的是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] + 10
    fmt.Println(sl)
}

func main() {
    f2()
}

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

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

// https://github.com/bigwhite/experiments/tree/master/closure/closure1.go

 1 package main
 2
 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中,函数也是一等公民的特性”。难道是闭包函数这一对象引用了foo函数的本地变量? 那么闭包函数在内存布局上是如何引用到foo函数的本地整型切片变量i的呢?闭包函数在内存布局中被映射为什么了呢?

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

2. Go闭包函数对象

要解答这个问题,我们只能寻求Go汇编的帮助。我们生成上面的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.s

    0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX
    0x0059 00089 (closure1.go:7)    MOVQ    CX, (SP)
    0x005d 00093 (closure1.go:7)    PCDATA  $1, $1
    0x005d 00093 (closure1.go:7)    NOP
    0x0060 00096 (closure1.go:7)    CALL    runtime.newobject(SB)
    0x0065 00101 (closure1.go:7)    MOVQ    8(SP), AX
    0x006a 00106 (closure1.go:7)    LEAQ    "".foo.func1(SB), CX
    0x0071 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, $-2
    0x0084 00132 (closure1.go:7)    CMPL    runtime.writeBarrier(SB), $0
    0x008b 00139 (closure1.go:7)    JNE 165
    0x008d 00141 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX
    0x0092 00146 (closure1.go:7)    MOVQ    CX, 8(AX)
    0x0096 00150 (closure1.go:7)    PCDATA  $0, $-1
    0x0096 00150 (closure1.go:7)    MOVQ    AX, "".~r0+40(SP)
    0x009b 00155 (closure1.go:7)    MOVQ    24(SP), BP
    0x00a0 00160 (closure1.go:7)    ADDQ    $32, SP
    0x00a4 00164 (closure1.go:7)    RET
    0x00a5 00165 (closure1.go:7)    PCDATA  $0, $-2
    0x00a5 00165 (closure1.go:7)    LEAQ    8(AX), DI
    0x00a9 00169 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX
    0x00ae 00174 (closure1.go:7)    CALL    runtime.gcWriteBarrierCX(SB)
    0x00b3 00179 (closure1.go:7)    JMP 150
    0x00b5 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 uintptr
    i []int
}

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

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

// https://github.com/bigwhite/experiments/tree/master/closure/closure2.go

func foo() func(int) int {
    var a, b, c int = 11, 12, 13
    return func(n int) int {
        a += n
        b += n
        c += n
        return a + b + c
    }
}

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

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

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

struct {
    F uintptr
    a *int
    b *int
    c *int
}

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

// https://github.com/bigwhite/experiments/tree/master/closure/closure2.go

func foo() func(int) int {
    var a, b, c int = 11, 12, 13
    return func(n int) int {
        a += n
        b += n
        c += n
        return a + b + c
    }
}

type closure struct {
    f uintptr
    a *int
    b *int
    c *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语言规范中对闭包诠释中提到的一句话:“只要它们可以被访问,它们就会继续存在”。现在看来,我们可以将其理解为:只要闭包函数对象存在,其捕获的那些变量就会存在,就不会被回收

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

本文涉及的所有代码可以从这里下载: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个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

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

img{512x368}

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

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址: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

微信赞赏:
img{512x368}

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats