Go函数闭包底层实现(误删,重发)

原创 机器铃砍菜刀 Golang技术分享 前天函数闭包对于大多数读者而言并不是什么高级词汇,那什么是闭包呢?这里摘自Wiki上的定义:

a closure is a record storing a function together with an environment.The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

简而言之,闭包是一个由函数和引用环境而组合的实体。闭包在实现过程中,往往是通过调用外部函数返回其内部函数来实现的。在这其中,引用环境是指的外部函数中自由变量(内部函数使用,但是却定义于外部函数中)的映射。内部函数通过引入外部的自由变量,使得这些变量即使离开了外部函数的环境也不会被释放或删除,在返回的内部函数仍然持有这些信息。

这段话可能不太好理解,我们直接用看例子。

 1package main 2 3import "fmt" 4 5func outer() func() int { 6    x := 1 7    return func() int { 8        x++ 9        return x10    }11}1213func main() {14    closure := outer()15    fmt.Println(closure())16    fmt.Println(closure())17}1819// output202213

可以看到,Go中的两条特性(函数是一等公民,支持匿名函数)使其很容易实现闭包。

在上面的例子中,closure是闭包函数,变量x就是引用环境,它们的组合就是闭包实体。x本是outer函数之内,匿名函数之外的局部变量。在正常函数调用结束之后,x就会随着函数栈的销毁而销毁。但是由于匿名函数的引用,outer返回的函数对象会一直持有x变量。这造成了每次调用闭包closurex变量都会得到累加。

这里和普通的函数调用不一样:局部变量x并没有随着函数的调用结束而消失。那么,这是为什么呢?

实现原理

我们不妨从汇编入手,将上述代码稍微修改一下

 1package main 2 3func outer() func() int { 4    x := 1 5    return func() int { 6        x++ 7        return x 8    } 9}1011func main() {12    _ := outer()13}

得到编译后的汇编语句如下。

 1$ go tool compile -S -N -l main.go  2"".outer STEXT size=181 args=0x8 locals=0x28 3        0x0000 00000 (main.go:3)        TEXT    "".outer(SB), ABIInternal, $40-8 4        ... 5        0x0021 00033 (main.go:3)        MOVQ    $0, "".~r0+48(SP) 6        0x002a 00042 (main.go:4)        LEAQ    type.int(SB), AX 7        0x0031 00049 (main.go:4)        MOVQ    AX, (SP) 8        0x0035 00053 (main.go:4)        PCDATA  $1, $0 9        0x0035 00053 (main.go:4)        CALL    runtime.newobject(SB)10        0x003a 00058 (main.go:4)        MOVQ    8(SP), AX11        0x003f 00063 (main.go:4)        MOVQ    AX, "".&x+24(SP)12        0x0044 00068 (main.go:4)        MOVQ    $1, (AX)13        0x004b 00075 (main.go:5)        LEAQ    type.noalg.struct { F uintptr; "".x *int }(SB), AX14        0x0052 00082 (main.go:5)        MOVQ    AX, (SP)15        0x0056 00086 (main.go:5)        PCDATA  $1, $116        0x0056 00086 (main.go:5)        CALL    runtime.newobject(SB)17        0x005b 00091 (main.go:5)        MOVQ    8(SP), AX18        0x0060 00096 (main.go:5)        MOVQ    AX, ""..autotmp_4+16(SP)19        0x0065 00101 (main.go:5)        LEAQ    "".outer.func1(SB), CX20        0x006c 00108 (main.go:5)        MOVQ    CX, (AX)21        ...

首先,我们发现不一样的是 x:=1 会调用 runtime.newobject 函数(内置new函数的底层函数,它返回数据类型指针)。在正常函数局部变量的定义时,例如

 1package main 2 3func add() int { 4    x := 100 5    x++ 6    return x 7} 8 9func main() {10    _ = add()11}

我们能发现 x:=100 是不会调用 runtime.newobject 函数的,它对应的汇编是如下

