o語言中文網,致力于每日分享編碼、開源等知識,歡迎關注我,會有意想不到的收獲!
剛開始寫這篇文章的時候,目標非常大,想要探索 Go 程序的一生:編碼、編譯、匯編、鏈接、運行、退出。它的每一步具體如何進行,力圖弄清 Go 程序的這一生。
在這個過程中,我又復習了一遍《程序員的自我修養》。這是一本講編譯、鏈接的書,非常詳細,值得一看!數年前,我第一次看到這本書的書名,就非常喜歡。因為它模仿了周星馳喜劇之王里出現的一本書 ——《演員的自我修養》。心向往之!
在開始本文之前,先推薦一下王晶大佬(以前在滴滴)的博客——《面向信仰編程》,他的 Go 編譯系列文章,非常有深度,直接深入編譯器源代碼,我是看了很多遍了。博客鏈接可以從參考資料里獲取。
理想很大,實現的難度也是非常大。為了避免砸了“深度解密”這個牌子,這次起了個更溫和的名字,嘿嘿。
我們從一個 Hello World 的例子開始:
package main import "fmt" func main() { fmt.Println("hello world") }
當我用我那價值 1800 元的 cherry 鍵盤瀟灑地敲完上面的 hello world 代碼時,保存在硬盤上的 hello.go 文件就是一個字節序列了,每個字節代表一個字符。
用 vim 打開 hello.go 文件,在命令行模式下,輸入命令:
:%!xxd
就能在 vim 里以十六進制查看文件內容:
最左邊的一列代表地址值,中間一列代表文本對應的 ASCII 字符,最右邊的列就是我們的代碼。再在終端里執行 man ascii:
和 ASCII 字符表一對比,就能發現,中間的列和最右邊的列是一一對應的。也就是說,剛剛寫完的 hello.go 文件都是由 ASCII 字符表示的,它被稱為文本文件,其他文件被稱為二進制文件。
當然,更深入地看,計算機中的所有數據,像磁盤文件、網絡中的數據其實都是一串比特位組成,取決于如何看待它。在不同的情景下,一個相同的字節序列可能表示成一個整數、浮點數、字符串或者是機器指令。
而像 hello.go 這個文件,8 個 bit,也就是一個字節看成一個單位(假定源程序的字符都是 ASCII 碼),最終解釋成人類能讀懂的 Go 源碼。
Go 程序并不能直接運行,每條 Go 語句必須轉化為一系列的低級機器語言指令,將這些指令打包到一起,并以二進制磁盤文件的形式存儲起來,也就是可執行目標文件。
從源文件到可執行目標文件的轉化過程:
完成以上各個階段的就是 Go 編譯系統。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名為 GNU 編譯器套裝,它支持像 C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能夠為很多不同的機器生成機器碼。
可執行目標文件可以直接在機器上執行。一般而言,先執行一些初始化的工作;找到 main 函數的入口,執行用戶寫的代碼;執行完成后,main 函數退出;再執行一些收尾的工作,整個過程完畢。
在接下來的文章里,我們將探索編譯和運行的過程。
Go 源碼里的編譯器源碼位于 src/cmd/compile 路徑下,鏈接器源碼位于 src/cmd/link 路徑下。
我比較喜歡用 IDE(集成開發環境)來寫代碼, Go 源碼用的 Goland,有時候直接點擊 IDE 菜單欄里的“運行”按鈕,程序就跑起來了。這實際上隱含了編譯和鏈接的過程,我們通常將編譯和鏈接合并到一起的過程稱為構建(Build)。
編譯過程就是對源文件進行詞法分析、語法分析、語義分析、優化,最后生成匯編代碼文件,以 .s 作為文件后綴。
之后,匯編器會將匯編代碼轉變成機器可以執行的指令。由于每一條匯編語句幾乎都與一條機器指令相對應,所以只是一個簡單的一一對應,比較簡單,沒有語法、語義分析,也沒有優化這些步驟。
編譯器是將高級語言翻譯成機器語言的一個工具,編譯過程一般分為 6 步:掃描、語法分析、語義分析、源代碼優化、代碼生成、目標代碼優化。下圖來自《程序員的自我修養》:
通過前面的例子,我們知道,Go 程序文件在機器看來不過是一堆二進制位。我們能讀懂,是因為 Goland 按照 ASCII 碼(實際上是 UTF-8)把這堆二進制位進行了編碼。例如,把 8個 bit 位分成一組,對應一個字符,通過對照 ASCII 碼表就可以查出來。
當把所有的二進制位都對應成了 ASCII 碼字符后,我們就能看到有意義的字符串。它可能是關鍵字,例如:package;可能是字符串,例如:“Hello World”。
詞法分析其實干的就是這個。輸入是原始的 Go 程序文件,在詞法分析器看來,就是一堆二進制位,根本不知道是什么東西,經過它的分析后,變成有意義的記號。簡單來說,詞法分析是計算機科學中將字符序列轉換為標記(token)序列的過程。
我們來看一下維基百科上給出的定義:
詞法分析(lexical analysis)是計算機科學中將字符序列轉換為標記(token)序列的過程。進行詞法分析的程序或者函數叫作詞法分析器(lexical analyzer,簡稱lexer),也叫掃描器(scanner)。詞法分析器一般以函數的形式存在,供語法分析器調用。
.go 文件被輸入到掃描器(Scanner),它使用一種類似于有限狀態機的算法,將源代碼的字符系列分割成一系列的記號(Token)。
記號一般分為這幾類:關鍵字、標識符、字面量(包含數字、字符串)、特殊符號(如加號、等號)。
例如,對于如下的代碼:
slice[i]=i * (2 + 6)
總共包含 16 個非空字符,經過掃描后:
上面的例子源自《程序員的自我修養》,主要講解編譯、鏈接相關的內容,很精彩,推薦研讀。
Go 語言(本文的 Go 版本是 1.9.2)掃描器支持的 Token 在源碼中的路徑:
src/cmd/compile/internal/syntax/token.go
感受一下:
還是比較熟悉的,包括名稱和字面量、操作符、分隔符和關鍵字。
而掃描器的路徑是:
src/cmd/compile/internal/syntax/scanner.go
其中最關鍵的函數就是 next 函數,它不斷地讀取下一個字符(不是下一個字節,因為 Go 語言支持 Unicode 編碼,并不是像我們前面舉得 ASCII 碼的例子,一個字符只有一個字節),直到這些字符可以構成一個 Token。
代碼的主要邏輯就是通過 c :=s.getr() 獲取下一個未被解析的字符,并且會跳過之后的空格、回車、換行、tab 字符,然后進入一個大的 switch-case 語句,匹配各種不同的情形,最終可以解析出一個 Token,并且把相關的行、列數字記錄下來,這樣就完成一次解析過程。
當前包中的詞法分析器 scanner 也只是為上層提供了 next 方法,詞法解析的過程都是惰性的,只有在上層的解析器需要時才會調用 next 獲取最新的 Token。
上一步生成的 Token 序列,需要經過進一步處理,生成一棵以表達式為結點的語法樹。
比如最開始的那個例子,slice[i]=i * (2 + 6),得到的一棵語法樹如下:
整個語句被看作是一個賦值表達式,左子樹是一個數組表達式,右子樹是一個乘法表達式;數組表達式由 2 個符號表達式組成;乘號表達式則是由一個符號表達式和一個加號表達式組成;加號表達式則是由兩個數字組成。符號和數字是最小的表達式,它們不能再被分解,通常作為樹的葉子節點。
語法分析的過程可以檢測一些形式上的錯誤,例如:括號是否缺少一半,+ 號表達式缺少一個操作數等。
語法分析是根據某種特定的形式文法(Grammar)對 Token 序列構成的輸入文本進行分析并確定其語法結構的一種過程。
語法分析完成后,我們并不知道語句的具體意義是什么。像上面的 * 號的兩棵子樹如果是兩個指針,這是不合法的,但語法分析檢測不出來,語義分析就是干這個事。
編譯期所能檢查的是靜態語義,可以認為這是在“代碼”階段,包括變量類型的匹配、轉換等。例如,將一個浮點值賦給一個指針變量的時候,明顯的類型不匹配,就會報編譯錯誤。而對于運行期間才會出現的錯誤:不小心除了一個 0 ,語義分析是沒辦法檢測的。
語義分析階段完成之后,會在每個節點上標注上類型:
Go 語言編譯器在這一階段檢查常量、類型、函數聲明以及變量賦值語句的類型,然后檢查哈希中鍵的類型。實現類型檢查的函數通常都是幾千行的巨型 switch/case 語句。
類型檢查是 Go 語言編譯的第二個階段,在詞法和語法分析之后我們得到了每個文件對應的抽象語法樹,隨后的類型檢查會遍歷抽象語法樹中的節點,對每個節點的類型進行檢驗,找出其中存在的語法錯誤。
在這個過程中也可能會對抽象語法樹進行改寫,這不僅能夠去除一些不會被執行的代碼對編譯進行優化提高執行效率,而且也會修改 make、new 等關鍵字對應節點的操作類型。
例如比較常用的 make 關鍵字,用它可以創建各種類型,如 slice,map,channel 等等。到這一步的時候,對于 make 關鍵字,也就是 OMAKE 節點,會先檢查它的參數類型,根據類型的不同,進入相應的分支。如果參數類型是 slice,就會進入 TSLICE case 分支,檢查 len 和 cap 是否滿足要求,如 len <=cap。最后節點類型會從 OMAKE 改成 OMAKESLICE。
我們知道,編譯過程一般可以分為前端和后端,前端生成和平臺無關的中間代碼,后端會針對不同的平臺,生成不同的機器碼。
前面詞法分析、語法分析、語義分析等都屬于編譯器前端,之后的階段屬于編譯器后端。
編譯過程有很多優化的環節,在這個環節是指源代碼級別的優化。它將語法樹轉換成中間代碼,它是語法樹的順序表示。
中間代碼一般和目標機器以及運行時環境無關,它有幾種常見的形式:三地址碼、P-代碼。例如,最基本的三地址碼是這樣的:
x=y op z
表示變量 y 和 變量 z 進行 op 操作后,賦值給 x。op 可以是數學運算,例如加減乘除。
前面我們舉的例子可以寫成如下的形式:
t1=2 + 6 t2=i * t1 slice[i]=t2
這里 2 + 6 是可以直接計算出來的,這樣就把 t1 這個臨時變量“優化”掉了,而且 t1 變量可以重復利用,因此 t2 也可以“優化”掉。優化之后:
t1=i * 8 slice[i]=t1
Go 語言的中間代碼表示形式為 SSA(Static Single-Assignment,靜態單賦值),之所以稱之為單賦值,是因為每個名字在 SSA 中僅被賦值一次。。
這一階段會根據 CPU 的架構設置相應的用于生成中間代碼的變量,例如編譯器使用的指針和寄存器的大小、可用寄存器列表等。中間代碼生成和機器碼生成這兩部分會共享相同的設置。
在生成中間代碼之前,會對抽象語法樹中節點的一些元素進行替換。這里引用王晶大佬《面向信仰編程》編譯原理相關博客里的一張圖:
例如對于 map 的操作 m[i],在這里會被轉換成 mapacess 或 mapassign。
Go 語言的主程序在執行時會調用 runtime 中的函數,也就是說關鍵字和內置函數的功能其實是由語言的編譯器和運行時共同完成的。
中間代碼的生成過程其實就是從 AST 抽象語法樹到 SSA 中間代碼的轉換過程,在這期間會對語法樹中的關鍵字在進行一次更新,更新后的語法樹會經過多輪處理轉變最后的 SSA 中間代碼。
不同機器的機器字長、寄存器等等都不一樣,意味著在不同機器上跑的機器碼是不一樣的。最后一步的目的就是要生成能在不同 CPU 架構上運行的代碼。
為了榨干機器的每一滴油水,目標代碼優化器會對一些指令進行優化,例如使用移位指令代替乘法指令等。
這塊實在沒能力深入,幸好也不需要深入。對于應用層的軟件開發工程師來說,了解一下就可以了。
編譯過程是針對單個文件進行的,文件與文件之間不可避免地要引用定義在其他模塊的全局變量或者函數,這些變量或函數的地址只有在此階段才能確定。
鏈接過程就是要把編譯器生成的一個個目標文件鏈接成可執行文件。最終得到的文件是分成各種段的,比如數據段、代碼段、BSS段等等,運行時會被裝載到內存中。各個段具有不同的讀寫、執行屬性,保護了程序的安全運行。
這部分內容,推薦看《程序員的自我修養》和《深入理解計算機系統》。
仍然使用 hello-world 項目的例子。在項目根目錄下執行:
go build -gcflags "-N -l" -o hello src/main.go
-gcflags "-N -l" 是為了關閉編譯器優化和函數內聯,防止后面在設置斷點的時候找不到相對應的代碼位置。
得到了可執行文件 hello,執行:
[qcrao@qcrao hello-world]$ gdb hello
進入 gdb 調試模式,執行 info files,得到可執行文件的文件頭,列出了各種段:
同時,我們也得到了入口地址:0x450e20。
(gdb) b *0x450e20 Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
這就是 Go 程序的入口地址,我是在 linux 上運行的,所以入口文件為 src/runtime/rt0_linux_amd64.s,runtime 目錄下有各種不同名稱的程序入口文件,支持各種操作系統和架構,代碼為:
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 從內存拉到了寄存器。這里 LEAQ 是計算內存地址,然后把內存地址本身放進寄存器里,也就是把 argv 的地址放到了 SI 寄存器中。最后跳轉到:
TEXT main(SB),NOSPLIT,$-8 MOVQ $runtime·rt0_go(SB), AX JMP AX
繼續跳轉到 runtime·rt0_go(SB),位置:/usr/local/go/src/runtime/asm_amd64.s,代碼:
參考文獻里的一篇文章【探索 golang 程序啟動過程】研究得比較深入,總結下:
最后用一張圖來總結 go bootstrap 過程吧:
main 函數里執行的一些重要的操作包括:新建一個線程執行 sysmon 函數,定期垃圾回收和調度搶占;啟動 gc;執行所有的 init 函數等等。
上面是啟動過程,看一下退出過程:
當 main 函數執行結束之后,會執行 exit(0) 來退出進程。若執行 exit(0) 后,進程沒有退出,main 函數最后的代碼會一直訪問非法地址:
exit(0) for { var x *int32 *x=0 }
正常情況下,一旦出現非法地址訪問,系統會把進程殺死,用這樣的方法確保進程退出。
關于程序退出這一段的闡述來自群聊《golang runtime 閱讀》,又是一個高階的讀源碼的組織,github 主頁見參考資料。
當然 Go 程序啟動這一部分其實還會涉及到 fork 一個新進程、裝載可執行文件,控制權轉移等問題。還是推薦看前面的兩本書,我覺得我不會寫得更好,就不敘述了。
GoRoot 是 Go 的安裝路徑。mac 或 unix 是在 /usr/local/go 路徑上,來看下這里都裝了些什么:
bin 目錄下面:
pkg 目錄下面:
Go 工具目錄如下,其中比較重要的有編譯器 compile,鏈接器 link:
GoPath 的作用在于提供一個可以尋找 .go 源碼的路徑,它是一個工作空間的概念,可以設置多個目錄。Go 官方要求,GoPath 下面需要包含三個文件夾:
src pkg bin
src 存放源文件,pkg 存放源文件編譯后的庫文件,后綴為 .a;bin 則存放可執行文件。
直接在終端執行:
go
就能得到和 go 相關的命令簡介:
和編譯相關的命令主要是:
go build go install go run
go build 用來編譯指定 packages 里的源碼文件以及它們的依賴包,編譯的時候會到 $GoPath/src/package 路徑下尋找源碼文件。go build 還可以直接編譯指定的源碼文件,并且可以同時指定多個。
通過執行 go help build 命令得到 go build 的使用方法:
usage: go build [-o output] [-i] [build flags] [packages]
-o 只能在編譯單個包的時候出現,它指定輸出的可執行文件的名字。
-i 會安裝編譯目標所依賴的包,安裝是指生成與代碼包相對應的 .a 文件,即靜態庫文件(后面要參與鏈接),并且放置到當前工作區的 pkg 目錄下,且庫文件的目錄層級和源碼層級一致。
至于 build flags 參數,build, clean, get, install, list, run, test 這些命令會共用一套:
我們知道,Go 語言的源碼文件分為三類:命令源碼、庫源碼、測試源碼。
命令源碼文件:是 Go 程序的入口,包含 func main() 函數,且第一行用 package main 聲明屬于 main 包。
庫源碼文件:主要是各種函數、接口等,例如工具類的函數。
測試源碼文件:以 _test.go 為后綴的文件,用于測試程序的功能和性能。
注意,go build 會忽略 *_test.go 文件。
我們通過一個很簡單的例子來演示 go build 命令。我用 Goland 新建了一個 hello-world 項目(為了展示引用自定義的包,和之前的 hello-world 程序不同),項目的結構如下:
最左邊可以看到項目的結構,包含三個文件夾:bin,pkg,src。其中 src 目錄下有一個 main.go,里面定義了 main 函數,是整個項目的入口,也就是前面提過的所謂的命令源碼文件;src 目錄下還有一個 util 目錄,里面有 util.go 文件,定義了一個可以獲取本機 IP 地址的函數,也就是所謂的庫源碼文件。
中間是 main.go 的源碼,引用了兩個包,一個是標準庫的 fmt;一個是 util 包,util 的導入路徑是 util。所謂的導入路徑是指相對于 Go 的源碼目錄 $GoRoot/src 或者 $GoPath/src 的下的子路徑。例如 main 包里引用的 fmt 的源碼路徑是 /usr/local/go/src/fmt,而 util 的源碼路徑是 /Users/qcrao/hello-world/src/util,正好我們設置的 GoPath=/Users/qcrao/hello-world。
最右邊是庫函數的源碼,實現了獲取本機 IP 的函數。
在 src 目錄下,直接執行 go build 命令,在同級目錄生成了一個可執行文件,文件名為 src,使用 ./src 命令直接執行,輸出:
hello world! Local IP: 192.168.1.3
我們也可以指定生成的可執行文件的名稱:
go build -o bin/hello
這樣,在 bin 目錄下會生成一個可執行文件,運行結果和上面的 src 一樣。
其實,util 包可以單獨被編譯。我們可以在項目根目錄下執行:
go build util
編譯程序會去 $GoPath/src 路徑找 util 包(其實是找文件夾)。還可以在 ./src/util 目錄下直接執行 go build 編譯。
當然,直接編譯庫源碼文件不會生成 .a 文件,因為:
go build 命令在編譯只包含庫源碼文件的代碼包(或者同時編譯多個代碼包)時,只會做檢查性的編譯,而不會輸出任何結果文件。
為了展示整個編譯鏈接的運行過程,我們在項目根目錄執行如下的命令:
go build -v -x -work -o bin/hello src/main.go
-v 會打印所編譯過的包名字,-x 打印編譯期間所執行的命令,-work 打印編譯期間生成的臨時文件路徑,并且編譯完成之后不會被刪除。
執行結果:
從結果來看,圖中用箭頭標注了本次編譯過程涉及 2 個包:util,command-line-arguments。第二個包比較詭異,源碼里根本就沒有這個名字好嗎?其實這是 go build 命令檢測到 [packages] 處填的是一個 .go 文件,因此創建了一個虛擬的包:command-line-arguments。
同時,用紅框圈出了 compile, link,也就是先編譯了 util 包和 main.go 文件,分別得到 .a 文件,之后將兩者進行鏈接,最終生成可執行文件,并且移動到 bin 目錄下,改名為 hello。
另外,第一行顯示了編譯過程中的工作目錄,此目錄的文件結構是:
可以看到,和 hello-world 目錄的層級基本一致。command-line-arguments 就是虛擬的 main.go 文件所處的包。exe 目錄下的可執行文件在最后一步被移動到了 bin 目錄下,所以這里是空的。
整體來看,go build 在執行時,會先遞歸尋找 main.go 所依賴的包,以及依賴的依賴,直至最底層的包。這里可以是深度優先遍歷也可以是寬度優先遍歷。如果發現有循環依賴,就會直接退出,這也是經常會發生的循環引用編譯錯誤。
正常情況下,這些依賴關系會形成一棵倒著生長的樹,樹根在最上面,就是 main.go 文件,最下面是沒有任何其他依賴的包。編譯器會從最左的節點所代表的包開始挨個編譯,完成之后,再去編譯上一層的包。
這里,引用郝林老師幾年前在 github 上發表的 go 命令教程,可以從參考資料找到原文地址。
從代碼包編譯的角度來說,如果代碼包 A 依賴代碼包 B,則稱代碼包 B 是代碼包 A 的依賴代碼包(以下簡稱依賴包),代碼包 A 是代碼包 B 的觸發代碼包(以下簡稱觸發包)。
執行 go build 命令的計算機如果擁有多個邏輯 CPU 核心,那么編譯代碼包的順序可能會存在一些不確定性。但是,它一定會滿足這樣的約束條件:依賴代碼包 -> 當前代碼包 -> 觸發代碼包。
順便推薦一個瀏覽器插件 Octotree,在看 github 項目的時候,此插件可以在瀏覽器里直接展示整個項目的文件結構,非常方便:
到這里,你一定會發現,對于 hello-wrold 文件夾下的 pkg 目錄好像一直沒有涉及到。
其實,pkg 目錄下面應該存放的是涉及到的庫文件編譯后的包,也就是一些 .a 文件。但是 go build 執行過程中,這些 .a 文件放在臨時文件夾中,編譯完成后會被直接刪掉,因此一般不會用到。
前面我們提到過,在 go build 命令里加上 -i 參數會安裝這些庫文件編譯的包,也就是這些 .a 文件會放到 pkg 目錄下。
在項目根目錄執行 go build -i src/main.go 后,pkg 目錄里增加了 util.a 文件:
darwin_amd64 表示的是:
GOOS 和 GOARCH。這兩個環境變量不用我們設置,系統默認的。
GOOS 是 Go 所在的操作系統類型,GOARCH 是 Go 所在的計算架構。
Mac 平臺上這個目錄名就是 darwin_amd64。
生成了 util.a 文件后,再次編譯的時候,就不會再重新編譯 util.go 文件,加快了編譯速度。
同時,在根目錄下生成了名稱為 main 的可執行文件,這是以 main.go 的文件名命令的。
hello-world 這個項目的代碼已經上傳到了 github 項目 Go-Questions,這個項目由問題導入,企圖串連 Go 的所有知識點,正在完善,期待你的 star。 地址見參考資料【Go-Questions hello-world項目】。
go install 用于編譯并安裝指定的代碼包及它們的依賴包。相比 go build,它只是多了一個“安裝編譯后的結果文件到指定目錄”的步驟。
還是使用之前 hello-world 項目的例子,我們先將 pkg 目錄刪掉,在項目根目錄執行:
go install src/main.go 或者 go install util
兩者都會在根目錄下新建一個 pkg 目錄,并且生成一個 util.a 文件。
并且,在執行前者的時候,會在 GOBIN 目錄下生成名為 main 的可執行文件。
所以,運行 go install 命令,庫源碼包對應的 .a 文件會被放置到 pkg 目錄下,命令源碼包生成的可執行文件會被放到 GOBIN 目錄。
go install 在 GoPath 有多個目錄的時候,會產生一些問題,具體可以去看郝林老師的 Go 命令教程,這里不展開了。
go run 用于編譯并運行命令源碼文件。
在 hello-world 項目的根目錄,執行 go run 命令:
go run -x -work src/main.go
-x 可以打印整個過程涉及到的命令,-work 可以看到臨時的工作目錄:
從上圖中可以看到,仍然是先編譯,再連接,最后直接執行,并打印出了執行結果。
第一行打印的就是工作目錄,最終生成的可執行文件就是放置于此:
main 就是最終生成的可執行文件。
這次的話題太大了,困難重重。從編譯原理到 go 啟動時的流程,到 go 命令原理,每個話題單獨抽出來都可以寫很多。
幸好有一些很不錯的書和博客文章可以去參考。這篇文章就作為一個引子,你可以跟隨參考資料里推薦的一些內容去發散。
【《程序員的自我修養》全書】https://book.douban.com/subject/3652388/
【面向信仰編程 編譯過程概述】https://draveness.me/golang-compile-intro
【golang runtime 閱讀】https://github.com/zboya/golang_runtime_reading
【Go-Questions hello-world項目】https://github.com/qcrao/Go-Questions/tree/master/examples/hello-world
【雨痕大佬的 Go 語言學習筆記】https://github.com/qyuhen/book
【vim 以 16 進制文本】https://www.cnblogs.com/meibenjin/archive/2012/12/06/2806396.html
【Go 編譯命令執行過程】https://halfrost.com/go_command/
【Go 命令執行過程】https://github.com/hyper0x/go_command_tutorial
【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-GOPATH_environment_variable
【Go package 的探索】https://mp.weixin.qq.com/s/OizVLXfZ6EC1jI-NL7HqeA
【Go 官方 關于 Go 項目的組織結構】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.ct8olib.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 的創建】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
本文作者:饒全成,原創授權發布
o語言中文網,致力于每日分享編碼、開源等知識,歡迎關注我,會有意想不到的收獲!
以常規方式編寫并發程序,需要對共享變量作正確的訪問控制,處理起來很困難。而golang提出一種不同的方式,即共享變量通過channel傳遞,共享變量從不被各個獨立運行的線程(goroutine)同時享有,在任一時刻,共享變量僅可被一個goroutine訪問。所以,不會產生數據競爭。并發編程,golang鼓勵以此種方式進行思考,精簡為一句口號——“勿通過共享內存來進行通信,而應通過通信來進行內存共享”。
Unbuffered channels的接收者阻塞直至收到消息,發送者阻塞直至接收者接收到消息,該機制可用于兩個goroutine的狀態同步。Buffered channels在緩沖區未滿時,發送者僅在值拷貝到緩沖區之前是阻塞的,而在緩沖區已滿時,發送者會阻塞,直至接收者取走了消息,緩沖區有了空余。
如下代碼使用Unbuffered channel作同步控制。給定一個整型數組,在主routine啟動另一個goroutine將該數組排序,當其完成時,給done channel發送完成消息,主routine會一直等待直至排序完成,打印結果。
如下代碼中,messages chan的緩沖區大小為2,因其為Buffered channel,所以消息發送與接收無須分開到兩個并發的goroutine中。
函數封裝時,對僅作消息接收或僅作消息發送的chan標識direction可以借用編譯器檢查增強類型使用安全。如下代碼中,ping函數中pings chan僅用來接收消息,所以參數列表中將其標識為接收者。pong函數中,pings chan僅用來發送消息,pongs chan僅用來接收消息,所以參數列表中二者分別標識為發送者與接收者。
使用select可以用來等待多個channel的消息,如下代碼,創建兩個chan,啟動兩個goroutine耗費不等時間計算結果,主routine監聽消息,使用兩次select,第一次接收到了ch2的消息,第二次接收到了ch1的消息,用時2.000521146s。
select with default可以用來處理非阻塞式消息發送、接收及多路選擇。如下代碼中,第一個select為非阻塞式消息接收,若收到消息,則落入<-messages case,否則落入default。第二個select為非阻塞式消息發送,與非阻塞式消息接收類似,因messages chan為Unbuffered channel且無異步消息接收者,因此落入default case。第三個select為多路非阻塞式消息接收。
當無需再給channel發送消息時,可將其close。如下代碼中,創建一個Buffered channel,首先啟動一個異步goroutine循環消費消息,然后主routine完成消息發送后關閉chan,消費goroutine檢測到chan關閉后,退出循環。
2.5 for range
for range語法不僅可對基礎數據結構(slice、map等)作迭代,還可對channel作消息接收迭代。如下代碼中,給messages chan發送兩條消息后將其關閉,然后迭代messages chan打印消息。
資源訪問、網絡請求等場景作超時控制是非常必要的,可以使用channel結合select來實現。如下代碼,對常規sum函數增加超時限制,sumWithTimeout函數中,select的v :=<-rlt在等待計算結果,若在時限范圍內計算完成,則正常返回計算結果,若超過時限則落入<-time.After(timeout) case,拋出timeout error。
本文代碼托管地址:https://github.com/olzhy/go-excercises/tree/master/channels
參考資料
[1] https://golang.org/doc/effective_go.html#channels
[2] https://gobyexample.com/channel-synchronization
[3] https://gobyexample.com/channel-buffering
[4] https://gobyexample.com/channel-directions
[5] https://gobyexample.com/select
[6] https://gobyexample.com/non-blocking-channel-operations
[7] https://gobyexample.com/closing-channels
[8] https://gobyexample.com/range-over-channels
[9] https://gobyexample.com/timeouts
原文:https://leileiluoluo.com/posts/golang-channels.html
本文作者:磊磊落落的博客,原創授權發布
為一款網紅編程語言,Go語言還十分年輕,很多程序員無法及時了解到Go語言的框架、庫和軟件應用。近日,Github用戶avelino分享了一張非常完整且龐大的表單,包括命令行、數據庫、Web框架、機器學習、自然語言處理......以下是部分內容截取,感謝avelino的分享。
標準CLI
用于構建標準或基本命令行應用程序的庫。
argv - 使用bash語法將庫命令行字符串拆分為參數數組。
cli - 基于golang的功能豐富且易于使用的命令行程序包。
cli-init - 開始構建Golang命令行應用程序的簡單方法。
climax - 具有“human face”的替代CLI。
cobra - CLI交互指揮官。
complete - 在Go + Go命令bash完成中寫入bash完成。
docopt.go - 命令行參數解析器。
drive - Google Drive客戶端命令行。
env - 基于標簽的結構環境配置。·
flag - 簡單而強大的命令行選項解析庫支持Go子命令。
go-arg - 在Go中基于結構的參數解析。
go-flags - go命令行選項解析器。
kingpin - 支持子命令的命令行和標志解析器。
liner - 用于命令行接口的類似于readline的庫。
mitchellh/ cli - 用于實現命令行界面的庫。
mow.cli - 用于構建具有復雜標志和參數解析驗證的CLI應用程序庫。
pflag - 替換Go的flag包,實現POSIX/GNU-style --flags。
readline - 純Golang實現,在MIT許可下提供GNU-Readline中的大部分功能。
sflags - 基于結構的標志生成器,用于flag, urfave/cli, pflag, cobra, kingpin和其他庫。
ukautz/ clif - 小型命令行界面框架。
urfave/ cli - 在Go(以前的codegangsta / cli)中構建命令行應用程序的簡單,快速和有趣的包。
wlog - 支持跨平臺顏色和并發性的簡單日志記錄界面。
wmenu - 易于使用的菜單結構,用于提示用戶進行選擇的cli應用程序。
高級控制臺UI
用于構建控制臺應用程序和控制臺用戶界面的庫。
aurora - 支持fmt.Printf / Sprintf的ANSI終端顏色。
chalk - 直觀的包裝,用于優化終端/控制臺輸出。
color - 用于彩色終端輸出的多功能包裝。
colourize - 終端中ANSI文本顏色的Go庫。
go-ataman - Go庫,用于在終端中呈現ANSI彩色文本模板。
go-colorable - Windows的可著色畫筆。
go-colortext - 用于在終端中輸出顏色的庫。
gocui - Minimalist —Go庫旨在創建控制臺用戶界面。
gommon / color - Style終端文本。
mpb - 終端應用程序的多進度條。
termbox-go - Termbox是一個用于創建跨平臺的、基于文本的界面的庫。
termtables - 將Ruby庫終端表的端口用于簡單的ASCII表生成以及提供HTML輸出。
termui - 終端儀表板,基于termbox-go,并受到blessed-contrib的啟發。
uilive - 用于實時更新終端輸出的庫。
uiprogress - 靈活的庫用于在終端應用程序中呈現進度條。
uitable - 使用表格數據提高終端應用程序的可讀性。
數據結構
Go中的通用數據結構和算法。
binpacker - 二進制打包程序和解包程序可幫助用戶構建自定義二進制流。
bit - Golang設置數據結構,帶有加密的bit-twiddling功能。
bitset - Go包執行位組。
bloom - 在Go中實現的Bloom過濾器。
bloom - Golang Bloom過濾器實現。
boomfilters - 用于處理連續,無界流的概率數據結構。
concurrent-writer - bufio.Writer的高度并發插件替換。
count-min-log - 執行計數最小日志草圖:使用近似計數器近似計數。
encoding - 整數壓縮庫。
go-adaptive-radix-tree - 執行自適應基數樹。
go-datastructures - 收集有用的,執行的和線程安全的數據結構。
go-ef - 執行Elias-Fano編碼。
go-geoindex - 內存geo索引。
go-rquad - 具有高效點位置和鄰居查找的區域四叉樹。
gods-數據結構。容器,集合,列表,堆棧,地圖,BidiMaps,樹,HashSet等
gangang-set - 線程安全和非線程安全的高性能Go集合。
goset - Go的一個有用的集合實現。
goskiplist - Go中的 Skip list實現。
goota - 數據框架和數據爭用方法實現。
hilbert - Go包,用于將值映射到空格填充曲線(如Hilbert和Peano曲線)。
hyperloglog - HyperLogLog實現與稀疏,LogLog-Beta偏差校正和TailCut空間縮減。
levenshtein - Levenshtein距離和相似性度量。
levenshtein - 在Go中計算levenshtein距離的實現。
mafsa - MA-FSA實現與最小完美哈希。
merkletree - 實現一個merkle樹,提供數據結構內容的高效安全驗證。
ttlcache - 內存中的LRU string-interface {}映射
willf/ bloom - 執行Bloom過濾器的包。
數據庫
Go中實現的數據庫。
badger - 快捷鍵值對存儲。
BigCache - 高效的鍵/值緩存,用于千兆字節數據。
bolt - Go的低級鍵/值數據庫。
buntdb - 具有自定義索引和空間支持的快速可嵌入內存中的鍵/值數據庫。
cache2go - 內存中的Key:value緩存,支持基于超時的自動無效。
cockroach - 可擴展,地理復制,事務性數據存儲。
couchcache - 由Couchbase服務器支持的RESTful緩存微服務。
dgraph - 可擴展,分布式,低延遲,高吞吐量圖形數據庫。
diskv - 支持鍵值存儲。
eliasdb - 具有REST API,短語搜索和類似SQL的查詢語言的無依賴關系的事務圖數據庫。
forestdb - ForestDB綁定。
GCache - 緩存庫,支持可預見的Cache,LFU,LRU和ARC。
geocache - 內存緩存,適用于基于位置的應用程序。
go-cache - 內存key:value存儲/緩存(類似于Memcached)庫,適用于單機應用程序。
goleveldb - 在Go中實現LevelDB鍵/值數據庫。
groupcache - Groupcache是一個緩存和緩存填充庫,用于在許多情況下替代memcached。
influxdb - 可擴展的數據存儲區,用于度量,事件和實時分析。
ledisdb - Ledisdb是一個基于LevelDB的高性能NoSQL,如Redis。
levigo - Levigo是LevelDB的Go包裝器。
Moss - Moss是一個簡單的LSM鍵值存儲引擎,用100%的Go語言編寫。
piladb - 基于堆棧數據結構的輕量級RESTful數據庫引擎。
prometheus - 監控系統和時間序列數據庫。
rqlite - 構建在SQLite上的輕量級,分布式,關系型數據庫。
Scribble - 微小的平面文件JSON存儲。
tempdb - 臨時項目的鍵值存儲。
tidb - TiDB是一個分布式SQL數據庫。靈感來自于Google F1的設計。
tiedot - 由Golang提供支持的NoSQL數據庫。
Tile38 - 具有空間索引和實時地理位置的地理數據庫。
數據庫模式遷移
darwin - Go的數據庫模式演化庫。
go-fixtures - 用于Golang內置數據庫/ sql庫的Django樣式裝置。
goose - 數據庫遷移工具,可以通過創建增量SQL或Go腳本來管理數據庫的演進。
gormigrate - Gorm ORM的數據庫模式遷移幫助器。
migrate - 數據庫遷移。CLI和Golang庫。
pravasan - 簡單的遷移工具 - 目前用于MySQL,但計劃即將支持Postgres,SQLite,MongoDB等。
soda - MySQL,PostgreSQL和SQLite的數據庫遷移,創建,ORM等。
sql-migrate - 數據庫遷移工具。允許使用go-bindata將遷移嵌入到應用程序中。
數據庫工具
go-mysql - Go工具集來處理MySQL協議和復制。
go-mysql-elasticsearch - 自動將MySQL數據同步到彈性搜索。
kingshard - kingshard是由Golang提供的MySQL高性能代理。
myreplication - MySql二進制日志復制偵聽器,支持語句和基于行的復制。
orchestrator - MySQL復制拓撲管理器和可視化器。
pgweb - 基于Web的PostgreSQL數據庫瀏覽器。
pREST - 從任何PostgreSQL數據庫提供RESTful API。
vitess - 提供服務器和工具,便于MySQL數據庫擴展大型Web服務。
SQL查詢構建器,用于構建和使用SQL庫
dat - Postgres數據訪問工具包。
Dotsql - 將sql文件保存在一個地方并輕松使用。
goqu - 慣用SQL構建器和查詢庫。
igor - PostgreSQL抽象層,支持高級功能,并使用類似gorm的語法。
ozzo-dbx - 強大的數據檢索方法以及與數據庫無關的查詢構建能力。
scaneo - 生成Go代碼將數據庫行轉換為任意結構。
sqrl - SQL查詢生成器,性能提升。
Squirrel - 構建SQL查詢的庫。
xo - 根據現有架構定義或支持PostgreSQL,MySQL,SQLite,Oracle和Microsoft SQL Server的自定義查詢,為數據庫生成慣用Go代碼。
機器學習
機器學習庫
bayesian - Go語言的樸素貝葉斯分類。
CloudForest - 以純Go為機器學習的快速,靈活,多線程的決策樹組合。
gago - 靈活并行的遺傳算法。
go-fann - 快速人工神經網絡(FANN)庫的綁定。
gogo galib - Go / golang編寫的遺傳算法庫。
go-pr - Golang中的圖像識別包。
gobrain - Go語言寫的神經網絡。
godist - 各種概率分布和相關方法。
goga - Go的遺傳算法庫。
GoLearn - Go的通用機器學習庫。
golinear - Go的liblinear綁定。
goml - 在線機器學習。
goRecommend - 使用Go編寫的推薦算法庫。
gorgonia - 基于圖形的計算庫,如Theano for Go,為構建各種機器學習和神經網絡算法提供原始數據。
goscore - 獲取PMML的API。
libsvm - libsvm golang版本派生工作基于LIBSVM 3.14。
mlgo - 該項目旨在提供Go中的簡約機器學習算法。
neat -NeuroEvolution增強拓撲(NEAT)的即插即用并行Go框架。
neural-go —Go中實施的多層感知網絡,通過反向傳播進行培訓。
probab - 概率分布函數。
regommend - 推薦和協同過濾引擎。
shield - 貝葉斯文本分類器,具有靈活的標記器和Go存儲后端。
自然語言處理
dpar - 基于過渡的統計依賴解析器。
go-eco - 相似性,不相似性和距離矩陣;多樣性,公平和不平等的措施;物種豐富度估計; coenocline模型。
go-i18n - 使用本地化文本的軟件包和隨附工具。
go-mystem - CGo綁定到Yandex.Mystem - 俄語形態分析器。
go-nlp - 使用離散概率分布和其他可用于執行NLP工具的實用程序。
go-stem - porter stemming算法實現。
go-unidecode - Unicode文本的ASCII音譯。
go2vec - word2vec嵌入式閱讀器和效用函數。
gojieba - 這是一個Go執行的jieba中文分詞算法。
gounidecode - 用于Go的Unicode音譯(也稱為unidecode)。
icu - Cgo綁定icu4c C庫檢測和轉換功能。保證與版本50.1兼容。
libtextcat - 用于libtextcat C庫的Cgo綁定。保證與版本2.2的兼容性。
MMSEGO - 這是一個中文分詞算法MMSEG的GO實現。
nlp - 從字符串中提取值并使用nlp填充結構體。
nlp - 自然語言處理庫支持LSA(潛在語義分析)。
paicehusk - Golang實施Paice / Husk Stemming算法。
porter - 這是一個非常簡單的 Martin Porter實現Porter干擾算法的端口。
prose - 支持標記化,詞性標注,命名實體提取等的文本處理庫。
RAKE.go - 快速自動關鍵詞提取算法(RAKE)的端口。
stemmer - 用于Go的Stemmer包。包括英語和德語詞干。
textcat - 基于n-gram的文本分類Go包,支持utf-8和原始文本。
whatlanggo - Go的自然語言檢測包。支持84種語言和24種腳本(寫作系統,如拉丁語,西里爾字體等)。
when - 具有可插拔規則的自然EN和RU語言日期/時間解析器。
如果你覺得這些還不過癮,可以去Github頁面(項目源地址:https://github.com/avelino/awesome-go#web-frameworks)與眾多Go語言程序員互動。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。