golang —— 语言交互性
为了能够重用已有的 C
语言库,我们在使用 Golang
开发项目或系统的时候难免会遇到 Go
和 C语言
混合编程,这时很多人都会选择使用 cgo
。
1 Cgo
话说 cgo
这个东西可算得上是让人又爱又恨,好处在于它可以让你快速重用已有的 C语言
库,无需再用 Gg
重造一遍轮子,而 坏处就在于它会在一定程度上削弱你的系统性能。关于cgo
的种种劣迹,Dave Cheney
大神在他的博客上有一篇专门的文章《cgo is not Go》。
但话说回来,有时候为了快速开发满足项目需求,使用 cgo
也实在是不得已而为之。
官方示例:
package main
import "fmt"
/*
#include <stdlib.h>
*/
import "C"
func Random() int {
return int(C.random())
}
func Seed(i int) {
C.srandom(C.uint(i))
}
func main() {
Seed(100)
fmt.Println("Random:", Random())
}
原理:
import "C"
是一个标记,告诉 Cgo
他应该开始工作了。Cgo
做的事情就是,对应这条 import
语句之前的块注释中的 C
源代码自动生成包装性质的 Go
代码。函数调用从汇编角度看,就是一个将参数按顺序压栈( push
),然后进行函数调用( call
)的过程。Cgo
的代码只不过是帮你封装了这个压栈和调用的过程,从外面看起来就是一个普通的 Go
调用。
语法:
import"C"
前面需要紧贴在注释的下面。块注释还是行注释无所谓。注释中的任意合法的 C
源代码,Cgo
都给会进行相应的处理并生成对应的 Go
代码,如下所示:
package hello
/*
#include <stdlib.h>
void hello() {
printf("Hello, Cgo! -- Frin C world.\n");
}
*/
import "C"
func Hello() int {
C.hello()
}
2 关键难点
跨语言交互中,比较复杂的问题有两个:类型映射 以及 **跨越调用边界传递指针 **所带来的对象生命周期和内存管理的问题。
2.1 类型映射
基础数据类型
C语言 | Go语言 |
---|---|
signed char | C.char / C.schar |
unsigned char | C.uchar |
unsigned short | C.short / C.ushort |
unsigned int | C.int / C.uint |
unsigned long | C.long / C.ulong |
long long | C.longlong |
unsigned long long | C.ulonglong |
float | C.float |
double | C.double |
void*(指针) | unsafe.Pointer |
数据结构类型
C语言 | Go语言 |
---|---|
struct | struct_(前缀) |
union | union_(前缀) |
enum | enum_(前缀) |
2.2 字符串映射
Cgo
提供了一系列函数来提供支持:C.CString
、C.GoString
、C.GoStringN
。
注意:
- 每次转换都将导致一次内存复制
- 无论是
C
语言还是Go
语言都不允许对字符串的内容进行修改
内存问题
由于 C.CString
的内存管理方式与 Go
语言自身的内存管理方式不兼容,所以再使用完之后,必需要现实的释放调用 C.CSting
所产生的内存快,否则将会造成内存泄漏。示例如下:
2.3 函数调用
对于常规返回了一个值的函数,调用者可以用以下方式,顺便得到错误码:
n, err := C.atoi("1234");
在传递数组类型的参数时,在 Go
中,将第一个元素的地址作为整个数组的起始地址传入,示例如下:
n, err := C.func(&array[0]); // 需要显示指定第一个元素的地址
2.4 C代码中依赖第三方库
当需要依赖非 C
标准的第三方库时,Cgo
提供了 #cgo
这样的伪 C
文法,让开发者有机会指定依赖的第三方库和编译选项。示例如下:
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo linux CFLAGS: -DLINUX=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"
以上代码中示范了如何通过 CFLAGS
来传入编译选项,使用 LDFLAGS
来传入链接选项。更简便的 #cgo
用法如下所示:
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"
在 Go
中使用 cgo
调用 C
库的时候,如果需要引用很多不同的第三方库,那么使用 #cgo CFLAGS:
和 #cgo LDFLAGS:
的方式会引入很多行代码。首先这会导致代码很丑陋,最重要的是如果引用的不是标准库,头文件路径和库文件路径写死的话就会很麻烦,一旦第三方库的安装路径变化了,Go
的代码也要跟着变化。所以使用 pkg-config
无疑是一种更为优雅的方法,不管库的安装路径有何变化,都不需要修改 Go
代码。
3 编译 Cgo
Go
安装后,自带一个 cgo
命令行工具,它用于处理所有带有 Cgo
代码的 Go
文件,生成 Go
语言版本的调用封装代码。而 Go
工具集又对 cgo
再次进行了良好的封装,所以我们不需要做任何特殊的处理。