日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

作者 | qcrao

責(zé)編 | 屠敏

出品 | CSDN博客

剛開始寫這篇文章的時候,目標(biāo)非常大,想要探索 Go 程序的一生:編碼、編譯、匯編、鏈接、運(yùn)行、退出。它的每一步具體如何進(jìn)行,力圖弄清 Go 程序的這一生。

在這個過程中,我又復(fù)習(xí)了一遍《程序員的自我修養(yǎng)》。這是一本講編譯、鏈接的書,非常詳細(xì),值得一看!數(shù)年前,我第一次看到這本書的書名,就非常喜歡。因?yàn)樗7铝酥苄邱Y喜劇之王里出現(xiàn)的一本書 ——《演員的自我修養(yǎng)》。心向往之!

在開始本文之前,先推薦一位頭條大佬的博客——《面向信仰編程》,他的 Go 編譯系列文章,非常有深度,直接深入編譯器源代碼,我是看了很多遍了。博客鏈接可以從參考資料里獲取。

理想很大,實(shí)現(xiàn)的難度也是非常大。為了避免砸了“深度解密”這個牌子,這次起了個更溫和的名字。

下面是文章的目錄:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

1.引入

我們從一個 HelloWorld 的例子開始:

package main 
import "fmt"
funcmain{
fmt.Println("hello world")
}

當(dāng)我用我那價值 1800 元的 cherry 鍵盤瀟灑地敲完上面的 hello world 代碼時,保存在硬盤上的 hello.go 文件就是一個字節(jié)序列了,每個字節(jié)代表一個字符。

用 vim 打開 hello.go 文件,在命令行模式下,輸入命令:

:%!xxd

就能在 vim 里以十六進(jìn)制查看文件內(nèi)容:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

最左邊的一列代表地址值,中間一列代表文本對應(yīng)的 ASCII 字符,最右邊的列就是我們的代碼。再在終端里執(zhí)行 man ascii:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

和 ASCII 字符表一對比,就能發(fā)現(xiàn),中間的列和最右邊的列是一一對應(yīng)的。也就是說,剛剛寫完的 hello.go 文件都是由 ASCII 字符表示的,它被稱為 文本文件,其他文件被稱為 二進(jìn)制文件。

當(dāng)然,更深入地看,計(jì)算機(jī)中的所有數(shù)據(jù),像磁盤文件、網(wǎng)絡(luò)中的數(shù)據(jù)其實(shí)都是一串比特位組成,取決于如何看待它。在不同的情景下,一個相同的字節(jié)序列可能表示成一個整數(shù)、浮點(diǎn)數(shù)、字符串或者是機(jī)器指令。

而像 hello.go 這個文件,8 個 bit,也就是一個字節(jié)看成一個單位(假定源程序的字符都是 ASCII 碼),最終解釋成人類能讀懂的 Go 源碼。

Go 程序并不能直接運(yùn)行,每條 Go 語句必須轉(zhuǎn)化為一系列的低級機(jī)器語言指令,將這些指令打包到一起,并以二進(jìn)制磁盤文件的形式存儲起來,也就是可執(zhí)行目標(biāo)文件。

從源文件到可執(zhí)行目標(biāo)文件的轉(zhuǎn)化過程:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

完成以上各個階段的就是 Go 編譯系統(tǒng)。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名為 GNU 編譯器套裝,它支持像 C,C++,JAVA,Python,Objective-C,Ada,F(xiàn)ortran,Pascal,能夠?yàn)楹芏嗖煌臋C(jī)器生成機(jī)器碼。

可執(zhí)行目標(biāo)文件可以直接在機(jī)器上執(zhí)行。一般而言,先執(zhí)行一些初始化的工作;找到 main 函數(shù)的入口,執(zhí)行用戶寫的代碼;執(zhí)行完成后,main 函數(shù)退出;再執(zhí)行一些收尾的工作,整個過程完畢。

在接下來的文章里,我們將探索 編譯和 運(yùn)行的過程。

2.編譯鏈接概述

Go 源碼里的編譯器源碼位于 src/cmd/compile 路徑下,鏈接器源碼位于 src/cmd/link 路徑下。

編譯過程

我比較喜歡用 IDE(集成開發(fā)環(huán)境)來寫代碼, Go 源碼用的 Goland,有時候直接點(diǎn)擊 IDE 菜單欄里的“運(yùn)行”按鈕,程序就跑起來了。這實(shí)際上隱含了編譯和鏈接的過程,我們通常將編譯和鏈接合并到一起的過程稱為構(gòu)建(Build)。

編譯過程就是對源文件進(jìn)行詞法分析、語法分析、語義分析、優(yōu)化,最后生成匯編代碼文件,以 .s 作為文件后綴。

