原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-02-basic.html
要使用 CGO 特性,需要安裝 C/C++ 構(gòu)建工具鏈,在 macOS 和 Linux 下是要安裝 GCC,在 windows 下是需要安裝 MinGW 工具。同時需要保證環(huán)境變量 CGO_ENABLED
被設(shè)置為 1,這表示 CGO 是被啟用的狀態(tài)。在本地構(gòu)建時 CGO_ENABLED
默認是啟用的,當交叉構(gòu)建時 CGO 默認是禁止的。比如要交叉構(gòu)建 ARM 環(huán)境運行的 Go 程序,需要手工設(shè)置好 C/C++
交叉構(gòu)建的工具鏈,同時開啟 CGO_ENABLED
環(huán)境變量。然后通過 import "C"
語句啟用 CGO 特性。
如果在 Go 代碼中出現(xiàn)了 import "C"
語句則表示使用了 CGO 特性,緊跟在這行語句前面的注釋是一種特殊語法,里面包含的是正常的 C 語言代碼。當確保 CGO 啟用的情況下,還可以在當前目錄中包含 C/C++ 對應(yīng)的源文件。
舉個最簡單的例子:
package main
/*
#include <stdio.h>
void printint(int v) {
printf("printint: %d\n", v);
}
*/
import "C"
func main() {
v := 42
C.printint(C.int(v))
}
這個例子展示了 cgo 的基本使用方法。開頭的注釋中寫了要調(diào)用的 C 函數(shù)和相關(guān)的頭文件,頭文件被 include 之后里面的所有的 C 語言元素都會被加入到”C” 這個虛擬的包中。需要注意的是,import "C" 導入語句需要單獨一行,不能與其他包一同 import。向 C 函數(shù)傳遞參數(shù)也很簡單,就直接轉(zhuǎn)化成對應(yīng) C 語言類型傳遞就可以。如上例中 C.int(v)
用于將一個 Go 中的 int 類型值強制類型轉(zhuǎn)換轉(zhuǎn)化為 C 語言中的 int 類型值,然后調(diào)用
C 語言定義的 printint 函數(shù)進行打印。
需要注意的是,Go 是強類型語言,所以 cgo 中傳遞的參數(shù)類型必須與聲明的類型完全一致,而且傳遞前必須用”C” 中的轉(zhuǎn)化函數(shù)轉(zhuǎn)換成對應(yīng)的 C 類型,不能直接傳入 Go 中類型的變量。同時通過虛擬的 C 包導入的 C 語言符號并不需要是大寫字母開頭,它們不受 Go 語言的導出規(guī)則約束。
cgo 將當前包引用的 C 語言符號都放到了虛擬的 C 包中,同時當前包依賴的其它 Go 語言包內(nèi)部可能也通過 cgo 引入了相似的虛擬 C 包,但是不同的 Go 語言包引入的虛擬的 C 包之間的類型是不能通用的。這個約束對于要自己構(gòu)造一些 cgo 輔助函數(shù)時有可能會造成一點的影響。
比如我們希望在 Go 中定義一個 C 語言字符指針對應(yīng)的 CChar 類型,然后增加一個 GoString 方法返回 Go 語言字符串:
package cgo_helper
//#include <stdio.h>
import "C"
type CChar C.char
func (p *CChar) GoString() string {
return C.GoString((*C.char)(p))
}
func PrintCString(cs *C.char) {
C.puts(cs)
}
現(xiàn)在我們可能會想在其它的 Go 語言包中也使用這個輔助函數(shù):
package main
//static const char* cs = "hello";
import "C"
import "./cgo_helper"
func main() {
cgo_helper.PrintCString(C.cs)
}
這段代碼是不能正常工作的,因為當前 main 包引入的 C.cs
變量的類型是當前 main
包的 cgo 構(gòu)造的虛擬的 C 包下的 *char
類型(具體點是 *C.char
,更具體點是 *main.C.char
),它和 cgo_helper 包引入的 *C.char
類型(具體點是 *cgo_helper.C.char
)是不同的。在
Go 語言中方法是依附于類型存在的,不同 Go 包中引入的虛擬的 C 包的類型卻是不同的(main.C
不等 cgo_helper.C
),這導致從它們延伸出來的 Go 類型也是不同的類型(*main.C.char
不等 *cgo_helper.C.char
),這最終導致了前面代碼不能正常工作。
有 Go 語言使用經(jīng)驗的用戶可能會建議參數(shù)轉(zhuǎn)型后再傳入。但是這個方法似乎也是不可行的,因為 cgo_helper.PrintCString
的參數(shù)是它自身包引入的 *C.char
類型,在外部是無法直接獲取這個類型的。換言之,一個包如果在公開的接口中直接使用了 *C.char
等類似的虛擬 C 包的類型,其它的 Go 包是無法直接使用這些類型的,除非這個 Go
包同時也提供了 *C.char
類型的構(gòu)造函數(shù)。因為這些諸多因素,如果想在 go test 環(huán)境直接測試這些 cgo 導出的類型也會有相同的限制。
在 import "C"
語句前的注釋中可以通過 #cgo
語句設(shè)置編譯階段和鏈接階段的相關(guān)參數(shù)。編譯階段的參數(shù)主要用于定義相關(guān)宏和指定頭文件檢索路徑。鏈接階段的參數(shù)主要是指定庫文件檢索路徑和要鏈接的庫文件。
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"
上面的代碼中,CFLAGS 部分,-D
部分定義了宏 PNG_DEBUG,值為 1;-I
定義了頭文件包含的檢索目錄。LDFLAGS 部分,-L
指定了鏈接時庫文件檢索目錄,-l
指定了鏈接時需要鏈接 png 庫。
因為 C/C++ 遺留的問題,C 頭文件檢索目錄可以是相對目錄,但是庫文件檢索目錄則需要絕對路徑。在庫文件的檢索目錄中可以通過 ${SRCDIR}
變量表示當前包目錄的絕對路徑:
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
上面的代碼在鏈接時將被展開為:
// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo
#cgo
語句主要影響 CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS 和 LDFLAGS 幾個編譯器環(huán)境變量。LDFLAGS 用于設(shè)置鏈接時的參數(shù),除此之外的幾個變量用于改變編譯階段的構(gòu)建參數(shù) (CFLAGS 用于針對 C 語言代碼設(shè)置編譯參數(shù))。
對于在 cgo 環(huán)境混合使用 C 和 C++ 的用戶來說,可能有三種不同的編譯選項:其中 CFLAGS 對應(yīng) C 語言特有的編譯選項、CXXFLAGS 對應(yīng)是 C++ 特有的編譯選項、CPPFLAGS 則對應(yīng) C 和 C++ 共有的編譯選項。但是在鏈接階段,C 和 C++ 的鏈接選項是通用的,因此這個時候已經(jīng)不再有 C 和 C++ 語言的區(qū)別,它們的目標文件的類型是相同的。
#cgo
指令還支持條件選擇,當滿足某個操作系統(tǒng)或某個 CPU 架構(gòu)類型時后面的編譯或鏈接選項生效。比如下面是分別針對 windows 和非 windows 下平臺的編譯和鏈接選項:
// #cgo windows CFLAGS: -DX86=1
// #cgo !windows LDFLAGS: -lm
其中在 windows 平臺下,編譯前會預(yù)定義 X86 宏為 1;在非 windows 平臺下,在鏈接階段會要求鏈接 math 數(shù)學庫。這種用法對于在不同平臺下只有少數(shù)編譯選項差異的場景比較適用。
如果在不同的系統(tǒng)下 cgo 對應(yīng)著不同的 c 代碼,我們可以先使用 #cgo
指令定義不同的 C 語言的宏,然后通過宏來區(qū)分不同的代碼:
package main
/*
#cgo windows CFLAGS: -DCGO_OS_WINDOWS=1
#cgo darwin CFLAGS: -DCGO_OS_DARWIN=1
#cgo linux CFLAGS: -DCGO_OS_LINUX=1
#if defined(CGO_OS_WINDOWS)
const char* os = "windows";
#elif defined(CGO_OS_DARWIN)
const char* os = "darwin";
#elif defined(CGO_OS_LINUX)
const char* os = "linux";
#else
# error(unknown os)
#endif
*/
import "C"
func main() {
print(C.GoString(C.os))
}
這樣我們就可以用 C 語言中常用的技術(shù)來處理不同平臺之間的差異代碼。
build tag 是在 Go 或 cgo 環(huán)境下的 C/C++ 文件開頭的一種特殊的注釋。條件編譯類似于前面通過 #cgo
指令針對不同平臺定義的宏,只有在對應(yīng)平臺的宏被定義之后才會構(gòu)建對應(yīng)的代碼。但是通過 #cgo
指令定義宏有個限制,它只能是基于 Go 語言支持的 windows、darwin 和 linux 等已經(jīng)支持的操作系統(tǒng)。如果我們希望定義一個 DEBUG 標志的宏,#cgo
指令就無能為力了。而
Go 語言提供的 build tag 條件編譯特性則可以簡單做到。
比如下面的源文件只有在設(shè)置 debug 構(gòu)建標志時才會被構(gòu)建:
// +build debug
package main
var buildMode = "debug"
可以用以下命令構(gòu)建:
go build -tags="debug"
go build -tags="windows debug"
我們可以通過 -tags
命令行參數(shù)同時指定多個 build 標志,它們之間用空格分隔。
當有多個 build tag 時,我們將多個標志通過邏輯操作的規(guī)則來組合使用。比如以下的構(gòu)建標志表示只有在”linux/386“或”darwin 平臺下非 cgo 環(huán)境 “才進行構(gòu)建。
// +build linux,386 darwin,!cgo
其中 linux,386
中 linux 和 386 用逗號鏈接表示 AND 的意思;而 linux,386
和 darwin,!cgo
之間通過空白分割來表示 OR 的意思。
![]() | ![]() |
更多建議: