一文告訴你神奇的 Go 內建函式原始碼在哪裡

bigwhite-github發表於2020-12-21

img{512x368}

Go 內建函式原始碼,我好像在哪裡見過你。 - 佚名

1. 何為 Go 內建函式

眾所周知,Go 是最簡單的主流程式語言之一,截至Go 1.15 版本,Go 語言的關鍵字的規模依舊保持在 25 個:

img{512x368}

很多剛入門的 gopher 可能會問:像 bool、byte、error、true、iota甚至 int 都難道都不是關鍵字?沒錯!和其他語言不同,這些識別符號並不是關鍵字,在 Go 中它們被稱為預定義識別符號。這些識別符號擁有universe block 作用域(關於 go 程式碼塊作用域的詳細解析,可參考我的技術專欄:“改善 Go 語⾔程式設計質量的 50 個有效實踐”),可以在任何原始碼位置使用。

img{512x368}

從上圖我們看到:所謂的Go 內建函式也包含在這個預定義識別符號集合中,只是這些識別符號被用作函式名稱識別符號罷了。

2. 預定義識別符號可被 override

Go 語言的關鍵字是保留的,我們無法將其用於規範之外的其他場合,比如作為變數的識別符號。但是預定義識別符號不是關鍵字,我們可以 override 它們。下面就是一個對預設表示整型型別的預定義識別符號int進行 override 的例子:

package main

import (
        "fmt"
        "unsafe"
)

type int int8

func main() {
        var a int = 5
        fmt.Printf("%T\n", a) // main.int,而不是int
        fmt.Println(unsafe.Sizeof(a)) // 1,而不是8
}

在上述這個原始檔中,預定義識別符號 int 被 override 為一個自定義型別 int,該型別的 underlying type 為 int8,於是當我們輸出該型別變數 (程式碼中的變數 a) 的型別和長度時,我們得到的是 main.int 和 1,而不是 int 和 8。

3. 預定義識別符號的宣告原始碼在哪裡

Go 是開源的程式語言,這些預定義識別符號想必也都有自己的 “歸宿” 吧,的確是這樣的。Go 的每個發行版都帶有一份原始碼,而預定義識別符號就在這份原始碼中。

Go 1.14為例,我們可以在下面路徑中找到預定義識別符號的原始碼:

$GOROOT/src/builtin/builtin.go

以 string、int、uint 這幾個代表原生型別的預定義識別符號為例,它們的宣告程式碼如下:

// $GOROOT/src/builtin/builtin.go

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
type int int

// uint is an unsigned integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, uint32.
type uint uint

同時,我們利用go doc builtin.int也可以檢視預定義識別符號 int 的文件:

$go doc builtin.int
package builtin // import "builtin"

type int int
    int is a signed integer type that is at least 32 bits in size. It is a
    distinct type, however, and not an alias for, say, int32.

func cap(v Type) int
func copy(dst, src []Type) int
func len(v Type) int

4. 內建函式的原始碼在哪裡?

作為預宣告識別符號子集的內建函式們在 builtin.go 中也都有自己的位置,比如:以 append 這個內建函式為例,我們可以在 Go 安裝包的 builtin.go 中找到它的原型 (Go 1.14):

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

但我們驚奇的發現:這裡沒有 append 函式的實現。那麼 append 內建函式實現的原始碼究竟在哪裡呢?本質上講 append 函式,包括其他內建函式其實並沒有自己的實現原始碼。

內建函式僅僅是一個識別符號,在 Go 原始碼編譯期間,Go 編譯器遇到內建函式識別符號時會將其替換為若干 runtime 的呼叫,我們還以 append 函式為例,我們輸出下面程式碼的彙編程式碼 (Go 1.14):

// append.go
package main

import "fmt"

func main() {
 var s = []int{5, 6}
 s = append(s, 7, 8)
 fmt.Println(s)
}

$go tool compile -S append.go > append.s

彙編節選如下 (append.s):

