GO闭包实现原理(汇编级讲解)

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

go语言闭包实现原理(汇编层解析)

1.起因

今天开始学习go语言,在学到go闭包时候,原本以为go闭包的实现方式就是类似于如下cpp lambda

  1. value通过值传递,mutable修饰可以让value可以修改,但是地址不可能一样
  2. value通过引用传递,但是在其他地方调用时,这个value局部变量早就释放,会访问到脏数据
std::function<void()> func(){
   
   
    int value = 666;
    return [value]()mutable{
   
   
            value++;
            std::cout << &value;
        }
}

但是经过测试,我居然惊奇的发现在go的fun函数闭包value变量的地址一模一样,但是在c++的理解中这是不可能的(c++中栈随着函数退出而销毁,value也成为脏数据,一旦访问很可能会读到意料之外的数据)

image-20231130212413187

image-20231130212421755

image-20231130212432275

2.探索

于是,我查看了go编译后的汇编代码,首先先看看闭包经典代码,再看删除第六行不在闭包内修改变量value的变化的汇编代码

1.闭包经典代码

image-20231130221210920

image-20231130221309176

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

2.删除第六行代码,不在闭包中修改value

image-20231130221400666

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

image-20231130223626467

对比两段代码的汇编,我们可以看见一个数据结构:闭包对象数据结构

type noalg struct{
   
   
    F unitptr    //函数对象
    X0 *int        //第一段代码的汇编,在闭包内修改对象
    //X0 int    //第二段代码的汇编,不在闭包内修改对象
}

之后,在通过 runtime.newobject 函数创建了闭包对象。而且由于 LEAQ xxx yyy代表的是将 xxx 指针,传递给 yyy,因此 outer 函数最终的返回,其实是闭包结构体对象指针。很明显,闭包对象会被分配至堆上,变量x也会随着对象逃逸至堆。这就很好地解释了为什么x变量没有随着函数栈的销毁而消亡。

验证

通过go build指令的逃逸分析,可以看见,第一段代码的变量value和函数对象都分配到了堆上面

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

PS D:\goProject\src\learn> go build -gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:4:6: fun capturing by ref: value (addr=false assign=true width=8)
./main.go:7:13: value escapes to heap:
./main.go:7:13:   flow: {storage for ... argument} = &{storage for value}:
./main.go:7:13:     from value (spill) at ./main.go:7:13
./main.go:7:13:     from ... argument (slice-literal-element) at ./main.go:7:12
./main.go:7:13:   flow: {heap} = {storage for ... argument}:
./main.go:7:13:     from ... argument (spill) at ./main.go:7:12
./main.go:7:13:     from fmt.Print(... argument...) (call parameter) at ./main.go:7:12
./main.go:5:9: func literal escapes to heap:
./main.go:5:9:   flow: ~r0 = &{storage for func literal}:
./main.go:5:9:     from func literal (spill) at ./main.go:5:9
./main.go:5:9:     from return func literal (return) at ./main.go:5:2
./main.go:4:6: value escapes to heap:
./main.go:4:6:   flow: {storage for func literal} = &value:
./main.go:4:6:     from value (captured by a closure) at ./main.go:6:3
./main.go:4:6:     from value (reference) at ./main.go:6:3
./main.go:4:6: moved to heap: value                //变量value逃逸
./main.go:5:9: func literal escapes to heap        //函数逃逸
./main.go:7:12: ... argument does not escape
./main.go:7:13: value escapes to heap

总结

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

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

目录
相关文章
|
7月前
|
Go
Go 语言 errgroup 库的使用方式和实现原理
Go 语言 errgroup 库的使用方式和实现原理
69 0
|
7月前
|
Go
Go 语言使用 goroutine 运行闭包的“坑”
Go 语言使用 goroutine 运行闭包的“坑”
38 0
|
7月前
|
Serverless Go
Go语言闭包不打烊,让你长见识!
Go语言闭包不打烊,让你长见识!
30 0
|
2天前
|
Go C++
go 语言回调函数和闭包
go 语言回调函数和闭包
|
2天前
|
存储 安全 Java
Go Slice的底层实现原理深度解析
在Go语言的世界里,切片(Slice)是一种极其重要的数据结构,它以其灵活性和高效性在众多编程场景中扮演着核心角色。本文将深入探讨Go切片的底层实现原理,通过实例和源码分析,带你领略Go语言设计之美。
|
6月前
|
存储 Java 编译器
GO 中 defer的实现原理
GO 中 defer的实现原理
|
6月前
|
监控 Go C++
GO 中 Chan 实现原理分享
GO 中 Chan 实现原理分享
|
6月前
|
存储 搜索推荐 Go
GO 中 map 的实现原理
GO 中 map 的实现原理
|
6月前
|
JSON Go 数据格式
GO 中 slice 的实现原理
GO 中 slice 的实现原理
GO 中 slice 的实现原理
|
6月前
|
存储 监控 搜索推荐
GO 中 string 的实现原理
GO 中 string 的实现原理