精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Go編譯的幾個細節,連專家也要停下來想想

開發 前端
因為CGO_ENABLED默認值為1,即開啟Cgo。也有些人會說:“其實Go編譯器默認是靜態鏈接的,只有在使用C語言庫時才會動態鏈接”。那么到底哪個是正確的呢?

在Go開發中,編譯相關的問題看似簡單,但實則蘊含許多細節。有時,即使是Go專家也需要停下來,花時間思考答案或親自驗證。本文將通過幾個具體問題,和大家一起探討Go編譯過程中的一些你可能之前未曾關注的細節。

注:本文示例使用的環境為Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。

1. Go編譯默認采用靜態鏈接還是動態鏈接?

我們來看第一個問題:Go編譯默認采用靜態鏈接還是動態鏈接呢?

很多人脫口而出:動態鏈接[3],因為CGO_ENABLED默認值為1,即開啟Cgo。也有些人會說:“其實Go編譯器默認是靜態鏈接的,只有在使用C語言庫時才會動態鏈接”。那么到底哪個是正確的呢?

我們來看一個具體的示例。但在這之前,我們要承認一個事實,那就是CGO_ENABLED默認值為1,你可以通過下面命令來驗證這一點:

$go env|grep CGO_ENABLED
CGO_ENABLED='1'

驗證Go默認究竟是哪種鏈接,我們寫一個hello, world的Go程序即可:

// go-compilation/main.go

package main

import "fmt"

func main() {
 fmt.Println("hello, world")
}

構建該程序:

$go build -o helloworld-default main.go

之后,我們查看一下生成的可執行文件helloworld-default的文件屬性:

$file helloworld-default
helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-default
   不是動態可執行文件

我們看到,雖然CGO_ENABLED=1,但默認情況下,Go構建出的helloworld程序是靜態鏈接的(statically linked)。

那么默認情況下,Go編譯器是否都會采用靜態鏈接的方式來構建Go程序呢?我們給上面的main.go添加一行代碼:

// go-compilation/main-with-os-user.go

package main

import (
 "fmt"
 _ "os/user"
)

func main() {
 fmt.Println("hello, world")
}

和之前的hello, world不同的是,這段代碼多了一行包的空導入,導入的是os/user這個包。

編譯這段代碼,我們得到helloworld-with-os-user可執行文件。

$go build -o helloworld-with-os-user main-with-os-user.go

使用file和ldd檢視文件helloworld-with-os-user:

$file helloworld-with-os-user
helloworld-with-os-user: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-os-user
    linux-vdso.so.1 =>  (0x00007ffcb8fd4000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb5d6fce000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fb5d6c00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb5d71ea000)

我們看到:一行新代碼居然讓helloworld從靜態鏈接變為了動態鏈接,同時這也是如何編譯出一個hello world版的動態鏈接Go程序的答案。

通過nm命令我們還可以查看Go程序依賴了哪些C庫的符號:

$nm -a helloworld-with-os-user |grep " U "
                 U abort
                 U __errno_location
                 U fprintf
                 U fputc
                 U free
                 U fwrite
                 U malloc
                 U mmap
                 U munmap
                 U nanosleep
                 U pthread_attr_destroy
                 U pthread_attr_getstack
                 U pthread_attr_getstacksize
                 U pthread_attr_init
                 U pthread_cond_broadcast
                 U pthread_cond_wait
                 U pthread_create
                 U pthread_detach
                 U pthread_getattr_np
                 U pthread_key_create
                 U pthread_mutex_lock
                 U pthread_mutex_unlock
                 U pthread_self
                 U pthread_setspecific
                 U pthread_sigmask
                 U setenv
                 U sigaction
                 U sigaddset
                 U sigemptyset
                 U sigfillset
                 U sigismember
                 U stderr
                 U strerror
                 U unsetenv
                 U vfprintf

由此,我們可以得到一個結論,在默認情況下(CGO_ENABLED=1),Go會盡力使用靜態鏈接的方式,但在某些情況下,會采用動態鏈接。那么究竟在哪些情況下會默認生成動態鏈接的程序呢?我們繼續往下看。

2. 在何種情況下默認會生成動態鏈接的Go程序?

在以下幾種情況下,Go編譯器會默認(CGO_ENABLED=1)生成動態鏈接的可執行文件,我們逐一來看一下。

2.1 一些使用C實現的標準庫包

根據上述示例,我們可以看到,在某些情況下,即使只依賴標準庫,Go 仍會在CGO_ENABLED=1的情況下采用動態鏈接。這是因為代碼依賴的標準庫包使用了C版本的實現。雖然這種情況并不常見,但os/user包[4]和net包[5]是兩個典型的例子。

os/user包的示例在前面我們已經見識過了。user包允許開發者通過名稱或ID查找用戶賬戶。對于大多數Unix系統(包括linux),該包內部有兩種版本的實現,用于解析用戶和組ID到名稱,并列出附加組ID。一種是用純Go編寫,解析/etc/passwd和/etc/group文件。另一種是基于cgo的,依賴于標準C庫(libc)中的例程,如getpwuid_r、getgrnam_r和getgrouplist。當cgo可用(CGO_ENABLED=1),并且特定平臺的libc實現了所需的例程時,將使用基于cgo的(libc支持的)代碼,即采用動態鏈接方式。

