在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ GO/ go tool cgo
go install
go clean
go list
go test
go doc與godoc
go build
go fix與go tool fix
go tool pprof
go run
go env
go tool cgo
標準命令詳解
go get
go vet與go tool vet

go tool cgo

cgo也是一個Go語言自帶的特殊工具。一般情況下,我們使用命令go tool cgo來運行它。這個工具可以使我們創(chuàng)建能夠調用C語言代碼的Go語言源碼文件。這使得我們可以使用Go語言代碼去封裝一些C語言的代碼庫,并提供給Go語言代碼或項目使用。

在執(zhí)行go tool cgo命令的時候,我們需要加入作為目標的Go語言源碼文件的路徑。這個路徑可以是絕對路徑也可以是相對路徑。但是,作者強烈建議在目標源碼文件所屬的代碼包目錄下執(zhí)行go tool cgo命令并以目標源碼文件的名字作為參數。因為,go tool cgo命令會在當前目錄(也就是我們執(zhí)行go tool cgo命令的目錄)中生成一個名為_obj的子目錄。該目錄下會包含一些Go源碼文件和C源碼文件。這個子目錄及其包含的文件理應被保存在目標代碼包目錄之下。至于原因,我們稍后再做解釋。

我們現在來看可以作為go tool cgo命令參數的Go語言源碼文件。這個源碼文件必須要包含一行只針對于代碼包C的導入語句。其實,Go語言標準庫中并不存在代碼包C。代碼包C是一個偽造的代碼包。導入這個代碼包是為了告訴cgo工具在這個源碼文件中需要調用C代碼,同時也是給予cgo所產生的代碼一個專屬的命名空間。除此之外,我們還需要在這個代碼包導入語句之前加入一些注釋,并且在注釋行中寫出我們真正需要使用的C語言接口文件的名稱。像這樣:

// #include <stdlib.h>
import "C"

在Go語言的規(guī)范中,把在代碼包C導入語句之前的若干注釋行叫做序文(preamble)。 在引入了C語言的標準代碼庫stdlib.h之后,我們就可以在后面的源碼中調用這個庫中的接口了。像這樣:

func Random() int {
    return int(C.rand())
}

func Seed(i int) {
    C.srand(C.uint(i))
}

我們把上述的這些Go語言代碼寫入Go語言的庫源碼文件rand.go中,并將這個源碼文件保存在goc2項目的代碼包basic/cgo/lib的對應目錄中。

在Go語言源碼文件rand.go中對代碼包C有四處引用,分別是三個函數調用語句C.randC.srandC.uint,以及一個導入語句import "C"。其中,在Go語言函數Random中調用了C語言標準庫代碼中的函數rand并返回了它的結果。但是,C語言的rand函數返回的結果的類型是C語言中的int類型。在cgo工具的環(huán)境中,C語言中的int類型與C.int相對應。作為一個包裝C語言接口的函數,我們必須將代碼包C的使用限制在當前代碼包內。也就是說,我們必須對當前代碼包之外的Go代碼隱藏代碼包C。這樣做也是為了遵循代碼隔離原則。我們在設計接口或者接口適配程序的時候經常會用到這種方法。因此,rand函數的結果的類型必須是Go語言的。所以,我們在這里使用函數int對C.int類型的C語言接口的結果進行了轉換。當然,為了更清楚的表達,我們也可以將函數Random中的代碼return int(C.rand())拆分成兩行,像這樣:

var r C.int = C.rand()
return int(r)

而Go語言函數Seed則恰好相反。C語言標準代碼庫中的函數srand接收一個參數,且這個參數的類型必須為C語言的uint類型,即C.uint。而Go語言函數Seed的參數為Go語言的int類型。為此,我們需要使用代碼包C的函數unit對其進行轉換。

實際上,標準C語言的數字類型都在cgo工具中有對應的名稱,包括:C.char、C.schar(有符號字符類型)、C.uchar(無符號字符類型)、C.short、C.ushort(無符號短整數類型)、C.int、C.uint(無符號整數類型)、C.long、C.ulong(無符號長整數類型)、C.longlong(對應于C語言的類型long long,它是在C語言的C99標準中定義的新整數類型)、C.ulonglong(無符號的long long類型)、C.float和C.double。另外,C語言類型void*對應于Go語言的類型unsafe.Pointer。