之后,匯編器會將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令。由于每一條匯編語句幾乎都與一條機(jī)器指令相對應(yīng),所以只是一個簡單的一一對應(yīng),比較簡單,沒有語法、語義分析,也沒有優(yōu)化這些步驟。

編譯器是將高級語言翻譯成機(jī)器語言的一個工具,編譯過程一般分為 6 步:掃描、語法分析、語義分析、源代碼優(yōu)化、代碼生成、目標(biāo)代碼優(yōu)化。下圖來自《程序員的自我修養(yǎng)》:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

詞法分析

通過前面的例子,我們知道,Go 程序文件在機(jī)器看來不過是一堆二進(jìn)制位。我們能讀懂,是因?yàn)?Goland 按照 ASCII 碼(實(shí)際上是 UTF-8)把這堆二進(jìn)制位進(jìn)行了編碼。例如,把 8個 bit 位分成一組,對應(yīng)一個字符,通過對照 ASCII 碼表就可以查出來。

當(dāng)把所有的二進(jìn)制位都對應(yīng)成了 ASCII 碼字符后,我們就能看到有意義的字符串。它可能是關(guān)鍵字,例如:package;可能是字符串,例如:“Hello World”。

詞法分析其實(shí)干的就是這個。輸入是原始的 Go 程序文件,在詞法分析器看來,就是一堆二進(jìn)制位,根本不知道是什么東西,經(jīng)過它的分析后,變成有意義的記號。簡單來說,詞法分析是計(jì)算機(jī)科學(xué)中將字符序列轉(zhuǎn)換為標(biāo)記(token)序列的過程。

我們來看一下維基百科上給出的定義:

詞法分析(lexical analysis)是計(jì)算機(jī)科學(xué)中將字符序列轉(zhuǎn)換為標(biāo)記(token)序列的過程。進(jìn)行詞法分析的程序或者函數(shù)叫作詞法分析器(lexical analyzer,簡稱lexer),也叫掃描器(scanner)。詞法分析器一般以函數(shù)的形式存在,供語法分析器調(diào)用。

.go 文件被輸入到掃描器(Scanner),它使用一種類似于 有限狀態(tài)機(jī)的算法,將源代碼的字符系列分割成一系列的記號(Token)。

記號一般分為這幾類:關(guān)鍵字、標(biāo)識符、字面量(包含數(shù)字、字符串)、特殊符號(如加號、等號)。

例如,對于如下的代碼:

slice[i] = i * (2 + 6)

總共包含 16 個非空字符,經(jīng)過掃描后:

記號 類型
slice 標(biāo)識符
[ 左方括號
i 標(biāo)識符
] 右方括號
= 賦值
i 標(biāo)識符
* 乘號
( 左圓括號
2 數(shù)字
+ 加號
6 數(shù)字
) 右圓括號

上面的例子源自《程序員的自我修養(yǎng)》,主要講解編譯、鏈接相關(guān)的內(nèi)容,很精彩,推薦研讀。

Go 語言(本文的 Go 版本是 1.9.2)掃描器支持的 Token 在源碼中的路徑:

src/cmd/compile/internal/syntax/token.go

感受一下:

var tokstrings = [...]string{ 
// source control
_EOF: "EOF",
// names and literals
_Name: "name",
_Literal: "literal",
// operators and operations
_Operator: "op",
_AssignOp: "op=",
_IncOp: "opop",
_Assign: "=",
_Define: ":=",
_Arrow: "<-",
_Star: "*",
// delimitors
_Lparen: "(",
_Lbrack: "[",
_Lbrace: "{",
_Rparen: ")",
_Rbrack: "]",
_Rbrace: "}",
_Comma: ",",
_Semi: ";",
_Colon: ":",
_Dot: ".",
_DotDotDot: "...",
// keywords
_Break: "break",
_Case: "case",
_Chan: "chan",
_Const: "const",
_Continue: "continue",
_Default: "default",
_Defer: "defer",
_Else: "else",
_Fallthrough: "fallthrough",
_For: "for",
_Func: "func",
_Go: "go",
_Goto: "goto",
_If: "if",
_Import: "import",
_Interface: "interface",
_Map: "map",
_Package: "package",
_Range: "range",
_Return: "return",
_Select: "select",
_Struct: "struct",
_Switch: "switch",
_Type: "type",
_Var: "var",
}

還是比較熟悉的,包括名稱和字面量、操作符、分隔符和關(guān)鍵字。

而掃描器的路徑是:

src/cmd/compile/internal/syntax/scanner.go

其中最關(guān)鍵的函數(shù)就是 next 函數(shù),它不斷地讀取下一個字符(不是下一個字節(jié),因?yàn)?Go 語言支持 Unicode 編碼,并不是像我們前面舉得 ASCII 碼的例子,一個字符只有一個字節(jié)),直到這些字符可以構(gòu)成一個 Token。