同樣,net包在名稱解析(Name Resolution,即域名或主機名對應IP查找)上針對大多數Unix系統也有兩個版本的實現:一個是純Go版本,另一個是基于C的版本。C版本會在cgo可用且特定平臺實現了相關C函數(比如getaddrinfo和getnameinfo等)時使用。

下面是一個簡單的使用net包并采用動態鏈接的示例:

// go-compilation/main-with-net.go

package main

import (
 "fmt"
 _ "net"
)

func main() {
 fmt.Println("hello, world")
}

編譯后,我們查看一下文件屬性:

$go build -o helloworld-with-net main-with-net.go 

$file helloworld-with-net 
helloworld-with-net: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-net 
 linux-vdso.so.1 =>  (0x00007ffd75dfd000)
 libresolv.so.2 => /lib64/libresolv.so.2 (0x00007fdda2cf9000)
 libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fdda2add000)
 libc.so.6 => /lib64/libc.so.6 (0x00007fdda270f000)
 /lib64/ld-linux-x86-64.so.2 (0x00007fdda2f13000)

我們看到C版本實現依賴了libresolv.so這個用于名稱解析的C庫。

由此可得,當Go在默認cgo開啟時,一旦依賴了標準庫中擁有C版本實現的包,比如os/user、net等,Go編譯器會采用動態鏈接的方式編譯Go可執行程序。

2.2 顯式使用cgo調用外部C程序

如果使用cgo與外部C代碼交互,那么生成的可執行文件必然會包含動態鏈接。下面我們來看一個調用cgo的簡單示例。

首先,建立一個簡單的C lib:

// go-compilation/my-c-lib

$tree my-c-lib
my-c-lib
├── Makefile
├── mylib.c
└── mylib.h

// go-compilation/my-c-lib/Makefile

.PHONY:  all static

all:
        gcc -c -fPIC -o mylib.o mylib.c
        gcc -shared -o libmylib.so mylib.o
static:
        gcc -c -fPIC -o mylib.o mylib.c
        ar rcs libmylib.a mylib.o

// go-compilation/my-c-lib/mylib.h

#ifndef MYLIB_H
#define MYLIB_H

void hello();
int add(int a, int b);

#endif // MYLIB_H


// go-compilation/my-c-lib/mylib.c

#include <stdio.h>

void hello() {
    printf("Hello from C!\n");
}

int add(int a, int b) {
    return a + b;
}

執行make all構建出動態鏈接庫libmylib.so!接下來,我們編寫一個Go程序通過cgo調用libmylib.so中:

// go-compilation/main-with-call-myclib.go 

package main

/*
#cgo CFLAGS: -I ./my-c-lib
#cgo LDFLAGS: -L ./my-c-lib -lmylib
#include "mylib.h"
*/
import "C"
import "fmt"

func main() {
 // 調用 C 函數
 C.hello()

 // 調用 C 中的加法函數
 result := C.add(3, 4)
 fmt.Printf("Result of addition: %d\n", result)
}

編譯該源碼:

$go build -o helloworld-with-call-myclib main-with-call-myclib.go

通過ldd可以看到,可執行文件helloworld-with-call-myclib是動態鏈接的,并依賴libmylib.so:

$ldd helloworld-with-call-myclib
 linux-vdso.so.1 =>  (0x00007ffcc39d8000)
 libmylib.so => not found
 libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f7166df5000)
 libc.so.6 => /lib64/libc.so.6 (0x00007f7166a27000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f7167011000)

設置LD_LIBRARY_PATH(為了讓程序找到libmylib.so)并運行可執行文件helloworld-with-call-myclib:

$ LD_LIBRARY_PATH=./my-c-lib:$LD_LIBRARY_PATH ./helloworld-with-call-myclib
Hello from C!
Result of addition: 7

2.3 使用了依賴cgo的第三方包

在日常開發中,我們經常依賴一些第三方包,有些時候這些第三方包依賴cgo,比如mattn/go-sqlite3[6]。下面就是一個依賴go-sqlite3包的示例:

// go-compilation/go-sqlite3/main.go
package main

import (
 "database/sql"
 "fmt"
 "log"

 _ "github.com/mattn/go-sqlite3"
)

func main() {
 // 打開數據庫(如果不存在,則創建)
 db, err := sql.Open("sqlite3", "./test.db")
 if err != nil {
  log.Fatal(err)
 }
 defer db.Close()

 // 創建表
 sqlStmt := `CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);`
 _, err = db.Exec(sqlStmt)
 if err != nil {
  log.Fatalf("%q: %s\n", err, sqlStmt)
 }

 // 插入數據
 _, err = db.Exec(`INSERT INTO user (name) VALUES (?)`, "Alice")
 if err != nil {
  log.Fatal(err)
 }

 // 查詢數據
 rows, err := db.Query(`SELECT id, name FROM user;`)
 if err != nil {
  log.Fatal(err)
 }
 defer rows.Close()

 for rows.Next() {
  var id int
  var name string
  err = rows.Scan(&id, &name)
  if err != nil {
   log.Fatal(err)
  }
  fmt.Printf("%d: %s\n", id, name)
 }

 // 檢查查詢中的錯誤
 if err = rows.Err(); err != nil {
  log.Fatal(err)
 }
}