如果想直接訪問C語言中的struct、union或enum類型的話,就需要在名稱前分別加入前綴struct_、union或enum。比如,我們需要在Go源碼文件中訪問C語言代碼中的名為command的struct類型的話,就需要這樣寫:C.structcommand。那么,如果我們想在Go語言代碼中訪問C語言類型struct中的字段需要怎樣做呢?解決方案是,同樣以C語言類型struct的實例名以及選擇符“.”作為前導,但需要在字段的名稱前加入下劃線“”。例如,如果command1是名為command的C語言struct類型的實例名,并且這個類型中有一個名為name的字段,那么我們在Go語言代碼中訪問這個字段的方式就是command1._name。需要注意的是,我們不能在Go的struct類型中嵌入C語言類型的字段。這與我們在前面所說的代碼隔離原則具有相同的意義。

在上面展示的庫源碼文件rand.go中有多處對C語言函數的訪問。實際上,任何C語言的函數都可以 被Go語言代碼調用。只要在源碼文件中導入了代碼包C。并且,我們還可以同時取回C語言函數的結果,以及一個作為錯誤提示信息的變量。這與我們在Go語言中同時獲取多個函數結果的方法一樣。同樣的,我們可以使用下劃線“_”直接丟棄取回的值。這在調用無結果的C語言函數時非常有用。請看下面的例子:

package cgo

/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"

func Sqrt(p float32) (float32, error) {
    n, err := C.sqrt(C.double(p))
    return float32(n), err
}

上面這段代碼被保存在了Go語言庫源碼文件math.go中,并與源碼文件rand.go在同一個代碼包目錄。在Go語言函數Sqrt中的C.sqrt是一個在C語言標準代碼庫math.h中的函數。它會返回參數的平方根。但是在第一行代碼中,我們接收由函數C.sqrt返回的兩個值。其中,第一個值即為C語言函數sqrt的結果。而第二個值就是我們上面所說的那個作為錯誤提示信息的變量。實際上,這個變量的類型是Go語言的error接口類型。它包裝了一個C語言的全局變量errno。這個全局變量被定義在了C語言代碼庫errno.h中。cgo工具在為我們生成C語言源碼文件時會默認引入兩個C語言標準代碼庫,其中一個就是errno.h。所以我們并不用在Go語言源碼文件中使用指令符#include顯式的引入這個代碼庫。cgo工具默認為我們引入的另一個是C語言標準代碼庫string.h。它包含了很多用于字符串處理和內存處理的函數。

在我們以“C.*”的形式調用C語言代碼庫時,有一點需要特別注意。在C語言中,如果一個函數的參數是一個具有固定尺寸的數組,那么實際上這個函數所需要的是指向這個數組的第一個元素的指針。C編譯器能夠正確識別和處理這個調用慣例。它可以自行獲取到這個指針并傳給函數。但是,這在我們使用cgo工具調用C語言代碼庫時是行不通的。在Go語言中,我們必須顯式的將這個指向數組的第一個元素的指針傳遞給C語言的函數,像這樣:``C.func1(&x[0])````。

另一個需要特別注意的地方是,在C語言中沒有像Go語言中獨立的字符串類型。C語言使用最后一個元素為‘\0’的字符數組來代表字符串。在Go語言的字符串和C語言的字符串之間進行轉換的時候,我們就需要用到代碼包C中的C.C.CStringC.GoStringC.GoStringN等函數。這些轉換操作是通過對字符串數據的拷貝來完成的。Go語言內存管理器并不能感知此類內存分配操作。因為它們是由C語言代碼引發(fā)的。所以,我們在使用與C.CString函數類似的會導致內存分配操作的函數之后,需要調用代碼包C的free函數以手動的釋放內存。這里有一個小技巧,即我們可以把對C.free函數的調用放到defer語句中或者放入在defer之后的匿名函數中。這樣就可以保證在退出當前函數之前釋放這些被分配的內存了。請看下面這個示例:

func Print(s string) {
        cs := C.CString(s)
        defer C.free(unsafe.Pointer(cs))
        C.myprint(cs)
}

上面這段代碼被存放在goc2p項目的代碼包basic/cgo/lib的庫源碼文件print.go中。其中的函數C.myprint是我們在該庫源碼文件的序文中自定義的。關于這種C語言函數定義方式,我們一會兒再解釋。在這段代碼中,我們首先把Go語言的字符串轉換為了C語言的字符串。注意,變量cs的值實際上是指向字符串(在C語言中,字符串由字符數組代表)中第一個字符的指針。在cgo工具對應的上下文環(huán)境中,cs變量的類型是*C.Char。然后,我們通過defer語句和C.free函數保證由C語言代碼分配的內存得以釋放。請注意子語句unsafe.Pointer(cs)。正因為cs變量在C語言中的類型是指針類型,且與之相對應的Go語言類型是unsafe.Pointer。所以,我們需要先將其轉換為Go語言可以識別的類型再作為參數傳遞給函數C.free。最后,我們將這個字符串打印到標準輸出。

