Cgo介绍

本文翻译自《C? Go? Cgo!》。

Andrew Gerrand

2011/03/17

介绍

Cgo可以让Go包调用C代码。给定一个使用一些特殊特性编写的Go源文件,cgo输出Go和C文件,它们可以组合成单个Go包。

举个例子,这里有一个Go包,它提供了两个函数——RandomSeed——用来包装C的random函数和srrandom函数。

package rand

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

让我们看看这里发生了什么,从import语句开始。

rand包导入“C”,但你会发现在标准Go库中没有这个包。这是因为C是一个“伪包(pseudo-package)”,一个被cgo解释为引用C命名空间的特殊名称。

rand包包含对C包的四个引用:对C.randomC.srrandom的调用、C.uint(i)import "C"语句。

Random函数调用标准C库的random函数并返回结果。在C语言中,random返回一个long类型的值,cgo将其表示为C.long类型。必须将其转换为Go类型,才能由该包外的Go代码使用,使用普通的Go类型转换语句即可:

func Random() int {
    return int(C.random())
}

下面是一个等效函数,它使用一个临时变量来更明确地说明这种类型转换:

func Random() int {
    var r C.long = C.random()
    return int(r)
}

Seed函数在某种程度上起相反的作用。它获取一个常规的Go的int类型的变量,将其转换为C的unsigned int类型,并将其传递给C函数srrandom

func Seed(i int) {
    C.srandom(C.uint(i))
}

注意,cgo知道C语言的unsigned int类型用C.uint来表示;有关这些数值类型的名称的完整列表,请参阅cgo文档

我们还没有研究这个例子的一个细节是import语句上方的注释。

/*
#include <stdlib.h>
*/
import "C"

Cgo认识这一注释。任何以#cgo开头、后跟一个空格字符的行都将被删除;这些都成为cgo的指令。在编译Go包的C语言部分代码时,剩余的行用作C语言头部。在上例中,这些行只是一个#include语句,但它们几乎可以是任何C代码。在构建Go包的C语言部分时,#cgo指令用于为编译器和链接器提供标志。

有一个限制:如果你的程序使用任何//export指令,那么注释中的C代码可能只包括这种声明(extern int f();),而不是定义(int f() { return 1; })。你可以使用//export指令使Go函数可被C语言代码访问。

#cgo//export指令记录在cgo文档中。

字符串相关

与Go不同,C没有显式的字符串类型。C中的字符串由一个以零结尾的字符数组表示。

Go和C字符串之间的转换是通过C.CStringC.GoStringC.GoStringN函数完成的。这些转换返回字符串数据的一个拷贝。

下一个示例实现了一个Print函数,该函数使用C的stdio库中的fputs函数将字符串写到标准输出:

package print

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
    cs := C.CString(s)
    C.fputs(cs, (*C.FILE)(C.stdout))
    C.free(unsafe.Pointer(cs))
}

Go的内存管理器看不见由C代码进行的内存分配。因此当你使用C.CString(或任何C内存分配)创建一个C字符串时,你必须记住在完成调用后使用C.free释放内存。

C.CString的调用返回一个指向char数组头部的指针,我们将其转换为一个unsafe.Pointer指针。并用C.free释放它指向的内存分配。cgo程序中的一个常见惯用法是在分配内存后立即defer释放(尤其是当后面的代码比单个函数调用更复杂时),例如以下对Print函数的重写:

func Print(s string) {
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.fputs(cs, (*C.FILE)(C.stdout))
}

构建cgo包

要构建cgo包,只需像往常一样使用go buildgo install即可。这些go工具能识别特殊的“C”导入,并自动使用cgo来处理。

cgo相关资源

cgo命令文档提供了关于C伪包和构建过程的更多详细信息。Go树中的cgo示例演示了更高级的概念。

最后,如果你想知道在内部这一切是如何工作的,请查看运行时包的cgocall.go的介绍性注释。