編譯和運行該源碼:

$go build demo
$ldd demo
    linux-vdso.so.1 =>  (0x00007ffe23d8e000)
    libdl.so.2 => /lib64/libdl.so.2 (0x00007faf0ddef000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007faf0dbd3000)
    libc.so.6 => /lib64/libc.so.6 (0x00007faf0d805000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faf0dff3000)
$./demo
1: Alice

到這里,有些讀者可能會問一個問題:如果需要在上述依賴場景中生成靜態鏈接的Go程序,該怎么做呢?接下來,我們就來看看這個問題的解決細節。

3. 如何在上述情況下實現靜態鏈接?

到這里是不是有些燒腦了啊!我們針對上一節的三種情況,分別對應來看一下靜態編譯的方案。

3.1 僅依賴標準包

在前面我們說過,之所以在使用os/user、net包時會在默認情況下采用動態鏈接,是因為Go使用了這兩個包對應功能的C版實現,如果要做靜態編譯,讓Go編譯器選擇它們的純Go版實現即可。那我們僅需要關閉CGO即可,以依賴標準庫os/user為例:

$CGO_ENABLED=0 go build -o helloworld-with-os-user-static main-with-os-user.go
$file helloworld-with-os-user-static
helloworld-with-os-user-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-with-os-user-static
    不是動態可執行文件

3.2 使用cgo調用外部c程序(靜態鏈接)

對于依賴cgo調用外部c的程序,我們要使用靜態鏈接就必須要求外部c庫提供靜態庫,因此,我們需要my-c-lib提供一份libmylib.a,這通過下面命令可以實現(或執行make static):

$gcc -c -fPIC -o mylib.o mylib.c
$ar rcs libmylib.a mylib.o

有了libmylib.a后,我們還要讓Go程序靜態鏈接該.a文件,于是我們需要修改一下Go源碼中cgo鏈接的flag,加上靜態鏈接的選項:

// go-compilation/main-with-call-myclib-static.go
... ...
#cgo LDFLAGS: -static -L my-c-lib -lmylib
... ...

編譯鏈接并查看一下文件屬性:

$go build -o helloworld-with-call-myclib-static main-with-call-myclib-static.go

$file helloworld-with-call-myclib-static
helloworld-with-call-myclib-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=b3da3ed817d0d04230460069b048cab5f5bfc3b9, not stripped

我們得到了預期的結果!

3.3 依賴使用cgo的外部go包(靜態鏈接)

最麻煩的是這類情況,要想實現靜態鏈接,我們需要找出外部go依賴的所有c庫的.a文件(靜態共享庫)。以我們的go-sqlite3示例為例,go-sqlite3是sqlite庫的go binding,它依賴sqlite庫,同時所有第三方c庫都依賴libc,我們還要準備一份libc的.a文件,下面我們就先安裝這些:

$yum install -y gcc glibc-static sqlite-devel 
... ...

已安裝:
  sqlite-devel.x86_64 0:3.7.17-8.el7_7.1                                                                                          

更新完畢:
  glibc-static.x86_64 0:2.17-326.el7_9.3

接下來,我們就來以靜態鏈接的方式在go-compilation/go-sqlite3-static下編譯一下:

$go build -tags 'sqlite_omit_load_extension' -ldflags '-linkmode external -extldflags "-static"' demo

$file ./demo
./demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c779f5c3eaa945d916de059b56d94c23974ce61c, not stripped

這里命令行中的-tags 'sqlite_omit_load_extension'用于禁用SQLite3的動態加載功能,確保更好的靜態鏈接兼容性。而-ldflags '-linkmode external -extldflags "-static"'的含義是使用外部鏈接器(比如gcc linker),并強制靜態鏈接所有庫。

我們再看完略燒腦的幾個細節后,再來看一個略輕松的話題。

4. Go編譯出的可執行文件過大,能優化嗎?

Go編譯出的二進制文件一般較大,一個簡單的“Hello World”程序通常在2MB左右:

$ls -lh helloworld-default
-rwxr-xr-x 1 root root 2.1M 11月  3 10:39 helloworld-default

這一方面是因為Go將整個runtime都編譯到可執行文件中了,另一方面也是因為Go靜態編譯所致。那么在默認情況下,Go二進制文件的大小還有優化空間么?方法不多,有兩種可以嘗試:

  • 去除符號表和調試信息

在編譯時使用-ldflags="-s -w"標志可以去除符號表和調試符號,其中-s用于去掉符號表和調試信息,-w用于去掉DWARF調試信息,這樣能顯著減小文件體積。以helloworld為例,可執行文件的size減少了近四成:

$go build -ldflags="-s -w" -o helloworld-default-nosym main.go
$ls -l
-rwxr-xr-x 1 root root 2124504 11月  3 10:39 helloworld-default
-rwxr-xr-x 1 root root 1384600 11月  3 13:34 helloworld-default-nosym
  • 使用tinygo

