快速上手 CGO 程序
真实的 CGO 程序原理一般都比较复杂,但是在使用层面上来说,其实没有想象的那么难。
今天我们可以由浅入深来看看一个 CGO 程序该是怎么样实现的?
如果要构造一个简单的 CGO 程序,首先要忽视一些复杂的 CGO 特性,下面我们来快速上手一个 CGO 程序。
基于 C 标准库实现最简单的 CGO 程序
下面是我们构建的最简 CGO 程序:
基于自己写的 C 函数构建 CGO 程序
上面就是使用了C标准库中已有的函数来实现的一个简单的 CGO 程序。
下面我们再来看个例子。先自定义一个叫 SayHello 的 C 函数来实现打印,然后从 Go 语言环境中调用这个 SayHello 函数:
除了 SayHello 函数是我们自己实现的之外,其它的部分和前面的例子基本相似。
我们也可以将 SayHello 函数放到当前目录下的一个 C 语言源文件中(后缀名必须是.c)。因为是编写在独立的 C 文件中,为了允许外部引用,所以需要去掉函数的 static 修饰符。
然后在 CGO 部分先声明 SayHello 函数,其它部分不变:
模块化以上例子
在编程过程中,抽象和模块化是将复杂问题简化的通用手段。当代码语句变多时,我们可以将相似的代码封装到一个个函数中;当程序中的函数变多时,我们将函数拆分到不同的文件或模块中。
在前面的例子中,我们可以抽象一个名为 hello 的模块,模块的全部接口函数都声明在 hello.h 头文件中:
下面是 SayHello 函数的 C 语言实现,对应 hello.c 文件:
我们也可以用 C++语言来重新实现这个 C 语言函数:
用 Go 实现 C 函数并导出
其实 CGO 不仅仅用于 Go 语言中调用 C 语言函数,还可以用于导出 Go 语言函数给 C 语言函数调用。
在前面的例子中,我们已经抽象一个名为 hello 的模块,模块的全部接口函数都在 hello.h 头文件中定义:
现在我们创建一个 hello.go 文件,用 Go 语言重新实现 C 语言接口的 SayHello 函数:
//export SayHelloSayHelloconst
通过面向 C 语言接口的编程技术,我们不仅仅解放了函数的实现者,同时也简化的函数的使用者。现在我们可以将 SayHello 当作一个标准库的函数使用,如下:
用 C 接口的方式实现 Go 编程
简单来说就是将上面例子中的几个文件重新合并到一个 Go 文件实现,如下:
虽然看起来全部是 Go 语言代码,但是执行的时候是先从 Go 语言的 main 函数,到 CGO 自动生成的 C 语言版本 SayHello 桥接函数,最后又回到了 Go 语言环境的 SayHello 函数。这个代码包含了 CGO 编程的精华。
CGO 的主要基础参数
import "C" 语句说明
import "C"
#cgo 语句说明
import "C"#cgo
比如:
CFLAGS-D-ILDFLAGS-L-linet_addr
因为 C/C++遗留的问题,C 头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。
为什么要引入 CGO
突破 Go 创建切片的内存限制
由于 Go 语言实现的限制,我们无法在 Go 语言中创建大于 2GB 内存的切片(可参考 makeslice 实现源码)。不过借助 cgo 技术,我们可以在 C 语言环境创建大于 2GB 的内存,然后转为 Go 语言的切片使用:
例子中我们通过 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"
LDFLAGS
具体实现
如果 CGO 中引入的 C/C++资源有代码而且代码规模也比较小,直接使用源码是最理想的方式,但很多时候我们并没有源代码,或者从 C/C++源代码开始构建的过程异常复杂,这种时候使用 C 静态库也是一个不错的选择。
静态库因为是静态链接,最终的目标程序并不会产生额外的运行时依赖,也不会出现动态库特有的跨运行时资源管理的错误。
我们先用纯 C 语言构造一个简单的静态库。我们要构造的静态库名叫 sum,库中只有一个 sum_add 函数,用于表示数论中的模加法运算。sum 库的文件都在 sum 目录下。
sum/sum.h 头文件只有一个纯 C 语言风格的函数声明:
sum/sum.c 对应函数的实现:
通过以下命令可以生成一个叫 libsum.a 的静态库:
生成 libsum.a 静态库之后,放到当前的lib目录下,我们就可以在 CGO 中使用该资源了。
创建 main.go 文件如下:
#cgo
-I./sum
-L./lib-lsum
需要注意的是,在链接部分的检索路径不能使用相对路径(C/C++代码的链接程序所限制)
实战应用
这里以一个实际案例(分两块代码)来说明 CGO 如何使用静态库的。案例实现的功能说明:
- c++ 代码实现初始化配置、解析传入的 mq 消息,并处理具体的逻辑
- go 代码实现初始化相关配置(mq 等)、监听订单消息等工作
C++ 代码主要实现
Go 代码主要实现
main 函数实现:
init 函数实现:
run 函数代码:
总结
通过以上实例说明,可以知道CGO其实是C语言和Go语言混合编程的技术,因此要想熟练地使用CGO是非常有必要要了解这两门语言的。
任何技术和语言都有它自身的优点和不足,Go语言不是银弹,它无法解决全部问题。而通过CGO可以做到以下几点:
- 通过CGO可接入C/C++的世纪软件遗产
- 通过CGO可以用Go给其它系统写C接口的共享库
- 通过CGO技术也可以让Go语言编写的代码可以很好地融入现有的软件生态
而现在的软件确实大多数是建立在C/C++语言之上的。因此CGO可以说是一个统筹兼备的技术,是Go的一个重量级的技术,也是值得任何一个Go语言开发人员学习的。