函數——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]能夠獲取運行時的內存分佈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中,變量被分配到堆上仍是被分配到棧上是由編譯器在編譯時根據逃逸分析決定的,不能夠更改,只能利用規則儘可能讓變量被分配到棧上,由於局部性優點,棧空間的內存訪問速度快於堆空間訪問。
image-20200722194628782.pngget

方法的本質

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代碼段上函數的結構體。

image-20200722205328897.png
那咱們來看下面這個閉包,也就是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中除了包含地址之外,還會包含所引用的自由變量,全部自由變量構成捕獲列表。對於會被修改的值,捕獲的是值的指針,對於不會被修改的值,捕獲的是值拷貝。

image-20200722213320940.png

相關文章
相關標籤/搜索