func (s *scanner) next{
// ……
redo:
// skip white space
c := s.getr
for c == ' ' || c == 't' || c == 'n' && !nlsemi || c == 'r' {
c = s.getr
}
// token start
s.line, s.col = s.source.line0, s.source.col0
if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) {
s.ident
return
}
switch c {
// ……
case 'n':
s.lit = "newline"
s.tok = _Semi
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
s.number(c)
// ……
default:
s.tok = 0
s.error(fmt.Sprintf("invalid character %#U", c))
goto redo
return
assignop:
if c == '=' {
s.tok = _AssignOp
return
}
s.ungetr
s.tok = _Operator
}

代碼的主要邏輯就是通過 c:=s.getr 獲取下一個未被解析的字符,并且會跳過之后的空格、回車、換行、tab 字符,然后進(jìn)入一個大的 switch-case 語句,匹配各種不同的情形,最終可以解析出一個 Token,并且把相關(guān)的行、列數(shù)字記錄下來,這樣就完成一次解析過程。

當(dāng)前包中的詞法分析器 scanner 也只是為上層提供了 next 方法,詞法解析的過程都是惰性的,只有在上層的解析器需要時才會調(diào)用 next 獲取最新的 Token。

語法分析

上一步生成的 Token 序列,需要經(jīng)過進(jìn)一步處理,生成一棵以 表達(dá)式為結(jié)點(diǎn)的 語法樹。

比如最開始的那個例子, slice[i]=i*(2+6),得到的一棵語法樹如下:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

整個語句被看作是一個賦值表達(dá)式,左子樹是一個數(shù)組表達(dá)式,右子樹是一個乘法表達(dá)式;數(shù)組表達(dá)式由 2 個符號表達(dá)式組成;乘號表達(dá)式則是由一個符號表達(dá)式和一個加號表達(dá)式組成;加號表達(dá)式則是由兩個數(shù)字組成。符號和數(shù)字是最小的表達(dá)式,它們不能再被分解,通常作為樹的葉子節(jié)點(diǎn)。

語法分析的過程可以檢測一些形式上的錯誤,例如:括號是否缺少一半, + 號表達(dá)式缺少一個操作數(shù)等。

語法分析是根據(jù)某種特定的形式文法(Grammar)對 Token 序列構(gòu)成的輸入文本進(jìn)行分析并確定其語法結(jié)構(gòu)的一種過程。

語義分析

語法分析完成后,我們并不知道語句的具體意義是什么。像上面的 * 號的兩棵子樹如果是兩個指針,這是不合法的,但語法分析檢測不出來,語義分析就是干這個事。

編譯期所能檢查的是靜態(tài)語義,可以認(rèn)為這是在“代碼”階段,包括變量類型的匹配、轉(zhuǎn)換等。例如,將一個浮點(diǎn)值賦給一個指針變量的時候,明顯的類型不匹配,就會報(bào)編譯錯誤。而對于運(yùn)行期間才會出現(xiàn)的錯誤:不小心除了一個 0 ,語義分析是沒辦法檢測的。

語義分析階段完成之后,會在每個節(jié)點(diǎn)上標(biāo)注上類型:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

Go 語言編譯器在這一階段檢查常量、類型、函數(shù)聲明以及變量賦值語句的類型,然后檢查哈希中鍵的類型。實(shí)現(xiàn)類型檢查的函數(shù)通常都是幾千行的巨型 switch/case 語句。

類型檢查是 Go 語言編譯的第二個階段,在詞法和語法分析之后我們得到了每個文件對應(yīng)的抽象語法樹,隨后的類型檢查會遍歷抽象語法樹中的節(jié)點(diǎn),對每個節(jié)點(diǎn)的類型進(jìn)行檢驗(yàn),找出其中存在的語法錯誤。

在這個過程中也可能會對抽象語法樹進(jìn)行改寫,這不僅能夠去除一些不會被執(zhí)行的代碼對編譯進(jìn)行優(yōu)化提高執(zhí)行效率,而且也會修改 make、new 等關(guān)鍵字對應(yīng)節(jié)點(diǎn)的操作類型。

例如比較常用的 make 關(guān)鍵字,用它可以創(chuàng)建各種類型,如 slice,map,channel 等等。到這一步的時候,對于 make 關(guān)鍵字,也就是 OMAKE 節(jié)點(diǎn),會先檢查它的參數(shù)類型,根據(jù)類型的不同,進(jìn)入相應(yīng)的分支。如果參數(shù)類型是 slice,就會進(jìn)入 TSLICE case 分支,檢查 len 和 cap 是否滿足要求,如 len <= cap。最后節(jié)點(diǎn)類型會從 OMAKE 改成 OMAKESLICE。

中間代碼生成