TinyGo[7]是一個Go語言的編譯器,它專為資源受限的環境而設計,例如微控制器、WebAssembly和其他嵌入式設備。TinyGo的目標是提供一個輕量級的、能在小型設備上運行的Go運行時,同時盡可能支持Go語言的特性。tinygo的一大優點就是生成的二進制文件通常比標準Go編譯器生成的文件小得多:

$tinygo build -o helloworld-tinygo main.go
$ls -l
總用量 2728
-rwxr-xr-x  1 root root 2128909 11月  5 05:43 helloworld-default*
-rwxr-xr-x  1 root root  647600 11月  5 05:45 helloworld-tinygo*

我們看到:tinygo生成的可執行文件的size僅是原來的30%。

注:雖然TinyGo在特定場景(如IoT和嵌入式開發)中非常有用,但在常規服務器環境中,由于生態系統兼容性、性能、調試支持等方面的限制,可能并不是最佳選擇。對于需要高并發、復雜功能和良好調試支持的應用,標準Go仍然是更合適的選擇。

注:這里使用的tinygo為0.34.0版本。

5. 未使用的符號是否會被編譯到Go二進制文件中?

到這里,相信讀者心中也都會縈繞一些問題:到底哪些符號被編譯到最終的Go二進制文件中了呢?未使用的符號是否會被編譯到Go二進制文件中嗎?在這一小節中,我們就來探索一下。

出于對Go的了解,我們已經知道無論是GOPATH時代,還是Go module時代,Go的編譯單元始終是包(package),一個包(無論包中包含多少個Go源文件)都會作為一個編譯單元被編譯為一個目標文件(.a),然后Go鏈接器會將多個目標文件鏈接在一起生成可執行文件,因此如果一個包被依賴,那么它就會進入到Go二進制文件中,它內部的符號也會進入到Go二進制文件中。

那么問題來了!是否被依賴包中的所有符號都會被放到最終的可執行文件中呢?我們以最簡單的helloworld-default為例,它依賴fmt包,并調用了fmt包的Println函數,我們看看Println這個符號是否會出現在最終的可執行文件中:

$nm -a helloworld-default | grep "Println"
000000000048eba0 T fmt.(*pp).doPrintln

居然沒有!我們初步懷疑是inline優化在作祟。接下來,關閉優化再來試試:

$go build -o helloworld-default-noinline -gcflags='-l -N' main.go

$nm -a helloworld-default-noinline | grep "Println"
000000000048ec00 T fmt.(*pp).doPrintln
0000000000489ee0 T fmt.Println

看來的確如此!不過當使用"fmt."去過濾helloworld-default-noinline的所有符號時,我們發現fmt包的一些常見的符號并未包含在其中,比如Printf、Fprintf、Scanf等。

這是因為Go編譯器的一個重要特性:死碼消除(dead code elimination),即編譯器會將未使用的代碼和數據從最終的二進制文件中剔除。

我們再來繼續探討一個衍生問題:如果Go源碼使用空導入方式導入了一個包,那么這個包是否會被編譯到Go二進制文件中呢?其實道理是一樣的,如果用到了里面的符號,就會存在,否則不會。

以空導入os/user為例,即便在CGO_ENABLED=0的情況下,因為沒有使用os/user中的任何符號,在最終的二進制文件中也不會包含user包:

$CGO_ENABLED=0 go build -o helloworld-with-os-user-noinline -gcflags='-l -N' main-with-os-user.go
[root@iZ2ze18rmx2avqb5xgb4omZ helloworld]# nm -a helloworld-with-os-user-noinline |grep user
0000000000551ac0 B runtime.userArenaState

但是如果是帶有init函數的包,且init函數中調用了同包其他符號的情況呢?我們以expvar包為例看一下:

// go-compilation/main-with-expvar.go

package main

import (
 _ "expvar"
 "fmt"
)

func main() {
 fmt.Println("hello, world")
}

編譯并查看一下其中的符號:

$go build -o helloworld-with-expvar-noinline -gcflags='-l -N' main-with-expvar.go
$nm -a helloworld-with-expvar-noinline|grep expvar
0000000000556480 T expvar.appendJSONQuote
00000000005562e0 T expvar.cmdline
00000000005561c0 T expvar.expvarHandler
00000000005568e0 T expvar.(*Func).String
0000000000555ee0 T expvar.Func.String
00000000005563a0 T expvar.init.0
00000000006e0560 D expvar..inittask
0000000000704550 d expvar..interfaceSwitch.0
... ...

除此之外,如果一個包即便沒有init函數,但有需要初始化的全局變量,比如crypto包的hashes:

// $GOROOT/src/crypto/crypto.go
var hashes = make([]func() hash.Hash, maxHash)

crypto包的相關如何也會進入最終的可執行文件中,大家自己動手不妨試試。下面是我得到的一些輸出:

$go build -o helloworld-with-crypto-noinline -gcflags='-l -N' main-with-crypto.go
$nm -a helloworld-with-crypto-noinline|grep crypto
00000000005517b0 B crypto.hashes
000000000048ee60 T crypto.init
0000000000547280 D crypto..inittask

