Go語言開發(fā)快速學習CGO編程
快速上手 CGO 程序
真實的 CGO 程序原理一般都比較復雜,但是在使用層面上來說,其實沒有想象的那么難。
今天我們可以由淺入深來看看一個 CGO 程序該是怎么樣實現(xiàn)的?
如果要構(gòu)造一個簡單的 CGO 程序,首先要忽視一些復雜的 CGO 特性,下面我們來快速上手一個 CGO 程序。
基于 C 標準庫實現(xiàn)最簡單的 CGO 程序
下面是我們構(gòu)建的最簡 CGO 程序:
// hello.go
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("Hello, this is a CGO demo.\n"))
}
基于自己寫的 C 函數(shù)構(gòu)建 CGO 程序
上面就是使用了C標準庫中已有的函數(shù)來實現(xiàn)的一個簡單的 CGO 程序。
下面我們再來看個例子。先自定義一個叫 SayHello 的 C 函數(shù)來實現(xiàn)打印,然后從 Go 語言環(huán)境中調(diào)用這個 SayHello 函數(shù):
// 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 函數(shù)是我們自己實現(xiàn)的之外,其它的部分和前面的例子基本相似。
我們也可以將 SayHello 函數(shù)放到當前目錄下的一個 C 語言源文件中(后綴名必須是.c)。因為是編寫在獨立的 C 文件中,為了允許外部引用,所以需要去掉函數(shù)的 static 修飾符。
// hello.c
#include <stdio.h>
void SayHello(const char* s) {
puts(s);
}
然后在 CGO 部分先聲明 SayHello 函數(shù),其它部分不變:
// hello.go
package main
//void SayHello(const char* s);
import "C"
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
模塊化以上例子
在編程過程中,抽象和模塊化是將復雜問題簡化的通用手段。當代碼語句變多時,我們可以將相似的代碼封裝到一個個函數(shù)中;當程序中的函數(shù)變多時,我們將函數(shù)拆分到不同的文件或模塊中。
在前面的例子中,我們可以抽象一個名為 hello 的模塊,模塊的全部接口函數(shù)都聲明在 hello.h 頭文件中:
// hello.h void SayHello(const char* s);
下面是 SayHello 函數(shù)的 C 語言實現(xiàn),對應(yīng) hello.c 文件:
// hello.c
#include "hello.h"
#include <stdio.h>
void SayHello(const char* s) {
puts(s);
}
我們也可以用 C++語言來重新實現(xiàn)這個 C 語言函數(shù):
// hello.cpp
#include <iostream>
extern "C" {
#include "hello.h"
}
void SayHello(const char* s) {
std::cout << s;
}
用 Go 實現(xiàn) C 函數(shù)并導出
其實 CGO 不僅僅用于 Go 語言中調(diào)用 C 語言函數(shù),還可以用于導出 Go 語言函數(shù)給 C 語言函數(shù)調(diào)用。
在前面的例子中,我們已經(jīng)抽象一個名為 hello 的模塊,模塊的全部接口函數(shù)都在 hello.h 頭文件中定義:
// hello.h void SayHello(char* s);
現(xiàn)在我們創(chuàng)建一個 hello.go 文件,用 Go 語言重新實現(xiàn) C 語言接口的 SayHello 函數(shù):
// hello.go
package main
import "C"
import "fmt"
//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}
我們通過 CGO 的 //export SayHello 指令將 Go 語言實現(xiàn)的函數(shù) SayHello 導出為 C 語言函數(shù)。為了適配 CGO 導出的 C 語言函數(shù),我們禁止了在函數(shù)的聲明語句中的 const 修飾符。
通過面向 C 語言接口的編程技術(shù),我們不僅僅解放了函數(shù)的實現(xiàn)者,同時也簡化的函數(shù)的使用者。現(xiàn)在我們可以將 SayHello 當作一個標準庫的函數(shù)使用,如下:
// main.go
package main
//#include <hello.h>
import "C"
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
用 C 接口的方式實現(xiàn) Go 編程
簡單來說就是將上面例子中的幾個文件重新合并到一個 Go 文件實現(xiàn),如下:
// 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 語言代碼,但是執(zhí)行的時候是先從 Go 語言的 main 函數(shù),到 CGO 自動生成的 C 語言版本 SayHello 橋接函數(shù),最后又回到了 Go 語言環(huán)境的 SayHello 函數(shù)。這個代碼包含了 CGO 編程的精華。
CGO 的主要基礎(chǔ)參數(shù)
import "C" 語句說明
如果在 Go 代碼中出現(xiàn)了 import "C" 語句則表示使用了 CGO 特性,緊跟在這行語句前面的注釋是一種特殊語法,里面包含的是正常的 C 語言代碼。當確保 CGO 啟用的情況下,還可以在當前目錄中包含 C/C++對應(yīng)的源文件。比如上面的例子。
#cgo 語句說明
在 import "C" 語句前的注釋中可以通過 #cgo 語句設(shè)置編譯階段和鏈接階段的相關(guān)參數(shù)。編譯階段的參數(shù)主要用于定義相關(guān)宏和指定頭文件檢索路徑。鏈接階段的參數(shù)主要是指定庫文件檢索路徑和要鏈接的庫文件。
比如:
// #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 頭文件檢索目錄可以是相對目錄,但是庫文件檢索目錄則需要絕對路徑。
為什么要引入 CGO
突破 Go 創(chuàng)建切片的內(nèi)存限制
由于 Go 語言實現(xiàn)的限制,我們無法在 Go 語言中創(chuàng)建大于 2GB 內(nèi)存的切片(可參考 makeslice 實現(xiàn)源碼)。不過借助 cgo 技術(shù),我們可以在 C 語言環(huán)境創(chuàng)建大于 2GB 的內(nèi)存,然后轉(zhuǎn)為 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 來創(chuàng)建大于 4G 內(nèi)存大小的切片,從而繞過了 Go 語言實現(xiàn)的限制。而 freeByteSlice 輔助函數(shù)則用于釋放從 C 語言函數(shù)創(chuàng)建的切片。
因為 C 語言內(nèi)存空間是穩(wěn)定的,基于 C 語言內(nèi)存構(gòu)造的切片也是穩(wěn)定的,不會因為 Go 語言棧的變化而被移動。
方便在 Go 語言中接入使用 C/C++的軟件資源
CGO 提供了 golang 和 C 語言相互調(diào)用的機制。而在某些第三方庫可能只有 C/C++ 的實現(xiàn),也沒有必要用純 golang 重新實現(xiàn),因為可能工作量比較大,比較耗時,這時候 CGO 就派上用場了。
被調(diào)用的 C 代碼可以直接以源代碼形式提供或者打包靜態(tài)庫或動態(tài)庫在編譯時鏈接。
這里推薦使用靜態(tài)庫的方式,這樣方便代碼隔離,也符合 Go 的哲學。
CGO 帶來的問題
構(gòu)建時間變長
當你在 Go 包中導入 "C" 時,go build 需要做更多的工作來構(gòu)建你的代碼。
- 需要調(diào)用 cgo 工具來生成 C 到 Go 和 Go 到 C 的相關(guān)代碼。
- 系統(tǒng)中的 C 編譯器會為軟件包中的每個 C 文件進行調(diào)用處理。
- 各個編譯單元被合并到一個 .o 文件中。
- 生成的 .o 文件會通過系統(tǒng)的鏈接器,對其引用的共享對象進行修正。
構(gòu)建變得復雜
在引入了 cgo 之后,你需要設(shè)置所有的環(huán)境變量,跟蹤可能安裝在奇怪地方的共享對象和頭文件等。
另外需要注意,Go 支持許多的平臺,而 cgo 并不是。需要安裝 C 編譯器,而不僅僅是 Go 編譯器。而且可能還需要安裝你的項目所依賴的 C 語言庫,這也是需要技術(shù)成本的。
Go 和 C 內(nèi)存模型不同
內(nèi)存管理變得復雜,C 是沒有垃圾收集的,而 go 有,兩者的內(nèi)存管理機制不同,可能會帶來內(nèi)存泄漏。
CGO 是 Go 語言和 C 語言的橋梁,它使二者在二進制接口層面實現(xiàn)了互通,但是我們要注意因兩種語言的內(nèi)存模型的差異而可能引起的問題。
如果在 CGO 處理的跨語言函數(shù)調(diào)用時涉及到了指針的傳遞,則可能會出現(xiàn) Go 語言和 C 語言共享某一段內(nèi)存的場景。
我們知道 C 語言的內(nèi)存在分配之后就是穩(wěn)定的,但是 Go 語言因為函數(shù)棧的動態(tài)伸縮可能導致棧中內(nèi)存地址的移動(這是 Go 和 C 內(nèi)存模型的最大差異)。如果 C 語言持有的是移動之前的 Go 指針,那么以舊指針訪問 Go 對象時會導致程序崩潰。
使用 C 靜態(tài)庫實現(xiàn)
CGO 在使用 C/C++資源的時候一般有三種形式:
- 直接使用源碼;
- 鏈接靜態(tài)庫;
- 鏈接動態(tài)庫。
直接使用源碼就是在 import "C" 之前的注釋部分包含 C 代碼,或者在當前包中包含 C/C++源文件。
鏈接靜態(tài)庫和動態(tài)庫的方式比較類似,都是通過在 LDFLAGS 選項指定要鏈接的庫方式鏈接。這里主要關(guān)注在 CGO 中如何使用靜態(tài)庫的問題。
具體實現(xiàn)
如果 CGO 中引入的 C/C++資源有代碼而且代碼規(guī)模也比較小,直接使用源碼是最理想的方式,但很多時候我們并沒有源代碼,或者從 C/C++源代碼開始構(gòu)建的過程異常復雜,這種時候使用 C 靜態(tài)庫也是一個不錯的選擇。
靜態(tài)庫因為是靜態(tài)鏈接,最終的目標程序并不會產(chǎn)生額外的運行時依賴,也不會出現(xiàn)動態(tài)庫特有的跨運行時資源管理的錯誤。
我們先用純 C 語言構(gòu)造一個簡單的靜態(tài)庫。我們要構(gòu)造的靜態(tài)庫名叫 sum,庫中只有一個 sum_add 函數(shù),用于表示數(shù)論中的模加法運算。sum 庫的文件都在 sum 目錄下。
sum/sum.h 頭文件只有一個純 C 語言風格的函數(shù)聲明:
int sum_add(int a, int b);
sum/sum.c 對應(yīng)函數(shù)的實現(xiàn):
#include "sum.h"
int sum_add(int a, int b) {
return a+b;
}
通過以下命令可以生成一個叫 libsum.a 的靜態(tài)庫:
$ cd ./sum $ gcc -c -o sum.o sum.c $ ar rcs libsum.a sum.o
生成 libsum.a 靜態(tài)庫之后,放到當前的lib目錄下,我們就可以在 CGO 中使用該資源了。
創(chuàng)建 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 命令,分別是編譯和鏈接參數(shù)。
CFLAGS 通過 -I./sum 將 sum 庫對應(yīng)頭文件所在的目錄加入頭文件檢索路徑。
LDFLAGS 通過 -L./lib 將編譯后 sum 靜態(tài)庫所在目錄加為鏈接庫檢索路徑,-lsum 表示鏈接 libsum.a 靜態(tài)庫。
需要注意的是,在鏈接部分的檢索路徑不能使用相對路徑(C/C++代碼的鏈接程序所限制)
實戰(zhàn)應(yīng)用
這里以一個實際案例(分兩塊代碼)來說明 CGO 如何使用靜態(tài)庫的。案例實現(xiàn)的功能說明:
- c++ 代碼實現(xiàn)初始化配置、解析傳入的 mq 消息,并處理具體的邏輯
- go 代碼實現(xiàn)初始化相關(guān)配置(mq 等)、監(jiān)聽訂單消息等工作
C++ 代碼主要實現(xiàn)
#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;
// 服務(wù)初始化
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;
}
// 處理消息數(shù)據(jù)
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);
}
Go 代碼主要實現(xiàn)
main 函數(shù)實現(xiàn):
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() {
//解析參數(shù)
if Init() {
defer func() {
if err := recover(); err != nil {
md_log.Errorf(-100, nil, "panic:%v, stack:%v", err, string(debug.Stack()))
}
}()
for {
//業(yè)務(wù)處理
run()
}
}
}
init 函數(shù)實現(xiàn):
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 調(diào)用c++初始化函數(shù)
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 函數(shù)代碼:
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)
// 調(diào)用 C++ 處理消息的函數(shù)
C.RecvAndDealMessage(C.CString(strMsg), C.int(len(strMsg))) //c++ 處理mq消息
}
}
總結(jié)
通過以上實例說明,可以知道CGO其實是C語言和Go語言混合編程的技術(shù),因此要想熟練地使用CGO是非常有必要要了解這兩門語言的。
任何技術(shù)和語言都有它自身的優(yōu)點和不足,Go語言不是銀彈,它無法解決全部問題。而通過CGO可以做到以下幾點:
- 通過CGO可接入C/C++的世紀軟件遺產(chǎn)
- 通過CGO可以用Go給其它系統(tǒng)寫C接口的共享庫
- 通過CGO技術(shù)也可以讓Go語言編寫的代碼可以很好地融入現(xiàn)有的軟件生態(tài)
而現(xiàn)在的軟件確實大多數(shù)是建立在C/C++語言之上的。因此CGO可以說是一個統(tǒng)籌兼?zhèn)涞募夹g(shù),是Go的一個重量級的技術(shù),也是值得任何一個Go語言開發(fā)人員學習的。
以上就是Go語言開發(fā)快速學習CGO編程的詳細內(nèi)容,更多關(guān)于Go語言開發(fā)CGO編程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang使用泛型對數(shù)組進行去重的實現(xiàn)
本文主要介紹了Golang使用泛型對數(shù)組進行去重的實現(xiàn),通過使用類型參數(shù)T和類型約束any,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-02-02
一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化
內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷,本文主要為大家介紹了Go中內(nèi)聯(lián)優(yōu)化的具體使用,需要的可以參考下2023-05-05
Golang多線程下載器實現(xiàn)高效快速地下載大文件
Golang多線程下載器是一種高效、快速地下載大文件的方法。Golang語言天生支持并發(fā)和多線程,可以輕松實現(xiàn)多線程下載器的開發(fā)。通過使用Golang的協(xié)程和通道,可以將下載任務(wù)分配到多個線程中并行處理,提高了下載的效率和速度2023-05-05
go程序測試CPU占用率統(tǒng)計ps?vs?top兩種不同方式對比
這篇文章主要為大家介紹了go程序測試CPU占用率統(tǒng)計ps?vs?top兩種不同方式對比,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05