我們知道,編譯過程一般可以分為前端和后端,前端生成和平臺無關(guān)的中間代碼,后端會針對不同的平臺,生成不同的機(jī)器碼。

前面詞法分析、語法分析、語義分析等都屬于編譯器前端,之后的階段屬于編譯器后端。

編譯過程有很多優(yōu)化的環(huán)節(jié),在這個環(huán)節(jié)是指源代碼級別的優(yōu)化。它將語法樹轉(zhuǎn)換成中間代碼,它是語法樹的順序表示。

中間代碼一般和目標(biāo)機(jī)器以及運(yùn)行時環(huán)境無關(guān),它有幾種常見的形式:三地址碼、P-代碼。例如,最基本的 三地址碼是這樣的:

x = y op z

表示變量 y 和 變量 z 進(jìn)行 op 操作后,賦值給 x。op 可以是數(shù)學(xué)運(yùn)算,例如加減乘除。

前面我們舉的例子可以寫成如下的形式:

t1 = 2 + 6 
t2 = i * t1
slice[i] = t2

這里 2 + 6 是可以直接計(jì)算出來的,這樣就把 t1 這個臨時變量“優(yōu)化”掉了,而且 t1 變量可以重復(fù)利用,因此 t2 也可以“優(yōu)化”掉。優(yōu)化之后:

t1 = i * 8 
slice[i] = t1

Go 語言的中間代碼表示形式為 SSA(Static Single-Assignment,靜態(tài)單賦值),之所以稱之為單賦值,是因?yàn)槊總€名字在 SSA 中僅被賦值一次。

這一階段會根據(jù) CPU 的架構(gòu)設(shè)置相應(yīng)的用于生成中間代碼的變量,例如編譯器使用的指針和寄存器的大小、可用寄存器列表等。中間代碼生成和機(jī)器碼生成這兩部分會共享相同的設(shè)置。

在生成中間代碼之前,會對抽象語法樹中節(jié)點(diǎn)的一些元素進(jìn)行替換。這里引用《面向信仰編程》編譯原理相關(guān)博客里的一張圖:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

例如對于 map 的操作 m[i],在這里會被轉(zhuǎn)換成 mapacess 或 mapassign。

Go 語言的主程序在執(zhí)行時會調(diào)用 runtime 中的函數(shù),也就是說關(guān)鍵字和內(nèi)置函數(shù)的功能其實(shí)是由語言的編譯器和運(yùn)行時共同完成的。

中間代碼的生成過程其實(shí)就是從 AST 抽象語法樹到 SSA 中間代碼的轉(zhuǎn)換過程,在這期間會對語法樹中的關(guān)鍵字在進(jìn)行一次更新,更新后的語法樹會經(jīng)過多輪處理轉(zhuǎn)變最后的 SSA 中間代碼。

目標(biāo)代碼生成與優(yōu)化

不同機(jī)器的機(jī)器字長、寄存器等等都不一樣,意味著在不同機(jī)器上跑的機(jī)器碼是不一樣的。最后一步的目的就是要生成能在不同 CPU 架構(gòu)上運(yùn)行的代碼。

為了榨干機(jī)器的每一滴油水,目標(biāo)代碼優(yōu)化器會對一些指令進(jìn)行優(yōu)化,例如使用移位指令代替乘法指令等。

這塊實(shí)在沒能力深入,幸好也不需要深入。對于應(yīng)用層的軟件開發(fā)工程師來說,了解一下就可以了。

鏈接過程

編譯過程是針對單個文件進(jìn)行的,文件與文件之間不可避免地要引用定義在其他模塊的全局變量或者函數(shù),這些變量或函數(shù)的地址只有在此階段才能確定。

鏈接過程就是要把編譯器生成的一個個目標(biāo)文件鏈接成可執(zhí)行文件。最終得到的文件是分成各種段的,比如數(shù)據(jù)段、代碼段、BSS段等等,運(yùn)行時會被裝載到內(nèi)存中。各個段具有不同的讀寫、執(zhí)行屬性,保護(hù)了程序的安全運(yùn)行。

這部分內(nèi)容,推薦看《程序員的自我修養(yǎng)》和《深入理解計(jì)算機(jī)系統(tǒng)》。

3.Go 程序啟動

仍然使用 hello-world 項(xiàng)目的例子。在項(xiàng)目根目錄下執(zhí)行:

go build -gcflags "-N -l" -o hello src/main.go

-gcflags"-N -l" 是為了關(guān)閉編譯器優(yōu)化和函數(shù)內(nèi)聯(lián),防止后面在設(shè)置斷點(diǎn)的時候找不到相對應(yīng)的代碼位置。

得到了可執(zhí)行文件 hello,執(zhí)行:

[qcrao@qcrao hello-world]$ gdb hello

進(jìn)入 gdb 調(diào)試模式,執(zhí)行 info files,得到可執(zhí)行文件的文件頭,列出了各種段:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