有人會問:os/user包也有一些全局變量啊,為什么這些符號沒有被包含在可執行文件中呢?比如:

// $GOROOT/src/os/user/user.go
var (
    userImplemented      = true
    groupImplemented     = true
    groupListImplemented = true
)

這就要涉及Go包初始化的邏輯了。我們看到crypto包包含在可執行文件中的符號中有crypto.init和crypto..inittask這兩個符號,顯然這不是crypto包代碼中的符號,而是Go編譯器為crypto包自動生成的init函數和inittask結構。

Go編譯器會為每個包生成一個init函數,即使包中沒有顯式定義init函數,同時每個包都會有一個inittask結構[8],用于運行時的包初始化系統。當然這么說也不足夠精確,如果一個包沒有init函數、需要初始化的全局變量或其他需要運行時初始化的內容,則編譯器不會為其生成init函數和inittask。比如上面的os/user包。

os/user包確實有上述全局變量的定義,但是這些變量是在編譯期就可以確定值的常量布爾值,而且未被包外引用或在包內用于影響控制流。Go編譯器足夠智能,能夠判斷出這些初始化是"無副作用的",不需要在運行時進行初始化。只有真正需要運行時初始化的包才會生成init和inittask。這也解釋了為什么空導入os/user包時沒有相關的init和inittask符號,而crypto、expvar包有的init.0和inittask符號。

6. 如何快速判斷Go項目是否依賴cgo?

在使用開源Go項目時,我們經常會遇到項目文檔中沒有明確說明是否依賴Cgo的情況。這種情況下,如果我們需要在特定環境(比如CGO_ENABLED=0)下使用該項目,就需要事先判斷項目是否依賴Cgo,有些時候還要快速地給出判斷。

那究竟是否可以做到這種快速判斷呢?我們先來看看一些常見的作法。

第一類作法是源碼層面的靜態分析。最直接的方式是檢查源碼中是否存在import "C"語句,這種引入方式是CGO使用的顯著標志。

// 在項目根目錄中執行
$grep -rn 'import "C"' .

這個命令會遞歸搜索當前目錄下所有文件,顯示包含import "C"的行號和文件路徑,幫助快速定位CGO的使用位置。

此外,CGO項目通常包含特殊的編譯指令,這些指令以注釋形式出現在源碼中,比如前面見識過的#cgo CFLAGS、#cgo LDFLAGS等,通過對這些編譯指令的檢測,同樣可以來判斷項目是否依賴CGO。

不過第一類作法并不能查找出Go項目的依賴包是否依賴cgo。而找出直接依賴或間接依賴是否依賴cgo,我們需要工具幫忙,比如使用Go工具鏈提供的命令分析項目依賴:

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'

其中ImportPath是依賴包的導入路徑,而CgoFiles則是依賴中包含import "C"的Go源文件。我們以go-sqlite3那個依賴cgo的示例來驗證一下:

// cd go-compilation/go-sqlite3

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
runtime/cgo: [cgo.go]
github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go]

用空導入os/user的示例再來看一下:

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}'  main-with-os-user.go | grep -v '\[\]'
runtime/cgo: [cgo.go]
os/user: [cgo_lookup_cgo.go getgrouplist_unix.go]

我們知道os/user有純go和C版本兩個實現,因此上述判斷只能說“對了一半”,當我關閉CGO_ENABLED時,Go編譯器不會使用基于cgo的C版實現。

那是否在禁用cgo的前提下對源碼進行一次編譯便能驗證項目是否對cgo有依賴呢?這樣做顯然談不上是一種“快速”的方法,那是否有效呢?我們來對上面的go-sqlite3項目做一個測試,我們在關閉CGO_ENABLED時,編譯一下該示例:

// cd go-compilation/go-sqlite3
$ CGO_ENABLED=0 go build demo

我們看到,Go編譯器并未報錯!似乎該項目不需要cgo! 但真的是這樣嗎?我們運行一下編譯后的demo可執行文件:

$ ./demo
2024/11/03 22:10:36 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);

我們看到成功編譯出來的程序居然出現運行時錯誤,提示需要cgo!

到這里,沒有一種方法可以快速、精確的給出項目是否依賴cgo的判斷。也許判斷Go項目是否依賴CGO并沒有捷徑,需要從源碼分析、依賴檢查和構建測試等多個維度進行。

7. 小結

在本文中,我們深入探討了Go語言編譯過程中的幾個重要細節,尤其是在靜態鏈接和動態鏈接的選擇上。通過具體示例,我們了解到:

  • 默認鏈接方式:盡管CGO_ENABLED默認值為1,Go編譯器在大多數情況下會采用靜態鏈接,只有在依賴特定的C庫或標準庫包時,才會切換到動態鏈接。
  • 動態鏈接的條件:我們討論了幾種情況下Go會默認生成動態鏈接的可執行文件,包括依賴使用C實現的標準庫包、顯式使用cgo調用外部C程序,以及使用依賴cgo的第三方包。
  • 實現靜態鏈接:對于需要動態鏈接的場景,我們也提供了將其轉為靜態鏈接的解決方案,包括關閉CGO、使用靜態庫,以及處理依賴cgo的外部包的靜態鏈接問題。
  • 二進制文件優化:我們還介紹了如何通過去除符號表和使用TinyGo等方法來優化生成的Go二進制文件的大小,以滿足不同場景下的需求。
  • 符號編譯與死碼消除:最后,我們探討了未使用的符號是否會被編譯到最終的二進制文件中,并解釋了Go編譯器的死碼消除機制。

