乐趣区

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

退出移动版