同時,我們也得到了入口地址:0x450e20。

(gdb) b *0x450e20 
Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.

這就是 Go 程序的入口地址,我是在 linux 上運(yùn)行的,所以入口文件為 src/runtime/rt0_linux_amd64.s,runtime 目錄下有各種不同名稱的程序入口文件,支持各種操作系統(tǒng)和架構(gòu),代碼為:

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 
LEAQ 8(SP), SI // argv
MOVQ 0(SP), DI // argc
MOVQ $main(SB), AX
JMP AX

主要是把 argc,argv 從內(nèi)存拉到了寄存器。這里 LEAQ 是計(jì)算內(nèi)存地址,然后把內(nèi)存地址本身放進(jìn)寄存器里,也就是把 argv 的地址放到了 SI 寄存器中。最后跳轉(zhuǎn)到:

TEXT main(SB),NOSPLIT,$-8 
MOVQ $runtime·rt0_go(SB), AX
JMP AX

繼續(xù)跳轉(zhuǎn)到 runtime·rt0_go(SB),位置:/usr/local/go/src/runtime/asm_amd64.s,代碼:

TEXT runtime·rt0_go(SB),NOSPLIT,$0 
// 省略很多 CPU 相關(guān)的特性標(biāo)志位檢查的代碼
// 主要是看不懂,^_^
// ………………………………
// 下面是最后調(diào)用的一些函數(shù),比較重要
// 初始化執(zhí)行文件的絕對路徑
CALL runtime·args(SB)
// 初始化 CPU 個數(shù)和內(nèi)存頁大小
CALL runtime·osinit(SB)
// 初始化命令行參數(shù)、環(huán)境變量、gc、棧空間、內(nèi)存管理、所有 P 實(shí)例、HASH算法等
CALL runtime·schedinit(SB)
// 要在 main goroutine 上運(yùn)行的函數(shù)
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
// 新建一個 goroutine,該 goroutine 綁定 runtime.main,放在 P 的本地隊(duì)列,等待調(diào)度
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// 啟動M,開始調(diào)度goroutine
CALL runtime·mstart(SB)
MOVL $0xf1, 0xf1 // crash
RET
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,$8

參考文獻(xiàn)里的一篇文章【探索 golang 程序啟動過程】研究得比較深入,總結(jié)下:

  1. 檢查運(yùn)行平臺的CPU,設(shè)置好程序運(yùn)行需要相關(guān)標(biāo)志。

  2. TLS的初始化。

  3. runtime.args、runtime.osinit、runtime.schedinit 三個方法做好程序運(yùn)行需要的各種變量與調(diào)度器。

  4. runtime.newproc創(chuàng)建新的goroutine用于綁定用戶寫的main方法。

  5. runtime.mstart開始goroutine的調(diào)度。

最后用一張圖來總結(jié) go bootstrap 過程吧:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

main 函數(shù)里執(zhí)行的一些重要的操作包括:新建一個線程執(zhí)行 sysmon 函數(shù),定期垃圾回收和調(diào)度搶占;啟動 gc;執(zhí)行所有的 init 函數(shù)等等。

上面是啟動過程,看一下退出過程:

當(dāng) main 函數(shù)執(zhí)行結(jié)束之后,會執(zhí)行 exit(0) 來退出進(jìn)程。若執(zhí)行 exit(0) 后,進(jìn)程沒有退出,main 函數(shù)最后的代碼會一直訪問非法地址:

exit(0) 
for {
var x *int32
*x = 0
}

正常情況下,一旦出現(xiàn)非法地址訪問,系統(tǒng)會把進(jìn)程殺死,用這樣的方法確保進(jìn)程退出。

關(guān)于程序退出這一段的闡述來自群聊《golang runtime 閱讀》,又是一個高階的讀源碼的組織,Github 主頁見參考資料。

當(dāng)然 Go 程序啟動這一部分其實(shí)還會涉及到 fork 一個新進(jìn)程、裝載可執(zhí)行文件,控制權(quán)轉(zhuǎn)移等問題。還是推薦看前面的兩本書,我覺得我不會寫得更好,就不敘述了。

4.GoRoot 和 GoPath

GoRoot 是 Go 的安裝路徑。mac 或 unix 是在 /usr/local/go 路徑上,來看下這里都裝了些什么:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

bin 目錄下面:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

pkg 目錄下面:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

Go 工具目錄如下,其中比較重要的有編譯器 compile,鏈接器 link:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

GoPath 的作用在于提供一個可以尋找 .go 源碼的路徑,它是一個工作空間的概念,可以設(shè)置多個目錄。Go 官方要求,GoPath 下面需要包含三個文件夾:

src 
pkg
bin