"".main STEXT size=277 args=0x0 locals=0x58
 0x0000 00000 (xxx.go:5) TEXT "".main(SB), ABIInternal, $88-0
 0x0000 00000 (xxx.go:5) MOVQ (TLS), CX
 0x0009 00009 (xxx.go:5) CMPQ SP, 16(CX)
 0x000d 00013 (xxx.go:5) PCDATA $0, $-2
 0x000d 00013 (xxx.go:5) JLS  267
 0x0013 00019 (xxx.go:5) PCDATA $0, $-1
 0x0013 00019 (xxx.go:5) SUBQ $88, SP
 0x0017 00023 (xxx.go:5) MOVQ BP, 80(SP)
 0x001c 00028 (xxx.go:5) LEAQ 80(SP), BP
 0x0021 00033 (xxx.go:5) PCDATA $0, $-2
 0x0021 00033 (xxx.go:5) PCDATA $1, $-2
 0x0021 00033 (xxx.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
 0x0021 00033 (xxx.go:5) FUNCDATA $1, gclocals·568470801006e5c0dc3947ea998fe279(SB)
 0x0021 00033 (xxx.go:5) FUNCDATA $2, gclocals·bfec7e55b3f043d1941c093912808913(SB)
 0x0021 00033 (xxx.go:5) FUNCDATA $3, "".main.stkobj(SB)
 0x0021 00033 (xxx.go:6) PCDATA $0, $1
 0x0021 00033 (xxx.go:6) PCDATA $1, $0
 0x0021 00033 (xxx.go:6) LEAQ type.[2]int(SB), AX
 0x0028 00040 (xxx.go:6) PCDATA $0, $0
 0x0028 00040 (xxx.go:6) MOVQ AX, (SP)
 0x002c 00044 (xxx.go:6) CALL runtime.newobject(SB)
 0x0031 00049 (xxx.go:6) PCDATA $0, $1
 0x0031 00049 (xxx.go:6) MOVQ 8(SP), AX
 0x0036 00054 (xxx.go:6) MOVQ $5, (AX)
 0x003d 00061 (xxx.go:6) MOVQ $6, 8(AX)
 0x0045 00069 (xxx.go:7) PCDATA $0, $2
 0x0045 00069 (xxx.go:7) LEAQ type.int(SB), CX
 0x004c 00076 (xxx.go:7) PCDATA $0, $1
 0x004c 00076 (xxx.go:7) MOVQ CX, (SP)
 0x0050 00080 (xxx.go:7) PCDATA $0, $0
 0x0050 00080 (xxx.go:7) MOVQ AX, 8(SP)
 0x0055 00085 (xxx.go:7) MOVQ $2, 16(SP)
 0x005e 00094 (xxx.go:7) MOVQ $2, 24(SP)
 0x0067 00103 (xxx.go:7) MOVQ $4, 32(SP)
 0x0070 00112 (xxx.go:7) CALL runtime.growslice(SB)
 0x0075 00117 (xxx.go:7) PCDATA $0, $1
 0x0075 00117 (xxx.go:7) MOVQ 40(SP), AX
 0x007a 00122 (xxx.go:7) MOVQ 48(SP), CX
 0x007f 00127 (xxx.go:7) MOVQ 56(SP), DX
 0x0084 00132 (xxx.go:7) MOVQ $7, 16(AX)
 0x008c 00140 (xxx.go:7) MOVQ $8, 24(AX)
 0x0094 00148 (xxx.go:8) PCDATA $0, $0
 0x0094 00148 (xxx.go:8) MOVQ AX, (SP)
 0x0098 00152 (xxx.go:7) LEAQ 2(CX), AX
 0x009c 00156 (xxx.go:8) MOVQ AX, 8(SP)
 0x00a1 00161 (xxx.go:8) MOVQ DX, 16(SP)
 0x00a6 00166 (xxx.go:8) CALL runtime.convTslice(SB)
 ... ...

我們可以看到:append 並沒有以獨立的身份出現在 CALL 彙編指令的後面,而是被換成:runtime.growslice、runtime.convTslice 以及相關彙編指令了。


“Gopher 部落” 知識星球開球了!高品質首發 Go 技術文章,“三天” 首發閱讀權,每年兩期 Go 語言發展現狀分析,每天提前 1 小時閱讀到新鮮的 Gopher 日報,網課、技術專欄、圖書內容前瞻,六小時內必答保證等滿足你關於 Go 語言生態的所有需求!星球首開,福利自然是少不了的!2020 年年底之前,8.8 折 (很吉利吧^_^) 加入星球,下方圖片掃起來吧!

我的 Go 技術專欄:“改善 Go 語⾔程式設計質量的 50 個有效實踐” 上線了,歡迎大家訂閱學習!

img{512x368}

我的網課 “Kubernetes 實戰:高可用叢集搭建、配置、運維與應用” 在慕課網熱賣中,歡迎小夥伴們訂閱學習!

Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily

我的聯絡方式:

更多原創文章乾貨分享,請關注公眾號
  • 一文告訴你神奇的 Go 內建函式原始碼在哪裡
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章