1.1 最简CGO程序
// hello.go
package main
import "C"
func main() {
    println("hello cgo")
}
- 代码通过
import "C"语句启用CGO特性,主函数只是通过Go内置的println函数输出字符串,其中并没有任何和CGO相关的代码。 - 虽然没有调用CGO的相关函数,但是
go build命令会在编译和链接阶段启动gcc编译器,这已经是一个完整的CGO程序了。 
1.2 基于C标准库函数输出字符串
// hello.go
package main
//#include <stdio.h>
import "C"
func main() {
    C.puts(C.CString("Hello, World\n"))
}
- 通过
import "C"语句启用CGO特性, - 同时包含C语言的
<stdio.h>头文件。 - 然后通过CGO包的
C.CString函数将Go语言字符串转为C语言字符串, - 最后调用CGO包的
C.puts函数向标准输出窗口打印转换后的C字符串。 - 我们没有在程序退出前释放
C.CString创建的C语言字符串; - 还有我们改用
puts函数直接向标准输出打印,之前是采用fputs向标准输出打印。 - 没有释放使用
C.CString创建的C语言字符串会导致内存泄漏。 
1.3 使用自己的C函数
// 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的C函数来实现打印,然后从Go语言环境中调用这个SayHello函数 - 可以将
SayHello函数放到当前目录下的一个C语言源文件中(后缀名必须是.c)。因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static修饰符。 - 然后在CGO部分先声明
SayHello函数,其它部分不变: - 如果是以静态库或动态库方式引用
SayHello函数的话,需要将对应的C源文件移出当前目录(CGO构建程序会自动构建当前目录下的C源文件,从而导致C函数名冲突) 
1.4 用Go重新实现C函数
CGO不仅仅用于Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。
// 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修饰符。
 - 需要注意的是,这里其实有两个版本的
SayHello函数:一个Go语言环境的;另一个是C语言环境的。 - cgo生成的C语言版本SayHello函数最终会通过桥接代码调用Go语言版本的SayHello函数。
 
2.1 CGO基础
- 在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。
 - 同时需要保证环境变量
CGO_ENABLED被设置为1,这表示CGO是被启用的状态。 
2.2import "C"语句
- Go代码中出现了
import "C"语句则表示使用了CGO特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。 
package main
/*
#include <stdio.h>
void printint(int v) {
    printf("printint: %d\n", v);
}
*/
import "C"
func main() {
    v := 42
    C.printint(C.int(v))
}
- 开头的注释中写了要调用的C函数和相关的头文件,头文件被include之后里面的所有的C语言元素都会被加入到”C”这个虚拟的包中。
 - 需要注意的是,import “C"导入语句需要单独一行,不能与其他包一同import。
 - 向C函数传递参数也很简单,就直接转化成对应C语言类型传递就可以。
 - Go是强类型语言,所以cgo中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C”中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量。
 - 同时通过虚拟的C包导入的C语言符号并不需要是大写字母开头,它们不受Go语言的导出规则约束。
 
2.2 #cgo语句
- 在
import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。 - 编译阶段的参数主要用于定义相关宏和指定头文件检索路径。
 - 链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
 
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"
- 
CFLAGS部分,
-D部分定义了宏PNG_DEBUG,值为1; - 
-I定义了头文件包含的检索目录。LDFLAGS部分 - 
-L指定了链接时库文件检索目录, - 
-l指定了链接时需要链接png库。 - 
因为C/C++遗留的问题,C头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。
 - 
在库文件的检索目录中可以通过
${SRCDIR}变量表示当前包目录的绝对路径: 
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
#cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。- LDFLAGS用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。
 - 对于在cgo环境混合使用C和C++的用户来说,可能有三种不同的编译选项:
- CFLAGS对应C语言特有的编译选项、
 - CXXFLAGS对应是C++特有的编译选项、
 - CPPFLAGS则对应C和C++共有的编译选项。
 
 - 但是在链接阶段,C和C++的链接选项是通用的,因此这个时候已经不再有C和C++语言的区别,它们的目标文件的类型是相同的。
 