1"".add STEXT nosplit size=58 args=0x8 locals=0x102        0x0000 00000 (main.go:3)        TEXT    "".add(SB), NOSPLIT|ABIInternal, $16-83        ...4        0x000e 00014 (main.go:3)        MOVQ    $0, "".~r0+24(SP)5        0x0017 00023 (main.go:4)        MOVQ    $100, "".x(SP)  // x:=1006        0x001f 00031 (main.go:5)        MOVQ    $101, "".x(SP)7        0x0027 00039 (main.go:6)        MOVQ    $101, "".~r0+24(SP)8        ...

留着疑问,继续往下看。我们发现有以下语句

1        0x004b 00075 (main.go:5)        LEAQ    type.noalg.struct { F uintptr; "".x *int }(SB), AX2        0x0052 00082 (main.go:5)        MOVQ    AX, (SP)3        0x0056 00086 (main.go:5)        PCDATA  $1, $14        0x0056 00086 (main.go:5)        CALL    runtime.newobject(SB)

我们看到 type.noalg.struct { F uintptr; "".x *int }(SB),这其实就是定义的一个闭包数据类型,它的结构表示如下

1type closure struct {2    F uintptr   // 函数指针,代表着内部匿名函数3    x *int      // 自由变量x,代表着对外部环境的引用4}

之后,在通过 runtime.newobject 函数创建了闭包对象。而且由于 LEAQ xxx yyy代表的是将 xxx 指针,传递给 yyy,因此 outer 函数最终的返回,其实是闭包结构体对象指针。在《详解逃逸分析》一文中,我们详细地描述了Go编译器的逃逸分析机制,对于这种函数返回暴露给外部指针的情况,很明显,闭包对象会被分配至堆上,变量x也会随着对象逃逸至堆。这就很好地解释了为什么x变量没有随着函数栈的销毁而消亡。

我们可以通过逃逸分析来验证我们的结论

 1$  go build -gcflags '-m -m -l' main.go 2# command-line-arguments 3./main.go:6:3: outer.func1 capturing by ref: x (addr=true assign=true width=8) 4./main.go:5:9: func literal escapes to heap: 5./main.go:5:9:   flow: ~r0 = &{storage for func literal}: 6./main.go:5:9:     from func literal (spill) at ./main.go:5:9 7./main.go:5:9:     from return func literal (return) at ./main.go:5:2 8./main.go:4:2: x escapes to heap: 9./main.go:4:2:   flow: {storage for func literal} = &x:10./main.go:4:2:     from func literal (captured by a closure) at ./main.go:5:911./main.go:4:2:     from x (reference) at ./main.go:6:312./main.go:4:2: moved to heap: x                   // 变量逃逸13./main.go:5:9: func literal escapes to heap       // 函数逃逸

至此,我相信读者已经明白为什么闭包能持续持有外部变量的原因了。那么,我们来思考上文中留下的疑问,为什么在x:=1 时会调用 runtime.newobject 函数。

我们将上文中的例子改为如下,即删掉 x++ 代码

 1package main 2 3func outer() func() int { 4    x := 1 5    return func() int { 6        return x 7    } 8} 910func main() {11    _ = outer()12}

此时,x:=1处的汇编代码,将不再调用 runtime.newobject 函数,通过逃逸分析也会发现将x不再逃逸,生成的闭包对象中的x的将是值类型int

1type closure struct {2    F uintptr 3    x int      4}

这其实就是Go编译器做得精妙的地方:当闭包内没有对外部变量造成修改时,Go 编译器会将自由变量的引用传递优化为直接值传递,避免变量逃逸。

总结

函数闭包一点也不神秘,它就是函数和引用环境而组合的实体。在Go中,闭包在底层是一个结构体对象,它包含了函数指针与自由变量。

Go编译器的逃逸分析机制,会将闭包对象分配至堆中,这样自由变量就不会随着函数栈的销毁而消失,它能依附着闭包实体而一直存在。因此,闭包使用的优缺点是很明显的:闭包能够避免使用全局变量,转而维持自由变量长期存储在内存之中;但是,这种隐式地持有自由变量,在使用不当时,会很容易造成内存浪费与泄露。

在实际的项目中,闭包的使用场景并不多。当然,如果你的代码中写了闭包,例如你写的某回调函数形成了闭包,那就需要谨慎一些,否则内存的使用问题也许会给你带来麻烦。

不喜欢
不看的原因确定内容质量低不看此公众号
(0)

相关推荐

  • Go 语言的参数传递

    前言 对于一门编程语言,在我们调用一个函数并且传递参数的时候,可能会下意识的去思考,到底是按值传递(by value) 还是按引用(by reference) 传递. 首先,在 Go 的 faq 中明 ...

  • Go 切片传递的隐藏危机

    提出疑问 在Go的源码库或者其他开源项目中,会发现有些函数在需要用到切片入参时,它采用是指向切片类型的指针,而非切片类型.这里未免会产生疑问:切片底层不就是指针指向底层数组数据吗,为何不直接传递切片, ...

  • Go程是如何创建和何时销毁的?

    Go程如何创建? 通过go关键字进行创建,看一下代码,很简单: go test(j) // test是一个函数 Go程如何销毁,何时销毁? 创建一个Go程简单,但何时销毁呢?这个问题稍微有点复杂,看个 ...

  • C语言学习篇(15)-----函数传参详解

    https://m.toutiao.com/is/JpuAcLb/ 前面我们已经介绍过什么是指针,指针变量的用法等等,今天我们就来讲讲什么是函数,函数有啥作用,函数的参数有哪些需要注意的地方以及指针与 ...

  • 一文吃透 Go 语言解密之接口 interface

    大家好,我是煎鱼. 自古流传着一个传言...在 Go 语言面试的时候必有人会问接口(interface)的实现原理.这又是为什么?为何对接口如此执着? 实际上,Go 语言的接口设计在整体扮演着非常重要 ...

  • 用一个例子理解JS函数的底层处理机制

    个人笔记,如有错误烦请指正 以下面代码的运行举例,一行行进行运行的解析 var x = [12, 23]; function fn(y) { y[0] = 100; y = [100]; y[1] = ...

  • Python 中的函数装饰器和闭包

    函数装饰器可以被用于增强方法的某些行为,如果想自己实现装饰器,则必须了解闭包的概念. 装饰器的基本概念 装饰器是一个可调用对象,它的参数是另一个函数,称为被装饰函数.装饰器可以修改这个函数再将其返回, ...

  • JS匿名函数和闭包

    $(function() {}) 是$(document).ready(function()的简写, 这个函数什么时候执行的呢? 答案:DOM 加载完毕之后执行. 立即执行函数(function(){ ...

  • Go 面试题 013:Go 中闭包的底层原理是?

    大家好,我是明哥. 欢迎大家再次来到  『Go 语言面试题库』 这个专栏 本篇问题:Go 中闭包的底层原理? # 1. 什么是闭包? 一个函数内引用了外部的局部变量,这种现象,就称之为闭包. 例如下面 ...

  • 初中数学反比例函数闯关难题,建议收藏!

    i初中数学 公众号 初中数学反比例函数闯关难题 i初中数学 爱 · 初中数学,是一个由数学名师团发起的公众号,旨在为初中生提供数学同步知识学习,同步习题训练,期中期末知识要点总结,期中期末模拟试卷测评 ...

  • 函数考点全突破(十三)二次函数问题中四边形面积最值问题

    春熙初中数学 25篇原创内容 公众号 初中数学解题思路 本号致力于初中数学学习的钻研和探索.全面覆盖初中数学典型题集.解题模型.动点最值.思路方法.超级易错.几何辅助线.压轴破解等方面,欢迎关注! 1 ...

  • 函数考点全突破(十四)二次函数中特殊平行四边形的存在性问题

    春熙初中数学 25篇原创内容 公众号 初中数学解题思路 本号致力于初中数学学习的钻研和探索.全面覆盖初中数学典型题集.解题模型.动点最值.思路方法.超级易错.几何辅助线.压轴破解等方面,欢迎关注! 1 ...

  • 秒杀导数压轴题:之同构式下的函数体系#数...

    秒杀导数压轴题:之同构式下的函数体系#数...