您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Go語言怎么實現CGO編程”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Go語言怎么實現CGO編程”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
下面是我們構建的最簡 CGO 程序:
// hello.go package main //#include <stdio.h> import "C" func main() { C.puts(C.CString("Hello, this is a CGO demo.\n")) }
上面就是使用了C標準庫中已有的函數來實現的一個簡單的 CGO 程序。
下面我們再來看個例子。先自定義一個叫 SayHello 的 C 函數來實現打印,然后從 Go 語言環境中調用這個 SayHello 函數:
// hello.go package main /* #include <stdio.h> static void SayHello(const char* s) { puts(s); } */ import "C" func main() { C.SayHello(C.CString("Hello, World\n")) }
除了 SayHello 函數是我們自己實現的之外,其它的部分和前面的例子基本相似。
我們也可以將 SayHello 函數放到當前目錄下的一個 C 語言源文件中(后綴名必須是.c)。因為是編寫在獨立的 C 文件中,為了允許外部引用,所以需要去掉函數的 static 修飾符。
// hello.c #include <stdio.h> void SayHello(const char* s) { puts(s); }
然后在 CGO 部分先聲明 SayHello 函數,其它部分不變:
// hello.go package main //void SayHello(const char* s); import "C" func main() { C.SayHello(C.CString("Hello, World\n")) }
在編程過程中,抽象和模塊化是將復雜問題簡化的通用手段。當代碼語句變多時,我們可以將相似的代碼封裝到一個個函數中;當程序中的函數變多時,我們將函數拆分到不同的文件或模塊中。
在前面的例子中,我們可以抽象一個名為 hello 的模塊,模塊的全部接口函數都聲明在 hello.h 頭文件中:
// hello.h void SayHello(const char* s);
下面是 SayHello 函數的 C 語言實現,對應 hello.c 文件:
// hello.c #include "hello.h" #include <stdio.h> void SayHello(const char* s) { puts(s); }
我們也可以用 C++語言來重新實現這個 C 語言函數:
// hello.cpp #include <iostream> extern "C" { #include "hello.h" } void SayHello(const char* s) { std::cout << s; }
其實 CGO 不僅僅用于 Go 語言中調用 C 語言函數,還可以用于導出 Go 語言函數給 C 語言函數調用。
在前面的例子中,我們已經抽象一個名為 hello 的模塊,模塊的全部接口函數都在 hello.h 頭文件中定義:
// hello.h void SayHello(char* s);
現在我們創建一個 hello.go 文件,用 Go 語言重新實現 C 語言接口的 SayHello 函數:
// hello.go package main import "C" import "fmt" //export SayHello func SayHello(s *C.char) { fmt.Print(C.GoString(s)) }
我們通過 CGO 的 //export SayHello
指令將 Go 語言實現的函數 SayHello
導出為 C 語言函數。為了適配 CGO 導出的 C 語言函數,我們禁止了在函數的聲明語句中的 const
修飾符。
通過面向 C 語言接口的編程技術,我們不僅僅解放了函數的實現者,同時也簡化的函數的使用者。現在我們可以將 SayHello 當作一個標準庫的函數使用,如下:
// main.go package main //#include <hello.h> import "C" func main() { C.SayHello(C.CString("Hello, World\n")) }
簡單來說就是將上面例子中的幾個文件重新合并到一個 Go 文件實現,如下:
// main.go package main //void SayHello(char* s); import "C" import ( "fmt" ) func main() { C.SayHello(C.CString("Hello, World\n")) } //export SayHello func SayHello(s *C.char) { fmt.Print(C.GoString(s)) }
雖然看起來全部是 Go 語言代碼,但是執行的時候是先從 Go 語言的 main 函數,到 CGO 自動生成的 C 語言版本 SayHello 橋接函數,最后又回到了 Go 語言環境的 SayHello 函數。這個代碼包含了 CGO 編程的精華。
如果在 Go 代碼中出現了 import "C"
語句則表示使用了 CGO 特性,緊跟在這行語句前面的注釋是一種特殊語法,里面包含的是正常的 C 語言代碼。當確保 CGO 啟用的情況下,還可以在當前目錄中包含 C/C++對應的源文件。比如上面的例子。
在 import "C"
語句前的注釋中可以通過 #cgo
語句設置編譯階段和鏈接階段的相關參數。編譯階段的參數主要用于定義相關宏和指定頭文件檢索路徑。鏈接階段的參數主要是指定庫文件檢索路徑和要鏈接的庫文件。
比如:
// #cgo CFLAGS: -DADDR_DEBUG=1 -I./include // #cgo LDFLAGS: -L/usr/local/lib -linet_addr // #include <inet_addr.h> import "C"
上面的代碼中,CFLAGS
部分,-D
部分定義了宏 ADDR_DEBUG,值為 1;-I
定義了頭文件包含的檢索目錄。LDFLAGS
部分,-L
指定了鏈接時庫文件檢索目錄,-l
指定了鏈接時需要鏈接 inet_addr
庫。
因為 C/C++遺留的問題,C 頭文件檢索目錄可以是相對目錄,但是庫文件檢索目錄則需要絕對路徑。
由于 Go 語言實現的限制,我們無法在 Go 語言中創建大于 2GB 內存的切片(可參考 makeslice 實現源碼)。不過借助 cgo 技術,我們可以在 C 語言環境創建大于 2GB 的內存,然后轉為 Go 語言的切片使用:
package main /* #include <stdlib.h> void* makeslice(size_t memsize) { return malloc(memsize); } */ import "C" import "unsafe" func makeByteSlize(n int) []byte { p := C.makeslice(C.size_t(n)) return ((*[1 << 31]byte)(p))[0:n:n] } func freeByteSlice(p []byte) { C.free(unsafe.Pointer(&p[0])) } func main() { s := makeByteSlize(1<<32+1) s[len(s)-1] = 255 print(s[len(s)-1]) freeByteSlice(s) }
例子中我們通過 makeByteSlize 來創建大于 4G 內存大小的切片,從而繞過了 Go 語言實現的限制。而 freeByteSlice 輔助函數則用于釋放從 C 語言函數創建的切片。
因為 C 語言內存空間是穩定的,基于 C 語言內存構造的切片也是穩定的,不會因為 Go 語言棧的變化而被移動。
CGO 提供了 golang 和 C 語言相互調用的機制。而在某些第三方庫可能只有 C/C++ 的實現,也沒有必要用純 golang 重新實現,因為可能工作量比較大,比較耗時,這時候 CGO 就派上用場了。
被調用的 C 代碼可以直接以源代碼形式提供或者打包靜態庫或動態庫在編譯時鏈接。
這里推薦使用靜態庫的方式,這樣方便代碼隔離,也符合 Go 的哲學。
當你在 Go 包中導入 "C" 時,go build 需要做更多的工作來構建你的代碼。
需要調用 cgo 工具來生成 C 到 Go 和 Go 到 C 的相關代碼。
系統中的 C 編譯器會為軟件包中的每個 C 文件進行調用處理。
各個編譯單元被合并到一個 .o 文件中。
生成的 .o 文件會通過系統的鏈接器,對其引用的共享對象進行修正。
在引入了 cgo 之后,你需要設置所有的環境變量,跟蹤可能安裝在奇怪地方的共享對象和頭文件等。
另外需要注意,Go 支持許多的平臺,而 cgo 并不是。需要安裝 C 編譯器,而不僅僅是 Go 編譯器。而且可能還需要安裝你的項目所依賴的 C 語言庫,這也是需要技術成本的。
內存管理變得復雜,C 是沒有垃圾收集的,而 go 有,兩者的內存管理機制不同,可能會帶來內存泄漏。
CGO 是 Go 語言和 C 語言的橋梁,它使二者在二進制接口層面實現了互通,但是我們要注意因兩種語言的內存模型的差異而可能引起的問題。
如果在 CGO 處理的跨語言函數調用時涉及到了指針的傳遞,則可能會出現 Go 語言和 C 語言共享某一段內存的場景。
我們知道 C 語言的內存在分配之后就是穩定的,但是 Go 語言因為函數棧的動態伸縮可能導致棧中內存地址的移動(這是 Go 和 C 內存模型的最大差異)。如果 C 語言持有的是移動之前的 Go 指針,那么以舊指針訪問 Go 對象時會導致程序崩潰。
CGO 在使用 C/C++資源的時候一般有三種形式:
直接使用源碼;
鏈接靜態庫;
鏈接動態庫。
直接使用源碼就是在 import "C"
之前的注釋部分包含 C 代碼,或者在當前包中包含 C/C++源文件。
鏈接靜態庫和動態庫的方式比較類似,都是通過在 LDFLAGS
選項指定要鏈接的庫方式鏈接。這里主要關注在 CGO 中如何使用靜態庫的問題。
如果 CGO 中引入的 C/C++資源有代碼而且代碼規模也比較小,直接使用源碼是最理想的方式,但很多時候我們并沒有源代碼,或者從 C/C++源代碼開始構建的過程異常復雜,這種時候使用 C 靜態庫也是一個不錯的選擇。
靜態庫因為是靜態鏈接,最終的目標程序并不會產生額外的運行時依賴,也不會出現動態庫特有的跨運行時資源管理的錯誤。
我們先用純 C 語言構造一個簡單的靜態庫。我們要構造的靜態庫名叫 sum,庫中只有一個 sum_add 函數,用于表示數論中的模加法運算。sum 庫的文件都在 sum 目錄下。
sum/sum.h 頭文件只有一個純 C 語言風格的函數聲明:
int sum_add(int a, int b);
sum/sum.c 對應函數的實現:
#include "sum.h" int sum_add(int a, int b) { return a+b; }
通過以下命令可以生成一個叫 libsum.a 的靜態庫:
$ cd ./sum $ gcc -c -o sum.o sum.c $ ar rcs libsum.a sum.o
生成 libsum.a 靜態庫之后,放到當前的lib目錄下,我們就可以在 CGO 中使用該資源了。
創建 main.go 文件如下:
package main /* #cgo CFLAGS: -I./sum #cgo LDFLAGS: -L./lib -lsum #include "sum.h" */ import "C" import "fmt" func main() { fmt.Println(C.sum_add(10, 5)) }
其中有兩個 #cgo
命令,分別是編譯和鏈接參數。
CFLAGS 通過 -I./sum
將 sum 庫對應頭文件所在的目錄加入頭文件檢索路徑。
LDFLAGS 通過 -L./lib
將編譯后 sum 靜態庫所在目錄加為鏈接庫檢索路徑,-lsum
表示鏈接 libsum.a 靜態庫。
需要注意的是,在鏈接部分的檢索路徑不能使用相對路徑(C/C++代碼的鏈接程序所限制)
這里以一個實際案例(分兩塊代碼)來說明 CGO 如何使用靜態庫的。案例實現的功能說明:
c++ 代碼實現初始化配置、解析傳入的 mq 消息,并處理具體的邏輯
go 代碼實現初始化相關配置(mq 等)、監聽訂單消息等工作
#include <iostream> extern "C"{ int init(int logLevel, int disId); void RecvAndDealMessage(char* sbuf, int len); } // 初始化 int init(int logLevel, int disId) { g_xmfDisId = disId; // 服務初始化 if(CCGI_STUB_CNTL->Initialize() != 0) { printf("CCGI_STUB_CNTL->Init failed\n"); return -1; } CCGI_STUB_CNTL->setTimeout(5); // 日志初始化 std::string strModuleName = "xxxxxx"; int iRet = MD_LOG->QuickInitForAPP(strModuleName.c_str(), MD_LOG_FILE_PATH, logLevel); if (iRet != 0) { printf("log init failed. module:%s logswitch:%d ret:%d", strModuleName.c_str(), logLevel, iRet); return 1; } else { printf("Init log Ok\n"); } MD_COMM_LOG_DEBUG("Log Init Finished. level:%d", logLevel); return iRet; } // 處理消息數據 void RecvAndDealMessage(char* sbuf, int len) { MD_COMM_LOG_DEBUG("Begin receive message..."); MessageContainer oMsgCon; char strbuf[1024]; if(len > 1024) { MD_COMM_LOG_ERR(MESSAGE_TOO_LONG, "len = %d, message too long.", len); return ; } snprintf(strbuf, 1024, "%s", sbuf); MD_COMM_LOG_DEBUG("recvmessage:[%s] len:[%d]", strbuf, len); //解析并處理收到的消息 DealMsg(strbuf, oMsgCon); }
main 函數實現:
package main /* #cgo LDFLAGS: -lstdc++ #cgo LDFLAGS: -L../lib -ldaemon_qiyegou_finacial_deal_listen #cgo LDFLAGS: -L../lib -llibrary_util -lcgistub -linet_addr -ljsoncpp int init(int logLevel, int disId); void RecvAndDealMessage(char* sbuf, int len); */ import "C" func main() { //解析參數 if Init() { defer func() { if err := recover(); err != nil { md_log.Errorf(-100, nil, "panic:%v, stack:%v", err, string(debug.Stack())) } }() for { //業務處理 run() } } }
init 函數實現:
func Init() bool { iniFile, err := ini.LoadFile(os.Args[1]) if err != nil { fmt.Println("load config faild, config:", os.Args[1]) return false } logswitch := iniFile.GetInt("biz","logswitch",255) md_log.Init(DAEMON_NAME, iniFile.GetInt("biz","logswitch",255)) md_log.Debugf("log init success!") // cgo 調用c++初始化函數 ret := C.init(C.int(logswitch),C.int(xmf_dis_id)) if ret != 0 { fmt.Printf("init failed ret:%v \n", ret) return false } fmt.Println("initial success!") return true }
run 函數代碼:
func run() { var oConsumer rabbitmq.Consumer oConsumer.Init(Mqdns, MqexchangeName, Mqqueue, Mqroute) msgs, err := oConsumer.StartConsume(Mqqueue,false) if err != nil{ fmt.Printf("oConsumer.StartConsume failed:%+v, arg:%+v \n",err, Mq return } for msg := range msgs{ strMsg := string(msg.Body) msg.Ack(true) // 調用 C++ 處理消息的函數 C.RecvAndDealMessage(C.CString(strMsg), C.int(len(strMsg))) //c++ 處理mq消息 } }
讀到這里,這篇“Go語言怎么實現CGO編程”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。