在go的世界中,函数是一等公民,能够给变量赋值,能够做为参数传递,也能够直接赋值。闭包
package main import ( "fmt" "time" ) func A() { // ... fmt.Println("this is a") } func B(f func()) { // ... } func C() func() { return A } var f func() = C() func main() { time.Sleep(time.Minute) v := C() v() }
在go语言中将这样的变量、参数、返回值,即在堆空间和栈空间中绑定函数的值,称为function value异步
函数的指令在编译期间生成,使用go tool compile -S main.go
能够获取汇编代码, 以OSX 10.15.6,go 1.14为例,将看到下述汇编代码(下面只引用部分)函数
... "".B STEXT nosplit size=1 args=0x8 locals=0x0 0x0000 00000 (main.go:9) TEXT "".B(SB), NOSPLIT|ABIInternal, $0-8 0x0000 00000 (main.go:9) PCDATA $0, $-2 0x0000 00000 (main.go:9) PCDATA $1, $-2 0x0000 00000 (main.go:9) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB) 0x0000 00000 (main.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:9) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:11) PCDATA $0, $-1 0x0000 00000 (main.go:11) PCDATA $1, $-1 0x0000 00000 (main.go:11) RET 0x0000 c3 ...
运行时将存放在__TEXT段中,也就是存放在代码段中,读写权限为rx/rwx, 经过vmmap [pid]
能够获取运行时的内存分布this
==== Non-writable regions for process 13443 REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL __TEXT 0000000001000000-0000000001167000 [ 1436K 1436K 0K 0K] r-x/rwx SM=COW .../test
使用otool -v -l [file]
能够看到下述内容(下面只引用了一部分)spa
... Load command 1 cmd LC_SEGMENT_64 cmdsize 632 segname __TEXT vmaddr 0x0000000001000000 vmsize 0x0000000000167000 fileoff 0 filesize 1470464 maxprot rwx initprot r-x nsects 7 flags (none) Section sectname __text segname __TEXT addr 0x0000000001001000 size 0x000000000009c365 offset 4096 align 2^4 (16) reloff 0 nreloc 0 type S_REGULAR attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS reserved1 0 reserved2 0 ...
因此若是要问函数在go语言里的本质是什么,那么其实就是指向__TEXT段内存地址的一个指针指针
在go语言中,每个goroutine持有一个连续栈,栈基础大小为2kb,当栈大小超过预分配大小后,会触发栈扩容,也就是分配一个大小为当前栈2倍的新栈,而且将原来的栈拷贝到新的栈上。使用连续栈而不是分段栈的目的是,利用局部性优点提高执行速度,原理是CPU读取地址时会将相邻的内存读取到访问速度比内存快的多级cache中,地址连续性越好,L一、L二、L3 cache命中率越高,速度也就越快。code
在go中,和其余一些语言有所不一样,函数的返回值、参数都是由被caller保存。每次函数调用时,会在caller的栈中压入函数返回值列表、参数列表、函数返回时的PC地址,而后更改bp和pc为新函数,执行新函数,执行完以后将变量存到caller的栈空间中,利用栈空间中保存的返回地址和caller的栈基地址,恢复pc和sp回到caller的执行过程。内存
对于栈变量的访问是经过bp+offset的方式来访问,而对于在堆上分配的变量来讲,就是经过地址来访问。在go中,变量被分配到堆上仍是被分配到栈上是由编译器在编译时根据逃逸分析决定的,不能够更改,只能利用规则尽可能让变量被分配到栈上,由于局部性优点,栈空间的内存访问速度快于堆空间访问。get
go里面其实方法就是语法糖,请看下述代码,两个Println打印的结果是同样的,实际上Method就是将receiver做为函数的第一个参数输入的语法糖而已,本质上和函数没有区别cmd
type T struct { name string } func (t T) Name() string { return "Hi! " + t.name } func main() { t := T{name: "test"} fmt.Println(t.Name()) // Hi! test fmt.Println(T.Name(t)) // Hi! test }
前面已经提到在go语言中将这在堆空间和栈空间中绑定函数的值,称为function value。这也就是闭包在go语言中的实体。一个最简单的funcval其实是经过二级指针指向__TEXT代码段上函数的结构体。
那咱们来看下面这个闭包,也就是main函数中的变量f
func getFunc() func() int { a := 0 return func() int { a++ return a } } func main() { f := getFunc() for i := 0; i < 10; i++ { fmt.Println(f()) } }
上面这段代码执行完后会输出1~10,也就是说f在执行的时候所使用的a会累计,可是a并非一个全局变量,为何f就变成了一个有状态的函数呢?其实这也就是go里面的闭包了。那咱们来看go是如何实现闭包的。
首先来解释一下闭包的含义,闭包在实现上一个结构体,须要存储函数入口和关联环境,关联环境包含约束变量(函数内部变量)和自由变量(函数外部变量,在函数外被定义,可是在函数内被引用),和函数不一样的事,在捕获闭包时才能肯定自由变量,当脱离了捕捉变量的上下文时,也能照常运行。基于闭包能够很容易的定义异步调用的回调函数。
在go语言中,闭包的状态是经过捕获列表实现的。具体来讲,有自由变量的闭包funcval的分配都在堆上,(没有自由变量的funcval在__DATA数据段上,和常量同样),funcval中除了包含地址之外,还会包含所引用的自由变量,全部自由变量构成捕获列表。对于会被修改的值,捕获的是值的指针,对于不会被修改的值,捕获的是值拷贝。