关于golang:函数go世界中的一等公民

函数的实质

在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]能够获取运行时的内存散布

==== 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]能够看到下述内容(上面只援用了一部分)

...
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中,地址连续性越好,L1、L2、L3 cache命中率越高,速度也就越快。

在go中,和其余一些语言有所不同,函数的返回值、参数都是由被caller保留。每次函数调用时,会在caller的栈中压入函数返回值列表、参数列表、函数返回时的PC地址,而后更改bp和pc为新函数,执行新函数,执行完之后将变量存到caller的栈空间中,利用栈空间中保留的返回地址和caller的栈基地址,复原pc和sp回到caller的执行过程。

对于栈变量的拜访是通过bp+offset的形式来拜访,而对于在堆上调配的变量来说,就是通过地址来拜访。在go中,变量被调配到堆上还是被调配到栈上是由编译器在编译时依据逃逸剖析决定的,不能够更改,只能利用规定尽量让变量被调配到栈上,因为局部性劣势,栈空间的内存访问速度快于堆空间拜访。

办法的实质

go外面其实办法就是语法糖,请看下述代码,两个Println打印的后果是一样的,实际上Method就是将receiver作为函数的第一个参数输出的语法糖而已,实质上和函数没有区别

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中除了蕴含地址以外,还会蕴含所援用的自在变量,所有自在变量形成捕捉列表。对于会被批改的值,捕捉的是值的指针,对于不会被批改的值,捕捉的是值拷贝。