src 存放源文件,pkg 存放源文件編譯后的庫文件,后綴為 .a;bin 則存放可執(zhí)行文件。

5.Go 命令詳解

直接在終端執(zhí)行:

go

就能得到和 go 相關(guān)的命令簡介:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

和編譯相關(guān)的命令主要是:

go build 
go install
go run

go build

go build 用來編譯指定 packages 里的源碼文件以及它們的依賴包,編譯的時候會到 $GoPath/src/package 路徑下尋找源碼文件。go build 還可以直接編譯指定的源碼文件,并且可以同時指定多個。

通過執(zhí)行 go help build 命令得到 go build 的使用方法:

usage: go build [-o output] [-i] [build flags] [packages]

-o 只能在編譯單個包的時候出現(xiàn),它指定輸出的可執(zhí)行文件的名字。

-i 會安裝編譯目標(biāo)所依賴的包,安裝是指生成與代碼包相對應(yīng)的 .a 文件,即靜態(tài)庫文件(后面要參與鏈接),并且放置到當(dāng)前工作區(qū)的 pkg 目錄下,且?guī)煳募哪夸泴蛹壓驮创a層級一致。

至于 build flags 參數(shù), build,clean,get,install,list,run,test 這些命令會共用一套:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

我們知道,Go 語言的源碼文件分為三類:命令源碼、庫源碼、測試源碼。

  • 命令源碼文件:是 Go 程序的入口,包含 func main 函數(shù),且第一行用 packagemain 聲明屬于 main 包。

  • 庫源碼文件:主要是各種函數(shù)、接口等,例如工具類的函數(shù)。

  • 測試源碼文件:以 _test.go 為后綴的文件,用于測試程序的功能和性能。

注意, go build 會忽略 *_test.go 文件。

我們通過一個很簡單的例子來演示 go build 命令。我用 Goland 新建了一個 hello-world 項(xiàng)目(為了展示引用自定義的包,和之前的 hello-world 程序不同),項(xiàng)目的結(jié)構(gòu)如下:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

最左邊可以看到項(xiàng)目的結(jié)構(gòu),包含三個文件夾:bin,pkg,src。其中 src 目錄下有一個 main.go,里面定義了 main 函數(shù),是整個項(xiàng)目的入口,也就是前面提過的所謂的命令源碼文件;src 目錄下還有一個 util 目錄,里面有 util.go 文件,定義了一個可以獲取本機(jī) IP 地址的函數(shù),也就是所謂的庫源碼文件。

中間是 main.go 的源碼,引用了兩個包,一個是標(biāo)準(zhǔn)庫的 fmt;一個是 util 包,util 的導(dǎo)入路徑是 util。所謂的導(dǎo)入路徑是指相對于 Go 的源碼目錄 $GoRoot/src 或者 $GoPath/src 的下的子路徑。例如 main 包里引用的 fmt 的源碼路徑是 /usr/local/go/src/fmt,而 util 的源碼路徑是 /Users/qcrao/hello-world/src/util,正好我們設(shè)置的 GoPath = /Users/qcrao/hello-world。

最右邊是庫函數(shù)的源碼,實(shí)現(xiàn)了獲取本機(jī) IP 的函數(shù)。

在 src 目錄下,直接執(zhí)行 go build 命令,在同級目錄生成了一個可執(zhí)行文件,文件名為 src,使用 ./src 命令直接執(zhí)行,輸出:

hello world! 
Local IP: 192.168.1.3

我們也可以指定生成的可執(zhí)行文件的名稱:

go build -o bin/hello

這樣,在 bin 目錄下會生成一個可執(zhí)行文件,運(yùn)行結(jié)果和上面的 src 一樣。

其實(shí),util 包可以單獨(dú)被編譯。我們可以在項(xiàng)目根目錄下執(zhí)行:

go build util

編譯程序會去 $GoPath/src 路徑找 util 包(其實(shí)是找文件夾)。還可以在 ./src/util 目錄下直接執(zhí)行 go build 編譯。

當(dāng)然,直接編譯庫源碼文件不會生成 .a 文件,因?yàn)椋?/p>

go build 命令在編譯只包含庫源碼文件的代碼包(或者同時編譯多個代碼包)時,只會做檢查性的編譯,而不會輸出任何結(jié)果文件。

為了展示整個編譯鏈接的運(yùn)行過程,我們在項(xiàng)目根目錄執(zhí)行如下的命令:

go build -v -x -work -o bin/hello src/main.go

-v 會打印所編譯過的包名字, -x 打印編譯期間所執(zhí)行的命令, -work 打印編譯期間生成的臨時文件路徑,并且編譯完成之后不會被刪除。

執(zhí)行結(jié)果:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