2.3 build tag 条件编译
#cgo指令针对不同平台定义的宏,只有在对应平台的宏被定义之后才会构建对应的代码。
比如下面的源文件只有在设置debug构建标志时才会被构建:
// +build debug
package main
var buildMode = "debug"
可以用以下命令构建:
go build -tags="debug"
go build -tags="windows debug"
我们可以通过-tags命令行参数同时指定多个build标志,它们之间用空格分隔。
当有多个build tag时,我们将多个标志通过逻辑操作的规则来组合使用。
3.1 类型转换
- Go语言中访问C语言的符号时,一般是通过虚拟的“C”包访问,比如
C.int对应C语言的int类型。 - 有些C语言的类型是由多个关键字组成,但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符,比如
unsigned int不能直接通过C.unsigned int访问。 - 因此CGO为C语言的基础数值类型都提供了相应转换规则,比如
C.uint对应C语言的unsigned int。 
应关系表2-1所示。
| C语言类型 | CGO类型 | Go语言类型 | 
|---|---|---|
| char | C.char | byte | 
| singed char | C.schar | int8 | 
| unsigned char | C.uchar | uint8 | 
| short | C.short | int16 | 
| unsigned short | C.ushort | uint16 | 
| int | C.int | int32 | 
| unsigned int | C.uint | uint32 | 
| long | C.long | int32 | 
| unsigned long | C.ulong | uint32 | 
| long long int | C.longlong | int64 | 
| unsigned long long int | C.ulonglong | uint64 | 
| float | C.float | float32 | 
| double | C.double | float64 | 
| size_t | C.size_t | uint | 
表 2-1 Go语言和C语言类型对比
在<stdint.h>文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。Go语言类型<stdint.h>头文件类型对比如表2-2所示。
| C语言类型 | CGO类型 | Go语言类型 | 
|---|---|---|
| int8_t | C.int8_t | int8 | 
| uint8_t | C.uint8_t | uint8 | 
| int16_t | C.int16_t | int16 | 
| uint16_t | C.uint16_t | uint16 | 
| int32_t | C.int32_t | int32 | 
| uint32_t | C.uint32_t | uint32 | 
| int64_t | C.int64_t | int64 | 
| uint64_t | C.uint64_t | uint64 | 
表 2-2 <stdint.h>类型对比
3.2 结构体、联合、枚举类型
- C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。
 - 在Go语言中,我们可以通过
C.struct_xxx来访问C语言中定义的struct xxx结构体类型。 
/*
struct A {
    int i;
    float f;
};
*/
import "C"
import "fmt"
func main() {
    var a C.struct_A
    fmt.Println(a.i)
    fmt.Println(a.f)
}
/*
struct A {
    int type; // type 是 Go 语言的关键字
};
*/
import "C"
import "fmt"
func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 对应 type
}
- 如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问:
 - 但是如果有2个成员:一个是以Go语言关键字命名,另一个刚好是以下划线和Go语言关键字命名,那么以Go语言关键字命名的成员将无法访问(被屏蔽):
 
