視頻筆記:Go 的構建模式 - David Crawshaw

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
  • 視頻資訊
  • 什麼是 Build Mode?
  • Go 的八種 Build Mode
    • exe (靜態編譯)
    • exe (用 libc)
    • exe (動態連結 libc 和非 Go 代碼)
    • pie - Position Independent Executables
    • c-archive C 的靜態連結庫
    • c-shared C 的動態連結程式庫
      • 為什麼會需要動態連結?
    • shared Go 的動態連結程式庫
    • plugin Go 的外掛程式
  • 優缺點
  • 未來

視頻資訊 #

Go Build Modes
by David Crawshaw, Google
at GopherCon 2017

https://www.youtube.com/watch?v=x-LhC-J2Vbk

什麼是 Build Mode? #

build mode 用於指導編譯器如何建立可執行二進位檔案。越多的執行方式,就意味著可以讓 Go 程式運行於更多的位置。

Go 的八種 Build Mode #

  • exe (靜態編譯)
  • exe (動態連結 libc)
  • exe (動態連結 libc 和非 Go 代碼)
  • pie 地址無關可執行檔(安全特性)
  • c-archive C 的靜態連結庫
  • c-shared C 的動態連結程式庫
  • shared Go 的動態連結程式庫
  • plugin Go 的外掛程式

exe (靜態編譯) #

這個是大家最喜歡的,所有的代碼都構建到一個可執行檔了。

1
CGO_ENABLED=0 go build hello.go

這是大家使用 Go 最喜歡的構建方式。所有的依賴都構建到了一個二進位檔案了,沒有任何外部依賴,可執行檔直接調用 syscall 和核心通訊。

這裡使用 CGO_ENABLED=0 來約束不使用任何 CGO 的部分,這樣不會依賴 libc 這類庫。

exe (用 libc) #

這樣的可執行檔大部分都是靜態編譯,只不過使用了 libc 動態連結程式庫,因此像一些 net 包的操作,比如 DNS 查詢、os/user 的使用者名稱查詢等等,這些會使用系統提供的 libc 動態連結程式庫。

其好處是,可以利用系統特定的實現,保證行為和系統一致。

exe (動態連結 libc 和非 Go 代碼) #

當程式編譯的時候,所有 Go 代碼自然都被編譯為 object 檔案,而所有非 Go 的代碼,也可以被被其編譯器(如 C, Fortran 等)編譯為 object 檔案,而這些非 Go 代碼可以被 cgo 調用。

當程式被串連(link)的時候,這些非 Go 代碼可以選擇被編譯進最終的二進位檔案中,也可以選擇動態連結,在運行時載入。

pie - Position Independent Executables #

這是構建運行地址無關的二進位可執行檔的形式,這是一種安全特性,可以在支援 PIE 的作業系統中,讓可執行檔在載入時,每次的地址都是不同的。避免已知地址的跳躍式的攻擊。

這種方式和 exe 基本一樣,將來可能會成為預設。

c-archive C 的靜態連結庫 #

從這裡開始,和前面構建可執行檔不同了。這裡構建的是供 C 程式調用的庫。更準確一些的說,這裡是把 Go 程式構建為 archive (.a) 檔案,這樣 C 類的程式可以靜態連結 .a 檔案,並調用其中代碼。

  • hello.go
1234567891011
package mainimport "fmt"import "C"func main() {}//export Hellofunc Hello() {fmt.Println("Hello, world.")}

注意這裡的 //export Hello,這是約定,所有需要匯出給 C 調用的函數,必須通過注釋添加這個構建資訊,否則不會構建產生 C 所需的標頭檔。

然後我們構建這個 hello.go 檔案:

1
go build -buildmode=c-archive hello.go

構建後,會產生兩個檔案,一個是靜態庫檔案 hello.a,另一個則是 C 的標頭檔 hello.h

12
hello.a:  current ar archive random libraryhello.h:  c program text, ASCII text

在所產生的 hello.h 的標頭檔中,我們可以看到 Go 的 Hello() 函數的定義:

12345678910
#ifdef __cplusplusextern "C" {#endifextern void Hello();#ifdef __cplusplus}#endif

然後我們可以在 hello.c 中引用標頭檔,並使用 Go 編譯的靜態庫:

123456
#include "hello.h"int main(void) {  Hello();  return 0;}

然後,構建 C 程式:

1
cc hello.a hello.c -o hello

最後執行:

12
$ ./helloHello, world.

c-shared C 的動態連結程式庫 #

和前一個例子不同的地方是,這將用 Go 代碼建立一個動態連結程式庫(Unix: .so/Windows .dll),然後用 C 語言程式動態載入運行。

Go 和 C 語言的代碼和上面是一樣的,但是構建過程不同:

1
go build -buildmode=c-shared -o hello.so hello.go

這裡我們使用了 -buildmode=c-shared,以構建 C 所支援的動態連結程式庫。

註:需要注意的是,這裡明確指定了 -o hello.so,這裡我和演講者不同,如果不指定輸出檔案名,那麼預設會使用 hello 作為檔案名稱,導致後續的操作找不到 hello.so 檔案。