從結(jié)果來看,圖中用箭頭標(biāo)注了本次編譯過程涉及 2 個包:util,command-line-arguments。第二個包比較詭異,源碼里根本就沒有這個名字好嗎?其實(shí)這是 go build 命令檢測到 [packages] 處填的是一個 .go 文件,因此創(chuàng)建了一個虛擬的包:command-line-arguments。

同時,用紅框圈出了 compile, link,也就是先編譯了 util 包和 main.go 文件,分別得到 .a 文件,之后將兩者進(jìn)行鏈接,最終生成可執(zhí)行文件,并且移動到 bin 目錄下,改名為 hello。

另外,第一行顯示了編譯過程中的工作目錄,此目錄的文件結(jié)構(gòu)是:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

可以看到,和 hello-world 目錄的層級基本一致。command-line-arguments 就是虛擬的 main.go 文件所處的包。exe 目錄下的可執(zhí)行文件在最后一步被移動到了 bin 目錄下,所以這里是空的。

整體來看, go build 在執(zhí)行時,會先遞歸尋找 main.go 所依賴的包,以及依賴的依賴,直至最底層的包。這里可以是深度優(yōu)先遍歷也可以是寬度優(yōu)先遍歷。如果發(fā)現(xiàn)有循環(huán)依賴,就會直接退出,這也是經(jīng)常會發(fā)生的循環(huán)引用編譯錯誤。

正常情況下,這些依賴關(guān)系會形成一棵倒著生長的樹,樹根在最上面,就是 main.go 文件,最下面是沒有任何其他依賴的包。編譯器會從最左的節(jié)點(diǎn)所代表的包開始挨個編譯,完成之后,再去編譯上一層的包。

這里,引用郝林老師幾年前在 github 上發(fā)表的 go 命令教程,可以從參考資料找到原文地址。

從代碼包編譯的角度來說,如果代碼包 A 依賴代碼包 B,則稱代碼包 B 是代碼包 A 的依賴代碼包(以下簡稱依賴包),代碼包 A 是代碼包 B 的觸發(fā)代碼包(以下簡稱觸發(fā)包)。

執(zhí)行 go build 命令的計(jì)算機(jī)如果擁有多個邏輯 CPU 核心,那么編譯代碼包的順序可能會存在一些不確定性。但是,它一定會滿足這樣的約束條件:依賴代碼包 -> 當(dāng)前代碼包 -> 觸發(fā)代碼包。

順便推薦一個瀏覽器插件 Octotree,在看 github 項(xiàng)目的時候,此插件可以在瀏覽器里直接展示整個項(xiàng)目的文件結(jié)構(gòu),非常方便:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

到這里,你一定會發(fā)現(xiàn),對于 hello-wrold 文件夾下的 pkg 目錄好像一直沒有涉及到。

其實(shí),pkg 目錄下面應(yīng)該存放的是涉及到的庫文件編譯后的包,也就是一些 .a 文件。但是 go build 執(zhí)行過程中,這些 .a 文件放在臨時文件夾中,編譯完成后會被直接刪掉,因此一般不會用到。

前面我們提到過,在 go build 命令里加上 -i 參數(shù)會安裝這些庫文件編譯的包,也就是這些 .a 文件會放到 pkg 目錄下。

在項(xiàng)目根目錄執(zhí)行 go build-i src/main.go 后,pkg 目錄里增加了 util.a 文件:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

darwin_amd64 表示的是:

  • GOOS 和 GOARCH。這兩個環(huán)境變量不用我們設(shè)置,系統(tǒng)默認(rèn)的。

  • GOOS 是 Go 所在的操作系統(tǒng)類型,GOARCH 是 Go 所在的計(jì)算架構(gòu)。

  • Mac 平臺上這個目錄名就是 darwin_amd64

生成了 util.a 文件后,再次編譯的時候,就不會再重新編譯 util.go 文件,加快了編譯速度。

同時,在根目錄下生成了名稱為 main 的可執(zhí)行文件,這是以 main.go 的文件名命令的。

hello-world 這個項(xiàng)目的代碼已經(jīng)上傳到了 github 項(xiàng)目 Go-Questions,這個項(xiàng)目由問題導(dǎo)入,企圖串連 Go 的所有知識點(diǎn),正在完善,期待你的 star。地址見參考資料【Go-Questions hello-world項(xiàng)目】。

go install

go install 用于編譯并安裝指定的代碼包及它們的依賴包。相比 go build,它只是多了一個“安裝編譯后的結(jié)果文件到指定目錄”的步驟。

還是使用之前 hello-world 項(xiàng)目的例子,我們先將 pkg 目錄刪掉,在項(xiàng)目根目錄執(zhí)行:

go install src/main.go 
或者
go install util

兩者都會在根目錄下新建一個 pkg 目錄,并且生成一個 util.a 文件。

并且,在執(zhí)行前者的時候,會在 GOBIN 目錄下生成名為 main 的可執(zhí)行文件。

所以,運(yùn)行 go install 命令,庫源碼包對應(yīng)的 .a 文件會被放置到 pkg 目錄下,命令源碼包生成的可執(zhí)行文件會被放到 GOBIN 目錄。

go install 在 GoPath 有多個目錄的時候,會產(chǎn)生一些問題,具體可以去看郝林老師的 Go命令教程,這里不展開了。

go run

go run 用于編譯并運(yùn)行命令源碼文件。

在 hello-world 項(xiàng)目的根目錄,執(zhí)行 go run 命令:

go run -x -work src/main.go

-x 可以打印整個過程涉及到的命令,-work 可以看到臨時的工作目錄:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

從上圖中可以看到,仍然是先編譯,再連接,最后直接執(zhí)行,并打印出了執(zhí)行結(jié)果。

第一行打印的就是工作目錄,最終生成的可執(zhí)行文件就是放置于此:

萬字長文詳解 Go 程序是怎樣跑起來的?| CSDN 博文精選

main 就是最終生成的可執(zhí)行文件。

6.總結(jié)

這次的話題太大了,困難重重。從編譯原理到 go 啟動時的流程,到 go 命令原理,每個話題單獨(dú)抽出來都可以寫很多。

幸好有一些很不錯的書和博客文章可以去參考。這篇文章就作為一個引子,你可以跟隨參考資料里推薦的一些內(nèi)容去發(fā)散。

參考資料

  • 【《程序員的自我修養(yǎng)》全書】https://book.douban.com/subject/3652388/

  • 【面向信仰編程 編譯過程概述】https://draveness.me/golang-compile-intro

  • 【golang runtime 閱讀】https://github.com/zboya/golangruntimereading

  • 【Go-Questions hello-world項(xiàng)目】https://github.com/qcrao/Go-Questions/tree/master/examples/hello-world

  • 【雨痕大佬的 Go 語言學(xué)習(xí)筆記】https://github.com/qyuhen/book

  • 【vim 以 16 進(jìn)制文本】https://www.cnblogs.com/meibenjin/archive/2012/12/06/2806396.html

  • 【Go 編譯命令執(zhí)行過程】https://halfrost.com/go_command/

  • 【Go 命令執(zhí)行過程】https://github.com/hyper0x/gocommandtutorial

  • 【Go 詞法分析】https://ggaaooppeenngg.github.io/zh-CN/2016/04/01/go-lexer-%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/

  • 【曹大博客 golang 與 ast】http://xargin.com/ast/

  • 【Golang 詞法解析器,scanner 源碼分析】https://blog.csdn.net/zhaoruixiang1111/article/details/89892435

  • 【Gopath Explained】https://flaviocopes.com/go-gopath/

  • 【Understanding the GOPATH】https://www.digitalocean.com/community/tutorials/understanding-the-gopath

  • 【討論】https://stackoverflow.com/questions/7970390/what-should-be-the-values-of-gopath-and-goroot

  • 【Go 官方 Gopath】https://golang.org/cmd/go/#hdr-GOPATHenvironmentvariable

  • 【Go package 的探索】https://mp.weixin.qq.com/s/OizVLXfZ6EC1jI-NL7HqeA

  • 【Go 官方 關(guān)于 Go 項(xiàng)目的組織結(jié)構(gòu)】https://golang.org/doc/code.html

  • 【Go modules】https://www.melvinvivas.com/go-version-1-11-modules/

  • 【Golang Installation, Setup, GOPATH, and Go Workspace】https://www.callicoder.com/golang-installation-setup-gopath-workspace/

  • 【編譯、鏈接過程鏈接】https://mikespook.com/2013/11/%E7%BF%BB%E8%AF%91-go-build-%E5%91%BD%E4%BB%A4%E6%98%AF%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%E7%9A%84%EF%BC%9F/

  • 【1.5 編譯器由 go 語言完成】https://www.infoq.cn/article/2015/08/go-1-5

  • 【Go 編譯過程系列文章】https://www.ctolib.com/topics-3724.html

  • 【曹大 go bootstrap】https://github.com/cch123/golang-notes/blob/master/bootstrap.md

  • 【golang 啟動流程】https://blog.iceinto.com/posts/go/start/

  • 【探索 golang 程序啟動過程】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2golang%E7%A8%8B%E5%BA%8F%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B/

  • 【探索 goroutine 的創(chuàng)建】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/

版權(quán)聲明:本文為CSDN博主「qcrao」的原創(chuàng)文章。

【END】

1024程序員節(jié)如期而至,CSDN Blink小姐姐的關(guān)愛來了!

掃描領(lǐng)取小姐姐的專屬福利!

程序員邂逅鼓勵師的正確姿勢!掃描前往福利現(xiàn)場>>

分享到:
標(biāo)簽:語言
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定