/*
struct A {
    int   type;  // type 是 Go 语言的关键字
    float _type; // 将屏蔽CGO对 type 成员的访问
};
*/
import "C"
import "fmt"
func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 对应 _type
}
对应零长数组的成员,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。
/*
struct A {
    int   size: 10; // 位字段无法访问
    float arr[];    // 零长的数组也无法访问
};
*/
import "C"
import "fmt"
func main() {
    var a C.struct_A
    fmt.Println(a.size) // 错误: 位字段无法访问
    fmt.Println(a.arr)  // 错误: 零长的数组也无法访问
}
对于联合类型,我们可以通过C.union_xxx来访问C语言中定义的union xxx类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。
/*
#include <stdint.h>
union B1 {
    int i;
    float f;
};
union B2 {
    int8_t i8;
    int64_t i64;
};
*/
import "C"
import "fmt"
func main() {
    var b1 C.union_B1;
    fmt.Printf("%T\n", b1) // [4]uint8
    var b2 C.union_B2;
    fmt.Printf("%T\n", b2) // [8]uint8
}
对于枚举类型,我们可以通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型。
/*
enum C {
    ONE,
    TWO,
};
*/
import "C"
import "fmt"
func main() {
    var c C.enum_C = C.TWO
    fmt.Println(c)
    fmt.Println(C.ONE)
    fmt.Println(C.TWO)
}
在C语言中,枚举类型底层对应int类型,支持负数类型的值。我们可以通过C.ONE、C.TWO等直接访问定义的枚举值。
4.1 函数调用
4.1.1 C函数的返回值
/*
static int div(int a, int b) {
    return a/b;
}
*/
import "C"
import "fmt"
func main() {
    v := C.div(6, 3)
    fmt.Println(v)
}
因为C语言不支持返回多个结果,因此<errno.h>标准库提供了一个errno宏用于返回错误状态。我们可以近似地将errno看成一个线程安全的全局变量,可以用于记录最近一次错误的状态码。
改进后的div函数实现如下:
#include <errno.h>
int div(int a, int b) {
    if(b == 0) {
        errno = EINVAL;
        return 0;
    }
    return a/b;
}
CGO也针对<errno.h>标准库的errno宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno错误状态。
/*
#include <errno.h>
static int div(int a, int b) {
    if(b == 0) {
        errno = EINVAL;
        return 0;
    }
    return a/b;
}
*/
import "C"
import "fmt"
func main() {
    v0, err0 := C.div(2, 1)
    fmt.Println(v0, err0)
    v1, err1 := C.div(1, 0)
    fmt.Println(v1, err1)
}
运行这个代码将会产生以下输出:
2 <nil>
0 invalid argument
我们可以近似地将div函数看作为以下类型的函数:
func C.div(a, b C.int) (C.int, [error])
5 内部机制
- 
CGO特性主要是通过一个叫cgo的命令行工具来辅助输出Go和C之间的桥接代码。本节我们尝试从生成的代码分析Go语言和C语言函数直接相互调用的流程。
 - 
我们可以在构建一个cgo包时增加一个
-work输出中间生成文件所在的目录并且在构建完成时保留中间文件。 - 
包中有4个Go文件,其中nocgo开头的文件中没有
import "C"指令,其它的2个文件则包含了cgo代码。 - 
cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件,比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。
 - 
然后会为整个包创建一个
_cgo_gotypes.goGo文件,其中包含Go语言部分辅助代码。 - 
此外还会创建一个
_cgo_export.h和_cgo_export.c文件,对应Go语言导出到C语言的类型和函数。 
6.C++ 类包装
- CGO是C语言和Go语言之间的桥梁,原则上无法直接支持C++的类。
 - CGO不支持C++语法的根本原因是C++至今为止还没有一个二进制接口规范(ABI)。
 - 一个C++类的构造函数在编译为目标文件时如何生成链接符号名称、方法在不同平台甚至是C++的不同版本之间都是不一样的。
 - 但是C++是兼容C语言,所以我们可以通过增加一组C语言函数接口作为C++类和CGO之间的桥梁,这样就可以间接地实现C++和Go之间的互联。
 - 当然,因为CGO只支持C语言中值类型的数据类型,所以我们是无法直接使用C++的引用参数等特性的。
 
6.1 C++ 类到 Go 语言对象
- 实现C++类到Go语言对象的包装需要经过以下几个步骤:首先是用纯C函数接口包装该C++类;
 - 其次是通过CGO将纯C函数接口映射到Go函数;
 - 最后是做一个Go包装对象,将C++类到方法用Go对象的方法实现。