這次也產生了兩個檔案,一個是 hello.so,一個是 hello.h

12
hello.h:  c program text, ASCII texthello.so: Mach-O 64-bit dynamically linked shared library x86_64

然後,編譯對應的 C 程式:

1
cc hello.c hello.so -o hello

如果對比 c-archive 例子和 c-shared 例子中的 hello 二進位可執行檔的大小,就會發現 c-shared 的例子的 hello 要小很多:

12345
# c-archive-rwxr-xr-x  1 taowang  staff   1.5M  3 Oct 17:51 hello# c-shared-rwxr-xr-x  1 taowang  staff   8.2K  3 Oct 19:17 hello

這是因為前者,將 Go 的代碼靜態編譯進了 C 的程式中;而後者,則是動態連結,C 的可執行檔內不包含我們寫的 Go 的代碼,所有這部分函數都在動態連結程式庫 hello.so 中。

1
-rw-r--r--  1 taowang  staff   2.2M  3 Oct 19:17 hello.so

因此,執行的時候,我們除了需要 hello 這個二進位可執行檔外,我們還需要 hello.so 這個動態連結程式庫。如果預設的 LD_LIBRARY_PATH 包含了目前的目錄,並且 hello.so 就在目前的目錄,那麼可以直接:

12
$ ./helloHello, world.

否則,如果提示找不到 hello.so,如:

1
dyld: Library not loaded: hello.so

那可以手動指定 LD_LIBRARY_PATH 變數,告訴作業系統到哪裡去尋找動態連結程式庫:

1234567
# On Linux$ LD_LIBRARY_PATH=. ./helloHello, world.# On macOS$ DYLD_LIBRARY_PATH=. ./helloHello, world.

為什麼會需要動態連結? #

從開始使用 Go 我們就反反覆複的聽到人說 Go 的靜態連結如何方便,既然如此,那麼我們為什麼需要動態連結?

因為動態連結可以在運行時需要的時候,由程式決定載入,也可以在不需要的時候卸載,這樣可以節約記憶體資源。

123456789101112131415
#include <dlfcn.h>#include <stdio.h>int main(void) {  void* lib = dlopen("hello", 0);  void (*fn)() = dlsym(lib, "Hello");  if (!fn) {    fprintf(stderr, "no fn: %s\n", dlerror());    return 1;  }  //  Calls Hello();  fn();  return 0;}

這裡我們使用 dlopen() 來載入庫,然後用 dlsym() 來載入符號(函數)到一個函數指標,然後我們調用該函數指標 fn()

shared Go 的動態連結程式庫 #

shared 模式和 c-shared 有些相似,都是構建一個動態連結程式庫,以便在運行時載入。所不同的是 shared 並非構建 C 語言的動態連結程式庫,而是專門為 Go 可執行檔構建動態連結程式庫。

macOS 下目前不支援 shared 模式。

這次還是 hello.go,不過稍有不同。

1234567
package mainimport "fmt"func main() {fmt.Println("Hello, World!")}

這裡就是獨立的一個檔案,一個 main(),執行後列印 Hello, World。我們可以像以前一樣用 exe 模式構建,然後執行。不過這次我們用一種不同的方式構建。

12
go install -buildmode=shared stdgo build -linkshared hello.go

這裡我們首先把 Go 標準庫 std 構建並安裝到 $GOPATH/pkg 下,然後使用 -linkshared 來構建 hello.go

執行結果和前面一樣,但是如果仔細觀察產生的檔案,就會發現和前面很不同。

12
$ ls -l hello-rwxr-xr-x 1 root root 16032 Oct  3 13:27 hello

可以看到這個 Hello World 程式只有十幾KB大小。對於 C 程式員來說,這沒啥驚訝的,因為就應該這麼大啊。但是對於 Go 程式員來說,這就是很奇怪了,因為一般不都得 7~8MB 嗎?

其原因就是使用了動態連結程式庫,所有標準庫部分,都用動態連結的辦法來調用,構建的二進位可執行檔中只包含了程式部分。C 程式構建的 Hello World 之所以小,也是因為動態連結的原因。

如果我們查閱程式所調用的庫就可以看到具體情況:

1234567
$ ldd hello        linux-vdso.so.1 (0x00007ffed3d4e000)        libstd.so => /usr/local/go/pkg/linux_amd64_dynlink/libstd.so (0x00007f608c409000)        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f608c06a000)        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f608be66000)        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f608bc49000)        /lib64/ld-linux-x86-64.so.2 (0x00007f608e866000)

如果我們進一步去查看 libstd.so,就會看到一個巨大的動態連結程式庫,這就是 Go 的標準庫:

1
-rw-r--r-- 1 root root 37M Oct  3 13:27 /usr/local/go/pkg/linux_amd64_dynlink/libstd.so

當然,要使用這個模式需要很多準備工作,所有的動態連結程式庫都需要在指定的位置,版本都必須相容等等,所以我們一般不常用這個模式。

plugin Go 的外掛程式 #