通過這些細節探討,我希望能夠幫助大家更好地理解Go編譯的復雜性,并在實際開發中做出更明智的選擇,亦能在面對Go編譯相關問題時,提供有效的解決方案。

本文涉及的源碼可以在這里[9]下載。

參考資料

[1] 本文永久鏈接: https://tonybai.com/2024/mm/dd/some-details-about-go-compilation

[2] Go 1.23.0: https://tonybai.com/2024/08/19/some-changes-in-go-1-23/

[3] 動態鏈接: https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/

[4] os/user包: https://pkg.go.dev/os/user

[5] net包: https://pkg.go.dev/net

[6] mattn/go-sqlite3: https://github.com/mattn/go-sqlite3

[7] TinyGo: https://github.com/tinygo-org/tinygo/

[8] 每個包都會有一個inittask結構: https://go.dev/src/cmd/compile/internal/pkginit/init.go

[9] 這里: https://github.com/bigwhite/experiments/tree/master/go-compilation

責任編輯:武曉燕 來源: TonyBai
相關推薦

2025-03-25 10:17:55

2017-11-22 15:11:33

Java線程停止

2019-08-21 10:15:20

vue組件前端

2025-04-15 08:20:32

Kotlinsuspend標簽

2025-11-07 17:05:13

大模型2000tokenClaude 3.5

2021-12-01 06:59:27

Go 對象內部

2015-10-13 10:33:25

游戲品質細節

2017-01-15 01:12:40

碼農簡歷專業名詞

2022-10-18 10:41:44

Flowable服務任務

2024-12-31 00:00:30

CursorAI編程

2021-09-02 07:04:44

Go 開發利器

2021-02-26 20:07:54

安全性健壯性代碼

2023-08-14 08:34:14

GolangHttp

2012-02-20 10:13:57

軟件

2010-10-27 10:39:44

求職

2019-06-06 15:49:53

多線程iOS開發操作

2022-07-18 08:08:16

Go?語言技巧

2020-12-24 06:59:02

機器學習MLAI

2021-01-19 06:16:05

前端Babel 技術熱點

2018-04-23 14:27:03

點贊
收藏

51CTO技術棧公眾號

