Go语言怎么实现CGO编程
来源:亿速云
时间:2023-04-17 07:44:08 342浏览 收藏
编程并不是一个机械性的工作,而是需要有思考,有创新的工作,语法是固定的,但解决问题的思路则是依靠人的思维,这就需要我们坚持学习和更新自己的知识。今天golang学习网就整理分享《Go语言怎么实现CGO编程》,文章讲解的知识点主要包括CGO、go语言,如果你对Golang方面的知识点感兴趣,就不要错过golang学习网,在这可以对大家的知识积累有所帮助,助力开发能力的提升。
本文小编为大家详细介绍“Go语言怎么实现CGO编程”,内容详细,步骤清晰,细节处理妥当,希望这篇“Go语言怎么实现CGO编程”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
基于 C 标准库实现最简单的 CGO 程序
下面是我们构建的最简 CGO 程序:
// hello.go package main //#includeimport "C" func main() { C.puts(C.CString("Hello, this is a CGO demo.\n")) }
基于自己写的 C 函数构建 CGO 程序
上面就是使用了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 #includevoid 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" #includevoid SayHello(const char* s) { puts(s); }
我们也可以用 C++语言来重新实现这个 C 语言函数:
// hello.cpp #includeextern "C" { #include "hello.h" } void SayHello(const char* s) { std::cout 用 Go 实现 C 函数并导出
其实 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 //#includeimport "C" func main() { C.SayHello(C.CString("Hello, World\n")) } 用 C 接口的方式实现 Go 编程
简单来说就是将上面例子中的几个文件重新合并到一个 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 编程的精华。
CGO 的主要基础参数
import "C" 语句说明
如果在 Go 代码中出现了
import "C"
语句则表示使用了 CGO 特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的 C 语言代码。当确保 CGO 启用的情况下,还可以在当前目录中包含 C/C++对应的源文件。比如上面的例子。#cgo 语句说明
在
import "C"
语句前的注释中可以通过#cgo
语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。比如:
// #cgo CFLAGS: -DADDR_DEBUG=1 -I./include // #cgo LDFLAGS: -L/usr/local/lib -linet_addr // #includeimport "C" 上面的代码中,
CFLAGS
部分,-D
部分定义了宏 ADDR_DEBUG,值为 1;-I
定义了头文件包含的检索目录。LDFLAGS
部分,-L
指定了链接时库文件检索目录,-l
指定了链接时需要链接inet_addr
库。因为 C/C++遗留的问题,C 头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。
为什么要引入 CGO
突破 Go 创建切片的内存限制
由于 Go 语言实现的限制,我们无法在 Go 语言中创建大于 2GB 内存的切片(可参考 makeslice 实现源码)。不过借助 cgo 技术,我们可以在 C 语言环境创建大于 2GB 的内存,然后转为 Go 语言的切片使用:
package main /* #includevoid* 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 例子中我们通过 makeByteSlize 来创建大于 4G 内存大小的切片,从而绕过了 Go 语言实现的限制。而 freeByteSlice 辅助函数则用于释放从 C 语言函数创建的切片。
因为 C 语言内存空间是稳定的,基于 C 语言内存构造的切片也是稳定的,不会因为 Go 语言栈的变化而被移动。
方便在 Go 语言中接入使用 C/C++的软件资源
CGO 提供了 golang 和 C 语言相互调用的机制。而在某些第三方库可能只有 C/C++ 的实现,也没有必要用纯 golang 重新实现,因为可能工作量比较大,比较耗时,这时候 CGO 就派上用场了。
被调用的 C 代码可以直接以源代码形式提供或者打包静态库或动态库在编译时链接。
这里推荐使用静态库的方式,这样方便代码隔离,也符合 Go 的哲学。
CGO 带来的问题
构建时间变长
当你在 Go 包中导入 "C" 时,go build 需要做更多的工作来构建你的代码。
需要调用 cgo 工具来生成 C 到 Go 和 Go 到 C 的相关代码。
系统中的 C 编译器会为软件包中的每个 C 文件进行调用处理。
各个编译单元被合并到一个 .o 文件中。
生成的 .o 文件会通过系统的链接器,对其引用的共享对象进行修正。
构建变得复杂
在引入了 cgo 之后,你需要设置所有的环境变量,跟踪可能安装在奇怪地方的共享对象和头文件等。
另外需要注意,Go 支持许多的平台,而 cgo 并不是。需要安装 C 编译器,而不仅仅是 Go 编译器。而且可能还需要安装你的项目所依赖的 C 语言库,这也是需要技术成本的。
Go 和 C 内存模型不同
内存管理变得复杂,C 是没有垃圾收集的,而 go 有,两者的内存管理机制不同,可能会带来内存泄漏。
CGO 是 Go 语言和 C 语言的桥梁,它使二者在二进制接口层面实现了互通,但是我们要注意因两种语言的内存模型的差异而可能引起的问题。
如果在 CGO 处理的跨语言函数调用时涉及到了指针的传递,则可能会出现 Go 语言和 C 语言共享某一段内存的场景。
我们知道 C 语言的内存在分配之后就是稳定的,但是 Go 语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是 Go 和 C 内存模型的最大差异)。如果 C 语言持有的是移动之前的 Go 指针,那么以旧指针访问 Go 对象时会导致程序崩溃。
使用 C 静态库实现
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 等)、监听订单消息等工作
C++ 代码主要实现
#includeextern "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); } Go 代码主要实现
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编程”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注golang学习网行业资讯频道。
文中关于golang的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《Go语言怎么实现CGO编程》文章吧,也可关注golang学习网公众号了解相关技术文章。
-
185 收藏
-
460 收藏
-
196 收藏
-
430 收藏
-
450 收藏
-
108 收藏
-
367 收藏
-
419 收藏
-
234 收藏
-
155 收藏
-
457 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习