再次重申,我們在使用C.CString函數將Go語言的字符串轉換為C語言字符串后,需要顯式的調用C.free函數以釋放用于數據拷貝的內存。而最佳實踐是,將在defer語句中調用C.free函數。

在前面我們已經提到過,在導入代碼包C的語句之上可以加入若干個為cgo工具而寫的若干注釋行(也被叫做序文)。并且,以#include和一個空格開始的注釋行可以用來引入一個C語言的接口文件。我們也把序文中這種形式的字符串叫做指令符。指令符#cgo的用途是為編譯器和連接器提供標記。這些標記在編譯當前源碼文件中涉及到代碼包C的那部分代碼時會被用到。

標記CFLAGSLDFLAGS``可以被放在指令符#cgo```之后,并用于定制編譯器gcc的行為。gcc(GNU Compiler Collection,GNU編譯器套裝),是一套由GNU開發(fā)的開源的編程語言編譯器。它是GNU項目的關鍵部分,也是類Unix操作系統(tǒng)(也包括Linux操作系統(tǒng))中的標準編譯器。gcc(特別是其中的C語言編譯器)也常被認為是跨平臺編譯器的事實標準。gcc原名為GNU C語言編譯器(GNU C Compiler),因為它原本只能處理C語言。不過,gcc變得可以處理更多的語言?,F在,gcc中包含了很多針對特定編程語言的編譯器。我們在本節(jié)第一小節(jié)的末尾提及的gccgo就是這款套件中針對Go語言的編譯器。標記CFLAGS可以指定用于gcc中的C編譯器的選項。它嘗嘗用于指定頭文件(.h文件)的路徑。而標記LDFLAGS則可以指定gcc編譯器會用到的一些優(yōu)化參數,也可以用來告訴鏈接器需要用到的C語言代碼庫文件的位置。

為了清晰起見,我們可以把這些標記及其值拆分成多個注釋行,并均以指令符#cgo作為前綴。另外,在指令符#cgo和標記之間,我們也可以加入一些可選的內容,即環(huán)境變量GOOS和GOARCH中的有效值。這樣,我們就可以使這些標記只在某些操作系統(tǒng)和/或某些計算架構的環(huán)境下起作用了。示例如下:

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo linux CFLAGS: -DLINUX=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"

在上面的示例中,序文由四個注釋行組成。第一行注釋的含義是預定義一個名為PNG_DEBUG的宏并將它的值設置為1。而第二行注釋的意思是,如果在Linux操作系統(tǒng)下,則預定義一個名為LINUX的宏并將它的值設置為1。第三行注釋是與鏈接器有關的。它告訴鏈接器需要用到一個庫名為png的代碼庫文件。最后,第四行注釋引入了C語言的標準代碼庫png.h。

如果我們有一些在所有場景下都會用到的CFLAGS標記或LDFLAGS標記的值,那么就可以把它們分別作為環(huán)境變量CGO_CFLAGSCGO_LDFLAGS的值。而對于需要針對某個導入了“C”的代碼包的標記值就只能連同指令符#cgo一起放入Go語言源碼文件的注釋行中了。

相信讀者對指令符#cgo#include的用法已經有所了解了。

實際上,我們幾乎可以在序文中加入任何C代碼。像這樣:

/*
#cgo LDFLAGS: -lsqlite3

#include <sqlite3.h>
#include <stdlib.h>

// These wrappers are necessary because SQLITE_TRANSIENT
// is a pointer constant, and cgo doesn't translate them correctly.
// The definition in sqlite3.h is:
//
// typedef void (*sqlite3_destructor_type)(void*);
// #define SQLITE_STATIC      ((sqlite3_destructor_type)0)
// #define SQLITE_TRANSIENT   ((sqlite3_destructor_type)-1)

static int my_bind_text(sqlite3_stmt *stmt, int n, char *p, int np) {
        return sqlite3_bind_text(stmt, n, p, np, SQLITE_TRANSIENT);
}
static int my_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) {
        return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT);
}

*/

上面這段代碼摘自開源項目gosqlite的Go語言源碼文件sqlite.go。gosqlite項目是一個開源數據SQLite的Go語言版本的驅動代碼庫。實際上,它只是把C語言版本的驅動代碼庫進行了簡單的封裝。在Go語言的世界里,這樣的封裝隨處可見,尤其是在Go語言發(fā)展早期。因為,這樣可以非常方便的重用C語言版本的客戶端程序,而大多數軟件都早已擁有這類程序了。并且,封裝C語言版本的代碼庫與從頭開發(fā)一個Go語言版本的客戶端程序相比,無論從開發(fā)效率還是運行效率上來講都會是非常迅速的?,F在讓我們看看在上面的序文中都有些什么。很顯然,在上面的序文中直接出現了兩個C語言的函數my_bind_textmy_bind_blob。至于為什么要把C語言函數直接寫到這里,在它們前面的注釋中已經予以說明。大意翻譯如下:這些包裝函數是必要的,這是因為SQLITE_TRANSIENT是一個指針常量,而cgo并不能正確的翻譯它們??吹贸鰜?,這是一種備選方案,只有在cgo不能幫我們完成工作時才會被選用。不管怎樣,在序文中定義的這兩個函數可以直接在當前的Go語言源碼文件中被使用。具體的使用方式同樣是通過“C.*”的形式來調用。比如源碼文件sqlite.go中的代碼:

rv := C.my_bind_text(s.stmt, C.int(i+1), cstr, C.int(len(str)))

rv := C.my_bind_blob(s.stmt, C.int(i+1), unsafe.Pointer(p), C.int(len(v)))

上述示例中涉及到的源碼文件可以通過這個網址訪問到。有興趣的讀者可以前往查看。

我們再來看看我們之前提到過的庫源碼文件print.go(位于goc2p項目的代碼包basic/cgo/lib之中)的序文:

/*
#include <stdio.h>
#include <stdlib.h>

void myprint(char* s) {
        printf("%s", s);
}
*/
import "C"

我們在序文中定義一個名為myprint的函數。在這個函數中調用了C語言的函數printf。自定義函數myprint充當了類似于適配器的角色。之后,我們就可以在后續(xù)的代碼中直接使用這個自定義的函數了:

C.myprint(cs)

關于在序文中嵌入C語言代碼的方法我們就介紹到這里。

現在,讓我們來使用go tool cgo命令并以rand.go作為參數生成_obj子目錄和相關源碼文件:

hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ go tool cgo rand.go 
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls
_obj  rand.go
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls _obj
_cgo_defun.c   _cgo_export.h  _cgo_gotypes.go  _cgo_.o       rand.cgo2.c
_cgo_export.c  _cgo_flags     _cgo_main.c      rand.cgo1.go

子目錄_obj中一共包含了九個文件。

其中,cgo工具會把作為參數的Go語言源碼文件rand.go轉換為四個文件。其中包括兩個Go語言源碼文件rand.cgo1.go和_cgo_gotypes.go,以及兩個C語言源碼文件_cgo_defun.c和rand.cgo2.c。

文件rand.cgo1.go用于存放cgo工具對原始源碼文件rand.go改寫后的內容。改寫具體細節(jié)包括去掉其中的代碼包C導入語句,以及替換涉及到代碼包C的語句,等等。最后,這些替換后的標識符所對應的Go語言的函數、類型或變量的定義,將會被寫入到文件_cgo_gotypes.go中。

需要說明的是,替換涉及到代碼包C的語句的具體做法是根據xxx的種類將標識符C.xxx替換為_Cfunc_xxx或者_Ctype_xxx。比如,作為參數的源碼文件rand.go中存在如下語句:

C.srand(C.uint(i))

cgo工具會把它改寫為:

_Cfunc_srand(_Ctype_uint(i))

其中,標識符C.srand被替換為_Cfunc_srand,而標識符C.uint被替換為了_Ctype_uint。并且,新的標識符_Cfunc_srand_Ctype_uint的定義將會在文件_cgo_gotypes.go中被寫明:

type _Ctype_uint uint32

type _Ctype_void [0]byte

func _Cfunc_srand(_Ctype_uint) _Ctype_void

其中,類型_Ctype_void可以表示空的參數列表或空的結果列表。

文件_cgo_defun.c中包含了相應的C語言函數的定義和實現。例如,C語言函數_Cfunc_srand的實現如下:

#pragma cgo_import_static _cgo_54716c7dc6a7_Cfunc_srand
void _cgo_54716c7dc6a7_Cfunc_srand(void*);

void
·_Cfunc_srand(struct{uint8 x[4];}p)
{
    runtime·cgocall(_cgo_54716c7dc6a7_Cfunc_srand, &p);
}

其中,十六進制數“54716c7dc6a7”是cgo工具由于作為參數的源碼文件的內容計算得出的哈希值。這個十六進制數作為了函數名稱_cgo_54716c7dc6a7_Cfunc_srand的一部分。這樣做是為了生成一個唯一的名稱以避免沖突。我們看到,在源碼文件_cgo_defun.c中只包含了函數_cgo_54716c7dc6a7_Cfunc_srand的定義。而其實現被寫入到了另一個C語言源碼文件中。這個文件就是rand.cgo2.c。函數_cgo_54716c7dc6a7_Cfunc_srand對應的實現代碼如下:

void
_cgo_f290d3e89fd1_Cfunc_srand(void *v)
{
    struct {
        unsigned int p0;
    } __attribute__((__packed__)) *a = v;
    srand(a->p0);
}

這個函數從指向函數_Cfunc_puts的參數幀中抽取參數,并調用系統(tǒng)C語言函數srand,最后將結果存儲在幀中并返回。

下面我們對在子目錄_obj中存放的其余幾個文件進行簡要說明:

  • 文件_cgo_flags用于存放CFLAGS標記和LDFLAGS標記的值。

  • 文件_cgo_main.c用于存放一些C語言函數的存根,也可以說是一些函數的空實現。函數的空實現即在函數體重沒有任何代碼(return語句除外)的實現。其中包括在源碼文件_cgo_export.c出現的聲明為外部函數的函數。另外,文件_cgo_main.c中還會有一個被用于動態(tài)鏈接處理的main函數。

  • 在文件_cgo_export.h中存放了可以暴露給C語言代碼的與Go語言類型相對應的C語言聲明語句。

  • 文件_cgo_export.c中則包含了與可以暴露給C語言代碼的Go語言函數相對應的C語言函數定義和實現代碼。

  • 文件cgo.o是gcc編譯器在編譯C語言源碼文件rand.cgo2.c、_cgo_export.c和_cgo_main.c之后生成的結果文件。

在上述的源碼文件中,文件rand.cgo1.go和_cgo_gotypes.go將會在構建代碼包時被Go官方Go語言編譯器(6g、8g或5g)編譯。文件_cgo_defun.c會在構建代碼包時被Go官方的C語言的編譯器(6c、8c或5c)編譯。而文件rand.cgo2.c、_cgo_export.c和_cgo_main.c 則會被gcc編譯器編譯。

如果我們在執(zhí)行go tool cgo命令時加入多個Go語言源碼文件作為參數,那么在當前目錄的_obj子目錄下會出現與上述參數數量相同的x.cgo1.go文件和x.cgo2.c文件。其中,x為作為參數的Go語言源碼文件主文件名。

通過上面的描述,我們基本了解了由cgo工具生成的文件的內容和用途。

與其它go命令一樣,我們在執(zhí)行go tool cgo命令的時候還可以加入一些標記。如下表。

表0-24 go tool cgo命令可接受的標記

名稱 默認值 說明
-cdefs false 將改寫后的源碼內容以C定義模式打印到標準輸出,而不生成相關的源碼文件。
-godefs false 將改寫后的源碼內容以Go定義模式打印到標準輸出,而不生成相關的源碼文件。
-objdir "" gcc編譯的目標文件所在的路徑。若未自定義則為當前目錄下的_obj子目錄。
-dynimport "" 如果值不為空字符串,則打印為其值所代表的文件生成的動態(tài)導入數據到標準輸出。
-dynlinker false 記錄在dynimport模式下的動態(tài)鏈接器信息。
-dynout "" 將-dynimport的輸出(如果有的話)寫入到其值所代表的文件中。
-gccgo false 生成可供gccgo編譯器使用的文件。
-gccgopkgpath "" 對應于gccgo編譯器的-fgo-pkgpath選項。
-gccgoprefix "" 對應于gccgo編譯器的-fgo-prefix選項。
-debug-define false 打印相關的指令符#defines及其后續(xù)內容到標準輸出。
-debug-gcc false 打印gcc調用信息到標準輸出。
-import_runtime_cgo true 在生成的代碼中加入語句“import runtime/cgo”。
-import_syscall true 在生成的代碼中加入語句“import syscall”。

在上表中,我們把標記分為了五類并在它們之間以空行分隔。

在第一類標記中,-cdefs標記和-godefs標記都可以打印相應的代碼到標準輸出,并且使cgo工具不生成相應的源碼文件。cgo工具在獲取目標源碼文件內容之后會改寫其中的內容,包括去掉代碼包C的導入語句,以及對代碼包C的調用語句中屬于代碼包C的類型、函數和變量進行等價替換。如果我們加入了標記-cdefs-godefs,那么cgo工具隨后就會把改寫后的目標源碼打印到標準輸出了。需要注意的是,我們不能同時使用這兩個標記。使用這兩個標記打印出來的源碼內容幾乎相同,而最大的區(qū)別也只是格式方面的。

