Part1 引言
go語言的一個(gè)優(yōu)勢(shì)是能夠生成靜態(tài)鏈接的可執(zhí)行程序。但是,這并不是說默認(rèn)情況下編譯出來的Go可執(zhí)行程序都是靜態(tài)鏈接的。在有些情況下,需要額外的操作才能實(shí)現(xiàn)。具體情況取決于操作系統(tǒng),本文介紹unix系統(tǒng)下如何達(dá)成這一目標(biāo)。
Part2 示例
下面是用Go語言編寫的hello world程序,在linux機(jī)器上將其編譯成可執(zhí)行文件。然后檢查該可執(zhí)行文件是靜態(tài)鏈接還是動(dòng)態(tài)鏈接。
代碼語言:JavaScript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
package mainimport "fmt"func main() { fmt.Println("hello world")}
編譯helloworld可執(zhí)行文件,操作如下。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048164940570.jpg)
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048164998250.jpg)
Part3 檢查可執(zhí)行程序是動(dòng)態(tài)鏈接還是靜態(tài)鏈接
有多種方法檢查可執(zhí)行程序鏈接類型,這里介紹三種方法:
1 通過file命令
file命令輸出中明確說明helloworld可執(zhí)行文件為 Statically linked類型,即靜態(tài)鏈接。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048164993582.jpg)
2 通過ldd命令
ldd命令會(huì)輸出helloworld程序依賴的動(dòng)態(tài)庫,由于helloworld非動(dòng)態(tài)鏈接,所以輸出結(jié)果為 不是動(dòng)態(tài)可執(zhí)行程序
3 通過nm命令
使用nm命令列舉出helloworld中未定義符號(hào)(期望在鏈接運(yùn)行時(shí)通過動(dòng)態(tài)庫加載)。輸出為空,表明helloworld中沒有任何未定義符號(hào)。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165019928.jpg)
Part4 DNS和用戶組
在Unix機(jī)器上,當(dāng)滿足特定條件時(shí),Go語言標(biāo)準(zhǔn)庫會(huì)借助libc實(shí)現(xiàn)DNS和用戶組功能。
當(dāng)cgo啟用時(shí)(默認(rèn)情況下,在Unix系統(tǒng)下 CGO_ENABLED 值為1,表示默認(rèn)開啟)。Go語言標(biāo)準(zhǔn)庫中的net包會(huì)調(diào)用c語言庫實(shí)現(xiàn)DNS查找功能。這意味著Go程序在進(jìn)行域名解析時(shí),會(huì)使用底層操作系統(tǒng)提供的C庫函數(shù),如getaddrinfo和getnameinfo。同理,當(dāng)啟用cgo時(shí),Go語言標(biāo)準(zhǔn)庫中的os/user包會(huì)調(diào)用C語言庫來查找用戶和用戶組。
DNS查詢代碼如下,保存在lookuphost.go文件中。
代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
package mainimport ( "fmt" "net")func main() { fmt.Println(net.LookupHost("go.dev"))}
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165060861.jpg)
編譯出可執(zhí)行程序 lookuphost
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165025879.jpg)
通過ldd命令可以看出lookuphost可執(zhí)行程序是一個(gè)動(dòng)態(tài)鏈接,需要在運(yùn)行時(shí)通過ilbc加載共享庫。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165043154.jpg)
為啥編譯出來的程序是動(dòng)態(tài)鏈接在官方net包文檔中有詳細(xì)說明。Go語言標(biāo)準(zhǔn)庫也提供了純Go實(shí)現(xiàn)的DNS功能(盡管純Go實(shí)現(xiàn)可能缺少一些高級(jí)特性),如果我們想使用純Go實(shí)現(xiàn),而不是依賴于系統(tǒng)的libc,可以在編譯的時(shí)候設(shè)置tags實(shí)現(xiàn)。具體操作如下:
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165040151.jpg)
此外,我們還可以通過關(guān)閉cgo來編譯出靜態(tài)鏈接程序。在Unix系統(tǒng)中,默認(rèn)cgo是開啟的,查看方法如下。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165073208.jpg)
關(guān)閉cgo再次編譯lookuphost程序。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165046546.jpg)
查找用戶的用戶組信息程序如下,代碼保存在userlookup.go文件中。
代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
package mainimport ( "encoding/json" "log" "os" "os/user")func main() { user, err := user.Lookup("bob") if err != nil { log.Fatal(err) } je := json.NewEncoder(os.Stdout) je.Encode(user)}
編譯上述代碼,然后通過ldd命令看到可執(zhí)行程序?yàn)閯?dòng)態(tài)鏈接。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165026424.jpg)
同上面的DNS程序,我們可以添加編譯tags,將其編譯為靜態(tài)鏈接程序。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165012252.jpg)
此外,我們也可以通過關(guān)閉cgo達(dá)到同樣的效果。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165172630.jpg)
Part5 將C代碼鏈接到Go二進(jìn)制文件
Go語言支持通過cgo調(diào)用C語言中的函數(shù)接口(FFI),下面通過一個(gè)具體例子說明,下述代碼保存在cstdio.go文件中。
代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
package main//#include <stdio.h>// void helloworld(){// printf("hello,world from Cn");//}import "C"func main() { C.helloworld()}
由于使用了cgo,C代碼中調(diào)用了printf函數(shù),該函數(shù)屬于libc,即使程序中沒有顯式調(diào)用C運(yùn)行時(shí),當(dāng)Go代碼通過cgo與C代碼交互時(shí),cgo會(huì)生成一些“膠水代碼”,確保程序能夠正常執(zhí)行。通過ldd命令可以看到上述程序編譯后的可執(zhí)行文件為動(dòng)態(tài)鏈接類型。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165155220.jpg)
注意:即使我們編寫的程序中沒有C代碼,cgo也可能會(huì)涉及。因?yàn)槲覀兂绦蛞玫囊蕾噹炜赡苁褂昧薱go,像常用的go-sqlite3驅(qū)動(dòng)程序就需要cgo,如果我們編寫的程序?qū)肓嗽摪瑒t自然使用了cgo。
這種情況下,通過關(guān)閉CGO_ENABLED是無效的。那有什么解決方法嗎?請(qǐng)看下一章節(jié)內(nèi)容。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165175509.jpg)
Part6 鏈接靜態(tài)libc
如果Go程序包含了C代碼,在Unix系統(tǒng)上編譯出來的二進(jìn)制文件是動(dòng)態(tài)鏈接。具體原因如下:
C代碼調(diào)用libc(C運(yùn)行時(shí))在Unix系統(tǒng)上通常使用的libc是glibc推薦的方式是通過動(dòng)態(tài)鏈接到glibc因此,go build得到的二進(jìn)制文件是動(dòng)態(tài)鏈接類型
我們可以換用其他的libc,而不是默認(rèn)的glibc,比如使用靜態(tài)鏈接的libc,這樣編譯后的可執(zhí)行文件就是靜態(tài)的。目前musl就是一個(gè)滿足我們期望的libc,它是一個(gè)輕量級(jí)的C標(biāo)準(zhǔn)庫,兼容POSIX的API,是許多靜態(tài)鏈接應(yīng)用程序和容器化應(yīng)用程序的首選。
1 安裝musl從官網(wǎng)下載musl源碼
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165120677.jpg)
解壓源碼壓縮包 musl-1.2.5.tar.gz
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165183043.jpg)
進(jìn)入 musl-1.2.5目錄,執(zhí)行 ./configure –prefix=
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165176381.jpg)
2 編譯
下面采用musl對(duì)cstdio.go文件進(jìn)行重行編譯,操作如下。CC告訴go build 使用哪個(gè)c編譯器進(jìn)行cgo編譯。后面的連接器參數(shù)設(shè)置使用外部連接器。最后執(zhí)行靜態(tài)鏈接。詳細(xì)信息閱讀官方文檔
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165129042.jpg)
上述編譯靜態(tài)程序的方法同樣適用于復(fù)雜程序,作者提供了一個(gè)use-sqlite.go文件,使用了go-sqlite3包,下面通過兩種方式編譯對(duì)比效果。
代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
// use-sqlite.go package mainimport ( "database/sql" "fmt" "log" _ "github.com/mattn/go-sqlite3")func main() { // Open the database file in /tmp/ db, err := sql.Open("sqlite3", "/tmp/example.db") if err != nil { log.Fatal(err) } defer db.Close() // Create a table if it doesn't exist createTableSQL := `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER NOT NULL );` _, err = db.Exec(createTableSQL) if err != nil { log.Fatal(err) } // Insert some data insertUserSQL := `INSERT INTO users (name, age) VALUES (?, ?)` _, err = db.Exec(insertUserSQL, "Alice", 30) if err != nil { log.Fatal(err) } _, err = db.Exec(insertUserSQL, "Bob", 25) if err != nil { log.Fatal(err) } // Query the data querySQL := `SELECT id, name, age FROM users` rows, err := db.Query(querySQL) if err != nil { log.Fatal(err) } defer rows.Close() fmt.Println("User data:") for rows.Next() { var id int var name string var age int err = rows.Scan(&id, &name, &age) if err != nil { log.Fatal(err) } fmt.Printf("ID: %d, Name: %s, Age: %dn", id, name, age) } err = rows.Err() if err != nil { log.Fatal(err) }}
采用默認(rèn)編譯器編譯
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165115021.jpg)
采用musl編譯
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165153099.jpg)
采用musl,我們可以在不使用-tags netgo標(biāo)簽,也不用禁用cgo的情況,編譯一個(gè)靜態(tài)鏈接的lookuphost程序。
![在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯] 在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]](https://img.php.cn/upload/article/001/503/042/175048165295545.jpg)
Part7 使用Zig作為C編譯器
Zig 是一種新的系統(tǒng)編程語言,與Go語言類似,它也提供了一系列編譯工具即Zig工具鏈,該工具鏈包含有Zig編譯器、C/c++編譯器、鏈接器和用于靜態(tài)鏈接的libc。所以Zig可以用來將Go二進(jìn)制文件與C代碼靜態(tài)鏈接。
安裝Zig后采用下面的命令編譯可執(zhí)行程序,其中ZIGDIR為Zig的安裝目錄。相比上一章節(jié)的musl-gcc,調(diào)用命令會(huì)簡(jiǎn)單一些。
代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
$ CC="$ZIGDIR/zig cc -target x86_64-linux-musl" go build cstdio.go$ CC="$ZIGDIR/zig cc -target x86_64-linux-musl" go build use-sqlite.go
Part8 總結(jié)大部分情況下Go語言編譯出來的可執(zhí)行程序是靜態(tài),但在某些情況下默認(rèn)編譯出來的是動(dòng)態(tài)鏈接,我們可以設(shè)置build tags或關(guān)閉cgo來編譯靜態(tài)鏈接程序。Go語言中有一個(gè)提案,即在 go build 命令中添加一個(gè) -static 標(biāo)志,表明將程序編譯為靜態(tài)類型,目前還未實(shí)現(xiàn)。