外掛程式形式和 c-sharedshared 相似,都是構建一個動態連結程式庫,和 shared 一樣,這是構建一個 Go 專用的動態連結程式庫,而和 shared 不同的是,動態連結程式庫並非在程式啟動時載入,而是由程式內決定何時載入和釋放。

這是個新的東西,所以意味著可能不能用?……,當然如果用的對的話,應該還可以用。

我們建立一個 plugin,myplugin.go

1234567
package mainimport "fmt"func Hello() {  fmt.Println("Hello, World!")}

可以看到,這和最初那個靜態連結庫的性質相似。不過不同的是,這裡既沒有 import "C",也沒有 //export Hello,而且也沒有 func main()。因為這裡不需要,我們是 Go 調用 Go 的代碼,因此很多東西都省了。

調用代碼這麼寫:

123456789101112131415161718
package mainimport "plugin"func main() {//載入 myplugin 庫p, err := plugin.Open("myplugin.so")if err != nil {log.Fatal(err)}//取得 Hello 函數fn, err := p.Lookup("Hello")if err != nil {log.Fatal(err)}//調用函數fn.(func())()}

可以看到,這個邏輯上,和 hello-dyn.c 很相似。plugin.Open() 有點兒像 dlopen();而 p.Lookup() 有點兒像 dlsym()。實際上也是如此,底層實現的時候就是調用的這兩個函數。

注意這裡的 fn.(func())()p.Lookup() 返回的是一個 interface{},因此這裡需要轉型為具體函數類型。

用下面的命令構建:

12
go build -buildmode=plugin myplugin.gogo build runplugin.go

前者會產生一個 myplugin.so,後者會產生調用者 runplugin

12
-rw-r--r-- 1 root root 3.8M Oct  3 13:58 myplugin.so-rwxr-xr-x 1 root root 3.5M Oct  3 13:58 runplugin

優缺點 #

  • exe (靜態編譯)
    • Pros:
      • 全部整合,不需要任何依賴
      • 非常適合超小型的容器環境
      • 很容易跨不同 Linux 發行版
  • exe (動態連結 libc)
    • Pros:
      • 可以利用系統功能,比如 DNS 查詢。
      • 可以通過 libc 直接使用系統配置。
    • Cons:
      • 依賴使用者空間的執行環境
  • exe (動態連結 libc 和非 Go 代碼)
    • Pros:
      • 可以直接在 Go 程式中使用非 Go 的代碼
      • 方便和老的系統整合
    • Cons:
      • 構建變得更加複雜
      • C 不是 Go
      • 更容易出問題。
        • 所有 Go 可能出問題地方
        • 所有 C 可能出問題的地方
        • 所有 Go <-> C 之間通訊可能出問題的地方
  • pie 地址無關可執行檔(安全特性)
    • Pros:
      • exe 一樣
      • 讓系統更難攻擊
    • Cons:
      • 二進位會更大一些(bug, will be fixed)
      • 大約會有 ~1% 的效能損失
  • c-archive C 的靜態連結庫
    • Pros:
      • 可以讓 Go 整合到現有的 C 程式中
      • 事實上,這就是 Go 在 iOS 上的工作方式
      • 非常適用於已存在的非 Go 環境的構建
    • Cons:
      • 跨語言調用會比較麻煩
  • c-shared C 的動態連結程式庫
    • Pros:
      • 比較方便 Go 整合進現有的 C 程式中
      • 可以在運行時載入
      • 這是目前 Go 在 Android 下的工作方式(Java 的 System.load()
    • Cons:
      • 跨語言調用會比較麻煩
      • 想想 Android 的環境,可能出問題的面積更大了:
        • Go 可能出問題的地方
        • C 可能出問題的地方
        • Java 可能出問題的地方
        • 所有它們之間通訊可能出問題的地方……?
  • shared Go 的動態連結程式庫
    • Pros:
      • 多個可執行檔可以共用動態連結程式庫,可以降低系統總的體積。
      • 一般作業系統廠商會比較青睞於這種方式,可以讓整個系統的體積降低。
        • 事實上,這就是 Canonical(Ubuntu) 力推實現的方式
      • 可以降低系統體積,不過現在儲存空間一般不是問題
      • 可以降低記憶體,可以在記憶體中共用動態連結程式庫代碼(如果動態連結程式庫的 loader 足夠聰明的話)
    • Cons:
      • 依賴管理、以及發布是非常難得
        • 不過一般作業系統廠商已經有成熟的發布系統了
  • plugin Go 的外掛程式
    • Pros:
      • 在運行時 Go 程式載入其它 Go 程式
      • 對於複雜應用來說,允許不同部分在不同時間構建
    • Cons:
      • 構建比較複雜,部署也會很複雜
      • 如果問演講者是否該用 plugin 模式,答案一般是 No

未來 #

還有很多地方需要改進。

  • c-shared:目前不支援 Windows(可能),macOS(部分支援)
  • shared:目前不支援 macOS
  • plugin:目前不支援 macOS、Windows
  • plugin:或許可以將 runtime 從外掛程式中移除以獲得更小的可執行檔

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.