第二類的三個標記都與動態(tài)鏈接庫有關。在類Unix系統(tǒng)下,標記-dynimport的值可以是一個ELF(Executable and Linkable Format)格式或者Mach-O(Mach Object)格式的文件的路徑。ELF即可執(zhí)行鏈接文件格式。ELF格式的文件保存了足夠的系統(tǒng)相關信息,以至于使它能夠支持不同平臺上的交叉編譯和交叉鏈接,可移植性很強。同時,它在執(zhí)行中支持動態(tài)鏈接共享庫。我們在Linux操作系統(tǒng)下使用go命令生成的命令源碼文件的可執(zhí)行文件就是ELF格式的。而Mach-O是一種用于可執(zhí)行文件、目標代碼、動態(tài)鏈接庫和內核轉儲的文件格式。在Windows下,這個標記的值應該是一個PE(Portable Execute)格式的文件的路徑。在Windows操作系統(tǒng)下,使用go命令生成的命令源碼文件的可執(zhí)行文件就是PE格式的。

實質上,加入標記-dynimportgo tool cgo命令相當于一個被構建在cgo工具內部的獨立的幫助命令。使用方法如go tool cgo -dynimport='cgo_demo.go'。這個命令會掃描這個標記的值所代表的可執(zhí)行文件,并將其中記錄的與已導入符號和已導入代碼庫相關的信息打印到標準輸出。go build命令程序中有專門為cgo工具制定的規(guī)則。這使得它可以在編譯直接或間接依賴了代碼包C的命令源碼文件時可以生成適當的可執(zhí)行文件。在這個可執(zhí)行文件中,直接包含了相關的已導入符號和已導入代碼庫的信息,以供之后使用。這樣就無需使鏈接器復制gcc編譯器的所有關于如何尋找已導入的符號以及使用它的位置的專業(yè)知識了。下面我們來試用一下go tool cgo -dynimport命令。

首先,我們創(chuàng)建一個命令源碼文件cgo_demo.go,并把它存放在goc2p項目的代碼包basic/cgo對應的目錄下。命令源碼文件cgo_demo.go的內容如下:

package main

import (
    cgolib "basic/cgo/lib"
    "fmt"
)

func main() {
    input := float32(2.33)
    output, err := cgolib.Sqrt(input)
    if err != nil {
        fmt.Errorf("Error: %s\n", err)
    }
    fmt.Printf("The square root of %f is %f.\n", input, output)
}

在這個命令源碼文件中,我們調用了goc2p項目的代碼包basic/cgo/lib中的函數Sqrt。這個函數是被保存在庫源碼文件math.go中的。而在文件math.go中,我們導入了代碼包C。也就是說,命令源碼文件cgo_demo.go間接的依賴了代碼包C。現在,我們使用go build命令將這個命令源碼文件編譯成ELF格式的可執(zhí)行文件。然后,我們就能夠使用go tool cgo -dynimport命令查看其中的導入信息了。請看如下示例:

hc@ubt:~/golang/goc2p/basic/cgo$ go build cgo_demo.go
hc@ubt:~/golang/goc2p/basic/cgo$ go tool cgo -dynimport='cgo_demo'
#pragma cgo_import_dynamic pthread_attr_init pthread_attr_init#GLIBC_2.1 
    "libpthread.so.0"
#pragma cgo_import_dynamic pthread_attr_destroy pthread_attr_destroy#GLIBC_2.0 
    "libpthread.so.0"
#pragma cgo_import_dynamic stderr stderr#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic sigprocmask sigprocmask#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic free free#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic fwrite fwrite#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic malloc malloc#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic strerror strerror#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic srand srand#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic setenv setenv#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic __libc_start_main __libc_start_main#GLIBC_2.0 
    "libc.so.6"
#pragma cgo_import_dynamic fprintf fprintf#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic pthread_attr_getstacksize
     pthread_attr_getstacksize#GLIBC_2.1 "libpthread.so.0"
#pragma cgo_import_dynamic sigfillset sigfillset#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic __errno_location __errno_location#GLIBC_2.0 
    "libpthread.so.0"
#pragma cgo_import_dynamic sqrt sqrt#GLIBC_2.0 "libm.so.6"
#pragma cgo_import_dynamic rand rand#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic pthread_create pthread_create#GLIBC_2.1 
    "libpthread.so.0"
#pragma cgo_import_dynamic abort abort#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic _ _ "libm.so.6"
#pragma cgo_import_dynamic _ _ "libpthread.so.0"
#pragma cgo_import_dynamic _ _ "libc.so.6"

從上面示例的輸出信息中,我們可以看到可執(zhí)行文件cgo_demo所涉及到的所有動態(tài)鏈接庫文件以及相關的函數名和代碼庫版本等信息。

如果我們再加入一個標記-dynlinker,那么在命令的輸出信息還會包含動態(tài)鏈接器的信息。示例如下:

hc@ubt:~/golang/goc2p/src/basic/cgo$ go tool cgo -dynimport='cgo_demo' -dynlinker
#pragma cgo_dynamic_linker "/lib/ld-linux.so.2"
<省略部分輸出內容>