欧美与黑人午夜性猛交久久久| 9191国产精品| 欧美日本国产精品| 真实新婚偷拍xxxxx| 我不卡伦不卡影院| 日韩女优av电影| 中文字幕无码精品亚洲35| 日韩午夜影院| 精品一区二区久久| 国内精品久久久久久久| 欧美午夜激情影院| 51精品国产| 91福利在线导航| 小泽玛利亚av在线| 搞黄视频在线观看| 成人午夜视频免费看| 国产精品视频白浆免费视频| 久久久国产精品黄毛片| 国产一区二区区别| 精品国产成人系列| av在线网址导航| 久久影院午夜精品| 亚洲欧美日韩在线不卡| 欧美一进一出视频| 亚洲黄色在线播放| 美女脱光内衣内裤视频久久网站 | 中文字幕亚洲日本| 韩国成人漫画| 亚洲成人第一页| 亚洲欧洲精品一区| 嫩草精品影院| 成人h动漫精品一区二| 国产在线a不卡| 国产污视频网站| 最新国产拍偷乱拍精品 | 青青在线视频| 亚洲国产高清aⅴ视频| 精品无码久久久久久久动漫| av网站在线免费看| 蜜桃久久av一区| 国产精品91免费在线| 九九视频免费在线观看| **女人18毛片一区二区| 一区二区三区动漫| 久久精品国产亚洲AV熟女| 国产毛片久久久| 日韩免费看网站| 污免费在线观看| 亚洲色图综合| 欧美日本一区二区在线观看| 日本va中文字幕| 精品国产免费人成网站| 精品久久久久久久久久国产| 日韩视频在线视频| 国精产品一区一区三区mba下载| 亚洲色图欧美激情| 成人免费看片视频在线观看| 免费观看久久久久| 亚洲欧美在线高清| 欧美一级特黄aaaaaa在线看片| 暖暖日本在线观看| 中文字幕色av一区二区三区| 综合网五月天| 精品欧美色视频网站在线观看| 亚洲欧美综合另类在线卡通| 免费看啪啪网站| 麻豆视频在线免费观看| 亚洲天堂网中文字| 777久久精品一区二区三区无码| 69成人在线| 亚洲午夜精品网| 黄色大片在线免费看| 日韩伦理福利| 欧美一a一片一级一片| 亚洲免费成人在线视频| 国产剧情一区二区在线观看| 日韩精品中文字幕在线不卡尤物| 极品白嫩少妇无套内谢| 亚洲精品亚洲人成在线观看| 国产亚洲精品久久久久久777| 午夜影院黄色片| 欧美国产激情| 97在线免费视频| 亚洲乱码国产乱码精品| 久久精品免费观看| 国产精品二区三区| 欧美日韩国产亚洲沙发| 国产精品国产三级国产普通话三级 | 97免费观看视频| 粉嫩av亚洲一区二区图片| 极品校花啪啪激情久久| 成人在线视频成人| 亚洲精品写真福利| 各处沟厕大尺度偷拍女厕嘘嘘| 成人h在线观看| 日韩免费高清av| 91网站免费入口| 欧美freesex交免费视频| 18久久久久久| 国产精品欧美亚洲| 久久青草国产手机看片福利盒子 | 久草视频在线看| 1区2区3区国产精品| 精品无码一区二区三区爱欲| 日韩在线短视频| 精品日产卡一卡二卡麻豆| 国产高潮呻吟久久| 欧美三级第一页| 国产精品国产亚洲伊人久久 | 国产91综合网| 日本欧洲国产一区二区| 污污视频在线| 欧美三级乱人伦电影| 日韩无码精品一区二区| 天天精品视频| 国产精品久久久久久久9999| 高潮毛片7777777毛片| 国产精品毛片久久久久久| 欧美成人高潮一二区在线看| 在线观看亚洲精品福利片| 亚洲欧美制服中文字幕| 国产精品suv一区二区69| 青娱乐精品视频| 欧美一区二区三区在线免费观看| 男女视频在线| 51久久夜色精品国产麻豆| 无码 人妻 在线 视频| 一区二区亚洲| 91传媒免费看| 老司机在线视频二区| 欧美优质美女网站| 国产综合精品在线| 免费在线日韩av| 国产偷国产偷亚洲高清97cao| 日韩免费网站| 欧美日韩中文国产| 免费在线观看a视频| 久久久久91| 久久久7777| a国产在线视频| 精品日韩在线一区| 久久久香蕉视频| 国产成人小视频| 国产精品久久国产| 日韩激情精品| 欧美成人精品一区二区| 国产麻豆精品一区| 亚洲欧美日韩综合aⅴ视频| 日韩成人av免费| 亚洲成av人电影| 91最新在线免费观看| 国产激情视频在线观看| 777a∨成人精品桃花网| 三级黄色在线观看| 激情国产一区二区| 女同性恋一区二区| 7777精品| 午夜精品久久久久久久99黑人| 欧洲成人一区二区三区| 午夜免费久久看| 91精品人妻一区二区| 久久亚洲精品伦理| 亚洲福利av| 国产日韩在线观看视频| 欧美乱妇高清无乱码| 黄色av免费观看| 欧美日韩在线视频首页| 九色porny自拍视频| 日日夜夜精品视频天天综合网| 欧美一区二区综合| 成人不卡视频| 毛片精品免费在线观看| 欧洲成人一区二区三区| 91久久线看在观草草青青| 一级特黄曰皮片视频| 国模大尺度一区二区三区| 国产精品三级一区二区| 久久综合社区| 国产精品99久久久久久久久久久久 | 自拍视频一区二区三区| xxxxxhd亚洲人hd| 日韩69视频在线观看| 日本三级视频在线播放| 欧美成人高清电影在线| 五月天婷婷久久| 国产精品不卡一区二区三区| 中文字幕一区二区三区人妻在线视频| 99精品视频免费| 性高潮久久久久久久久| 秋霞一区二区| 日本在线观看天堂男亚洲| 国产秀色在线www免费观看| 欧美精品一区二区三区在线播放| 无码人妻久久一区二区三区不卡| 亚洲天堂2014| 中文人妻一区二区三区| 激情综合亚洲精品| 免费无码国产v片在线观看| 国产电影一区二区在线观看| 久久爱av电影| 成人自拍视频| 秋霞av国产精品一区| 直接在线观看的三级网址| 日韩精品欧美国产精品忘忧草| 曰批又黄又爽免费视频| 天天综合日日夜夜精品| 亚洲欧美卡通动漫| 91免费版在线| 中文字幕人妻无码系列第三区| 亚洲欧美清纯在线制服| 三级网在线观看| 少妇精品久久久| 国产成人女人毛片视频在线| 九七影院97影院理论片久久| 97在线视频免费| www.欧美日本韩国| 亚洲人成欧美中文字幕| 高h调教冰块play男男双性文| 欧美日韩一级二级三级| 精品成人av一区二区在线播放| 亚洲同性gay激情无套| av黄色在线免费观看| a美女胸又www黄视频久久| 国产三级生活片| 日韩国产在线观看| heyzo亚洲| 狠狠色综合网| 日韩中文在线字幕| 欧美成人milf| 五月天色一区| 国产亚洲精品美女久久久久久久久久| 成人18视频| 日本一区二区三区电影免费观看 | 大荫蒂性生交片| 午夜精品毛片| 亚洲高清精品中出| 全球成人免费直播| 日韩av电影免费在线| 日本欧美高清| 久久久久久一区| 国偷自产视频一区二区久| 97人人模人人爽人人喊38tv| 国产精品99久久免费| 成人在线播放av| 巨胸喷奶水www久久久免费动漫| 亲爱的老师9免费观看全集电视剧| 黄色的视频在线观看| 久久久久一本一区二区青青蜜月| av网址在线看| 久久久精品欧美| 激情视频在线观看| 久久在线精品视频| av超碰免费在线| 久久色免费在线视频| 国产成人高清精品| 另类少妇人与禽zozz0性伦| 久久国产精品一区| 大胆欧美人体视频| 美女尤物在线视频| 久久久久在线观看| 在线天堂资源| 国产精品视频一区二区三区四| av成人在线观看| 91精品久久久久久久久青青| 国产电影一区| αv一区二区三区| 欧美freesex8一10精品| 久久亚洲高清| 欧美精品久久久久久 | 中文字幕亚洲综合久久五月天色无吗'' | 国模精品视频一区二区| 岛国在线视频网站| 日本午夜精品理论片a级appf发布| 日韩电影免费观| 国产免费亚洲高清| 欧美不卡在线观看| 精品国产乱码久久久久久郑州公司| 四虎5151久久欧美毛片| 日韩中文一区二区三区| 久久久久久久久丰满| 妺妺窝人体色www看人体| 99热精品在线| 欧美黄色性生活| 国产精品中文欧美| 精品人妻一区二区三区香蕉| 中文字幕免费不卡在线| 欧美日韩综合一区二区| 疯狂蹂躏欧美一区二区精品| 最近中文在线观看| 日韩一区二区免费在线电影| 无码国产精品一区二区免费16| 国产亚洲精品久久久久久777| 哥也色在线视频| 51久久精品夜色国产麻豆| 欧美黄色成人| 成人欧美一区二区三区视频xxx| 婷婷精品在线观看| 中文字幕中文字幕99| 99精品视频网| 国产精品久久久久久久99| av午夜精品一区二区三区| 国产精品麻豆免费版现看视频| 亚洲国产精品一区二区www在线| 无码人妻熟妇av又粗又大| 日韩欧美色综合| 国产在线视频资源| 国内外成人免费激情在线视频| 欧美日一区二区三区| 国产专区一区二区三区| 亚洲成人av| 99草草国产熟女视频在线| 国产91精品一区二区麻豆网站| 欧美偷拍一区二区三区| 午夜国产精品影院在线观看| 中文字幕在线观看视频一区| 亚洲激情第一页| 在线三级电影| 国产日韩欧美成人| 国产精品免费99久久久| 欧美精品久久久久久久久久久| 麻豆精品国产传媒mv男同 | 中文字幕中文字幕在线一区 | 免费一级做a爰片久久毛片潮| 亚洲激情六月丁香| 中文字幕人妻一区二区三区视频| 亚洲精品国产电影| 色呦呦视频在线观看| 成人国产在线激情| 欧美在线电影| 国内外成人免费激情视频| 国产91对白在线观看九色| 亚洲熟女毛茸茸| 欧美少妇xxx| 黄网站在线观看| 亲爱的老师9免费观看全集电视剧| 91大神精品| 国产专区在线视频| 国产酒店精品激情| 97在线观看免费高| 欧美福利电影网| 日本免费在线视频| 国产免费一区二区三区香蕉精| 欧美色蜜桃97| 国产三级三级三级看三级| 91视频国产观看| 在线观看亚洲天堂| 日韩精品亚洲精品| 周于希免费高清在线观看| 久久精品99久久| 国产精品日韩久久久| 中文字幕av观看| 欧美性猛交xxxxx免费看| 老司机午夜福利视频| 午夜精品久久久久久久99热| 另类图片第一页| 欧日韩免费视频| 99久久久久免费精品国产 | 欧美日韩中字一区| 国产69精品久久app免费版| 国产精品三级网站| 99精品视频在线| 下面一进一出好爽视频| 亚洲激情图片一区| 蜜桃av鲁一鲁一鲁一鲁俄罗斯的| 国产做受高潮69| 视频一区中文| 中文字幕av专区| 亚洲视频在线观看三级| 亚洲国产福利视频| 69视频在线播放| 国产成人1区| 久久久精品高清| 亚洲精品美腿丝袜| 天天操天天干天天插| 国产成人欧美在线观看| 国产精品久久久久久麻豆一区软件| 特级西西444www| 亚洲福利视频导航| 欧美美乳在线| 国产在线久久久| 亚洲小说欧美另类社区| 麻豆精品免费视频| 欧美群妇大交群中文字幕| 在线观看电影av| 麻豆av福利av久久av| 精品一区二区三区在线视频| 精品无码人妻一区二区三区品 | 亚洲aaaaaa| 一本一本久久| 国精品人伦一区二区三区蜜桃| 日韩写真欧美这视频| 天堂电影一区| 精品日韩在线播放| 99精品偷自拍| 国产精品久久影视| 欧美性资源免费| 国产精品久久久久久麻豆一区软件| 亚洲精品无码一区二区| 欧美日韩国产大片| 精品极品在线|