如果我們在命令go tool cgo -dynimport后加入標記-dynout,那么命令的輸出信息將會寫入到指定的文件中,而不是被打印到標準輸出。比如命令go tool cgo -dynimport='cgo_demo' -dynlinker -dynout='cgo_demo.di'就會將可執(zhí)行文件cgo_demo中的導入信息以及動態(tài)鏈接器信息轉儲到當前目錄下的名為“cgo_demo.di”的文件中。

第四類標記包含了-gccgo、-gccgopkgpath-gccgoprefix。它們都與編譯器gccgo有關。標記-gccgo的作用是使cgo工具生成可供gccgo編譯器使用的源碼文件。這些源碼文件會與默認情況下生成的源碼文件在內容上有一些不同。實際上,到目前為止,cgo工具還不能很好的與gccgo編譯器一同使用。但是,按照gccgo編譯器的主要開發(fā)者Ian Lance Taylor的話來說,gccgo編譯器并不需要cgo工具,也不應該使用gcc工具。不管怎樣,這種情況將會在Go語言的1.3版本中得到改善。

第五類標記用于打印調試信息,包括標記-debug-define-debug-gcc。gcc工具不但會生成新的Go語言源碼文件以保存其對目標源碼改寫后的內容,還會生成若干個C語言源碼文件。cgo工具為了編譯這些C語言源碼文件,就會用到gcc編譯器。在加入-debug-gcc標記之后,gcc編譯器的輸出信息就會被打印到標準輸出上。另外,gcc編譯器在對C語言源碼文件進行編譯之后會產生一個結果文件。這個結果文件就是在_obj子目錄下的名為cgo.o的文件。

第六類標記的默認值都為true。也就是說,在默認情況下cgo工具生成的_obj子目錄下的Go語言源碼文件_cgogotypes.go中會包含代碼包導入語句```import "runtime/cgo"import "syscall"。代碼包導入語句import _ "runtime/cgo"只是引發(fā)了代碼包runtime/cgo中的初始化函數的執(zhí)行而沒有被分配到一個具體的命名空間上。在這些初始化函數中,包含了對一些C語言的全局變量和函數聲明的初始化過程。需要注意的是,只要我們在執(zhí)行go tool cgo命令的時候加入了標記-gccgo,即使標記-import_runtime_cgo有效,在Go語言源碼文件_cgo_gotypes.go中也不會包含import _ "runtime/cgo"```語句。

至此,我們在本小節(jié)討論的都是Go語言代碼如果通過cgo工具調用標準C語言編寫的函數。其實,我們利用cgo工具還可以把Go語言編寫的函數暴露給C語言代碼。

Go語言可以使它的函數被C語言代碼所用。這是通過使用一個特殊的注釋“//export”來實現的。示例如下:

package cgo

/*
#include <stdio.h>
extern void CFunction1();
*/
import "C"

import "fmt"

//export GoFunction1
func GoFunction1() {
        fmt.Println("GoFunction1() is called.")
}

func CallCFunc() {
        C.CFunction1()
}

在這個示例中,我們使用注釋行“//export GoFunction1”把Go語言函數GoFunction1暴露給了C語言代碼。注意,注釋行中在“//export ”之后的字符串必須與其下一行的那個函數的名字一致。我們也可以把字符串“//export”看成一種指令符,就像#cgo#include。這里有一個限制,就是只要我們使用了指令符“//export”,在當前源碼文件的序文中就不能包含任何C語言定義語句,只可以包含C語言聲明語句。上面示例的序文中的extern void CFunction1();就是一個很好的例子。序文中的這一行C語言聲明語句會被拷貝到兩個不同的cgo工具生成的C語言源碼文件中。這也正是其序文中不能包含C語言定義語句的原因。那么C語言函數CFunction1的定義語句我們應該放在哪兒呢?答案是放到在同目錄的其它Go語言源碼文件的序文中,或者直接寫到C語言源碼文件中。

我們把上面示例中的內容保存到名為go_export.go的文件中,并放到goc2p項目的basic/cgo/lib代碼包中?,F在我們使用go tool cgo來處理這個源碼文件。如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go

之后,我們會發(fā)現在_obj子目錄下的C語言頭文件_cgo_export.h中包含了這樣一行代碼:

extern void GoFunction1();

這說明C語言代碼已經可以對函數GoFunction1進行調用了?,F在我們使用go build命令構建goc2p項目的代碼包basic/cgo,如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build
# basic/cgo/lib
/tmp/go-build477634277/basic/cgo/lib/_obj/go_export.cgo2.o: In function `_cgo_cc103c85817e_Cfunc_CFunction1':
./go_export.go:34: undefined reference to `CFunction1'
collect2: ld return 1

構建并沒有成功完成。根據錯誤提示信息我們獲知,C語言函數CFunction1未被定義。這個問題的原因是我們并沒有在Go語言源碼文件go_export.go的序文中寫入C語言函數CFunction1的實現,也即未對它進行定義。我們之前說過,在這種情況下,對應函數的定義應該被放到其它Go語言源碼文件的序文或者C語言源碼文件中?,F在,我們在當前目錄下創(chuàng)建一個新的Go語言源碼文件go_export_def.go。其內容如下:

package cgo

/*
#include <stdio.h>
void CFunction1() {
        printf("CFunction1() is called.\n");
        GoFunction1();
} 
*/
import "C"

這個文件是專門用于存放C語言函數定義的。注意,由于C語言函數printf來自C語言標準代碼庫stdio.h,所以我們需要在序文中使用指令符#include將它引入。保存好源碼文件go_export_def.go之后,我們重新使用go tool cgo命令處理這兩個文件,如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go go_export_def.go

然后,我們再次執(zhí)行go build命令構建代碼包basic/cgo/lib

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build

顯然,這次的構建成功完成。當然單獨構建代碼包basic/cgo/lib并不是必須的。我們在這里是為了檢查該代碼包中的代碼(包括Go語言代碼和C語言代碼)是否都能夠被正確編譯。

還記得goc2p項目的代碼包basic/cgo中的命令源碼文件cgo_demo.go?,F在我們在它的main函數的最后加入一行新代碼:cgo.CallCFunc(),即調用在代碼包``basic/cgo/lib```中的庫源碼文件go_export.go的函數。然后,我們運行這個命令源碼文件:

hc@ubt:~/golang/goc2p/basic/cgo$ go run cgo_demo.go
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.

從輸出的信息可以看出,我們定義的C語言函數CFunction1和Go語言函數GoFunction1都已被調用,并且調用順序正如我們所愿。這個例子也說明,我們可以非常方便的使用cgo工具完成如下幾件事:

  1. Go語言代碼調用標準C語言的代碼。這也使得我們可以使用Go語言封裝任何已存在的C語言代碼庫,并提供給其他Go語言代碼使用。

  2. 可以在Go語言源碼文件的序文中自定義任何C語言代碼并由Go語言代碼使用。這使得我們可以更靈活的對C語言代碼進行封裝。同時,我們還可以利用這一特性在我們自定義的C語言代碼中使用Go語言代碼。

  3. 通過指令符“//export”,可使C語言代碼能夠使用Go語言代碼。這里所說的C語言代碼是指我們在Go語言源碼文件的序文中自定義的C語言代碼。但是,go tool cgo命令會將序文中的C語言代碼聲明和定義分別寫入到其生成的C語言頭文件和C語言源碼文件中。所以,從原則上講,這已經具備了讓外部C語言代碼使用Go語言代碼的能力。

綜上所述,cgo工具不但可以使Go語言直接使用現存的非常豐富的C語言代碼庫,還可以使用Go語言代碼擴展現有的C語言代碼庫。

至此,我們介紹了怎樣獨立的使用cgo工具。但實際上,我們可以直接使用標準go命令構建、安裝和運行導入了代碼包C的代碼包和源碼文件。標準go命令能夠認出代碼包C的導入語句并自動使用cgo工具進行處理。示例如下:

hc@ubt:~/golang/goc2p/src/basic/cgo$ rm -rf lib/_obj
hc@ubt:~/golang/goc2p/src/basic/cgo$ go run cgo_demo.go
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.

在上例中,我們首先刪除了代碼包basic/cgo/lib目錄下的子目錄_obj,以此來保證原始的測試環(huán)境。然后,我們直接運行了命令源碼文件cgo_demo.go。在這個源碼文件中,包含了對代碼包basic/cgo/lib中函數的調用語句,而在這些函數中又包含了對代碼包C的引用。從輸出信息我們可以看出,命令源碼文件cgo_demo.go的運行成功的完成了。這也驗證了標準go命令在這方面的功能。不過,有時候我們還是很有必要單獨使用go tool cgo命令,比如對相關的Go語言代碼和C語言代碼的功能進行驗證或者需要通過標記定制化運行cgo工具的時候。另外,如果我們通過標準go命令構建或者安裝直接或間接導入了代碼C的命令源碼文件,那么在生成的可執(zhí)行文件中就會包含動態(tài)導入數據和動態(tài)鏈接器信息。我們可以使用go tool cgo命令查看可執(zhí)行文件中的這些信息。

上一篇:go env下一篇:go clean