本文是全系列中第13 / 62篇:Go语言高级编程
- Go语言高级编程-序言致谢
- Go语言高级编程-Advanced Go Programming
- Go语言高级编程-第 1 章 语言基础
- Go语言高级编程-1.1 Go 语言创世纪
- Go语言高级编程-1.2 Hello, World 的革命
- Go语言高级编程-1.3 数组、字符串和切片
- Go语言高级编程-1.4 函数、方法和接口
- Go语言高级编程-1.5 面向并发的内存模型
- Go语言高级编程-1.6 常见的并发模式
- Go语言高级编程-1.7 错误和异常
- Go语言高级编程-第 2 章 CGO 编程
- Go语言高级编程-2.1 快速入门
- Go语言高级编程-2.2 CGO 基础
- Go语言高级编程-2.3 类型转换
- Go语言高级编程-2.4 函数调用
- Go语言高级编程-2.5 内部机制
- Go语言高级编程-2.6 实战: 封装 qsort
- Go语言高级编程-2.7 CGO 内存模型
- Go语言高级编程-2.8 C++ 类包装
- Go语言高级编程-2.9 静态库和动态库
- Go语言高级编程-2.10 编译和链接参数
- Go语言高级编程-第 3 章 Go 汇编语言
- Go语言高级编程-3.1 快速入门
- Go语言高级编程-3.2 计算机结构
- Go语言高级编程-3.3 常量和全局变量
- Go语言高级编程-3.4 函数
- Go语言高级编程-3.5 控制流
- Go语言高级编程-3.6 再论函数
- Go语言高级编程-3.7 汇编语言的威力
- Go语言高级编程-3.8 例子:Goroutine ID
- Go语言高级编程-3.9 Delve 调试器
- Go语言高级编程-第 4 章 RPC 和 Protobuf
- Go语言高级编程-4.1 RPC 入门
- Go语言高级编程-4.2 Protobuf
- Go语言高级编程-4.3 玩转 RPC
- Go语言高级编程-4.4 gRPC 入门
- Go语言高级编程-4.5 gRPC 进阶
- Go语言高级编程-4.6 gRPC 和 Protobuf 扩展
- Go语言高级编程-4.7 pbgo: 基于 Protobuf 的框架
- Go语言高级编程-4.8 grpcurl 工具
- Go语言高级编程-第 5 章 go 和 Web
- Go语言高级编程-5.1 Web 开发简介
- Go语言高级编程-5.2 router 请求路由
- Go语言高级编程-5.3 中间件
- Go语言高级编程-5.4 validator 请求校验
- Go语言高级编程-5.5 Database 和数据库打交道
- Go语言高级编程-5.6 Ratelimit 服务流量限制
- Go语言高级编程-5.7 layout 常见大型 Web 项目分层
- Go语言高级编程-5.8 接口和表驱动开发
- Go语言高级编程-5.9 灰度发布和 A/B test
- Go语言高级编程-5.10 补充说明
- Go语言高级编程-第 6 章 分布式系统
- Go语言高级编程-6.1 分布式 id 生成器
- Go语言高级编程-6.2 分布式锁
- Go语言高级编程-6.3 延时任务系统
- Go语言高级编程-6.4 分布式搜索引擎
- Go语言高级编程-6.5 负载均衡
- Go语言高级编程-6.6 分布式配置管理
- Go语言高级编程-6.7 分布式爬虫
- Go语言高级编程-6.8 补充说明
- Go语言高级编程-附录B:有趣的代码片段
- Go语言高级编程-附录A:Go语言常见坑
要使用 CGO 特性,需要安装 C/C++ 构建工具链,在 macOS 和 Linux 下是要安装 GCC,在 windows 下是需要安装 MinGW 工具。同时需要保证环境变量 CGO_ENABLED
被设置为 1,这表示 CGO 是被启用的状态。在本地构建时 CGO_ENABLED
默认是启用的,当交叉构建时 CGO 默认是禁止的。比如要交叉构建 ARM 环境运行的 Go 程序,需要手工设置好 C/C++ 交叉构建的工具链,同时开启 CGO_ENABLED
环境变量。然后通过 import "C"
语句启用 CGO 特性。
2.2.1 import "C"
语句
如果在 Go 代码中出现了 import "C"
语句则表示使用了 CGO 特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的 C 语言代码。当确保 CGO 启用的情况下,还可以在当前目录中包含 C/C++ 对应的源文件。
举个最简单的例子:
package main
/*
#include
void printint(int v) {
printf("printint: %dn", v);
}
*/
import "C"
func main() {
v := 42
C.printint(C.int(v))
}
这个例子展示了 cgo 的基本使用方法。开头的注释中写了要调用的 C 函数和相关的头文件,头文件被 include 之后里面的所有的 C 语言元素都会被加入到”C” 这个虚拟的包中。需要注意的是,import "C" 导入语句需要单独一行,不能与其他包一同 import。向 C 函数传递参数也很简单,就直接转化成对应 C 语言类型传递就可以。如上例中 C.int(v)
用于将一个 Go 中的 int 类型值强制类型转换转化为 C 语言中的 int 类型值,然后调用 C 语言定义的 printint 函数进行打印。
需要注意的是,Go 是强类型语言,所以 cgo 中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C” 中的转化函数转换成对应的 C 类型,不能直接传入 Go 中类型的变量。同时通过虚拟的 C 包导入的 C 语言符号并不需要是大写字母开头,它们不受 Go 语言的导出规则约束。
cgo 将当前包引用的 C 语言符号都放到了虚拟的 C 包中,同时当前包依赖的其它 Go 语言包内部可能也通过 cgo 引入了相似的虚拟 C 包,但是不同的 Go 语言包引入的虚拟的 C 包之间的类型是不能通用的。这个约束对于要自己构造一些 cgo 辅助函数时有可能会造成一点的影响。
比如我们希望在 Go 中定义一个 C 语言字符指针对应的 CChar 类型,然后增加一个 GoString 方法返回 Go 语言字符串:
package cgo_helper
//#include
import "C"
type CChar C.char
func (p *CChar) GoString() string {
return C.GoString((*C.char)(p))
}
func PrintCString(cs *C.char) {
C.puts(cs)
}
现在我们可能会想在其它的 Go 语言包中也使用这个辅助函数:
package main
//static const char* cs = "hello";
import "C"
import "./cgo_helper"
func main() {
cgo_helper.PrintCString(C.cs)
}
这段代码是不能正常工作的,因为当前 main 包引入的 C.cs
变量的类型是当前 main
包的 cgo 构造的虚拟的 C 包下的 *char
类型(具体点是 *C.char
,更具体点是 *main.C.char
),它和 cgo_helper 包引入的 *C.char
类型(具体点是 *cgo_helper.C.char
)是不同的。在 Go 语言中方法是依附于类型存在的,不同 Go 包中引入的虚拟的 C 包的类型却是不同的(main.C
不等 cgo_helper.C
),这导致从它们延伸出来的 Go 类型也是不同的类型(*main.C.char
不等 *cgo_helper.C.char
),这最终导致了前面代码不能正常工作。
有 Go 语言使用经验的用户可能会建议参数转型后再传入。但是这个方法似乎也是不可行的,因为 cgo_helper.PrintCString
的参数是它自身包引入的 *C.char
类型,在外部是无法直接获取这个类型的。换言之,一个包如果在公开的接口中直接使用了 *C.char
等类似的虚拟 C 包的类型,其它的 Go 包是无法直接使用这些类型的,除非这个 Go 包同时也提供了 *C.char
类型的构造函数。因为这些诸多因素,如果想在 go test 环境直接测试这些 cgo 导出的类型也会有相同的限制。
2.2.2 #cgo
语句
在 import "C"
语句前的注释中可以通过 #cgo
语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include
import "C"
上面的代码中,CFLAGS 部分,-D
部分定义了宏 PNG_DEBUG,值为 1;-I
定义了头文件包含的检索目录。LDFLAGS 部分,-L
指定了链接时库文件检索目录,-l
指定了链接时需要链接 png 库。
因为 C/C++ 遗留的问题,C 头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。在库文件的检索目录中可以通过 ${SRCDIR}
变量表示当前包目录的绝对路径:
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
上面的代码在链接时将被展开为:
// #cgo LDFLAGS: -L/go/src/foo/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++ 语言的区别,它们的目标文件的类型是相同的。
#cgo
指令还支持条件选择,当满足某个操作系统或某个 CPU 架构类型时后面的编译或链接选项生效。比如下面是分别针对 windows 和非 windows 下平台的编译和链接选项:
// #cgo windows CFLAGS: -DX86=1
// #cgo !windows LDFLAGS: -lm
其中在 windows 平台下,编译前会预定义 X86 宏为 1;在非 windows 平台下,在链接阶段会要求链接 math 数学库。这种用法对于在不同平台下只有少数编译选项差异的场景比较适用。
如果在不同的系统下 cgo 对应着不同的 c 代码,我们可以先使用 #cgo
指令定义不同的 C 语言的宏,然后通过宏来区分不同的代码:
package main
/*
#cgo windows CFLAGS: -DCGO_OS_WINDOWS=1
#cgo darwin CFLAGS: -DCGO_OS_DARWIN=1
#cgo linux CFLAGS: -DCGO_OS_LINUX=1
#if defined(CGO_OS_WINDOWS)
const char* os = "windows";
#elif defined(CGO_OS_DARWIN)
const char* os = "darwin";
#elif defined(CGO_OS_LINUX)
const char* os = "linux";
#else
# error(unknown os)
#endif
*/
import "C"
func main() {
print(C.GoString(C.os))
}
这样我们就可以用 C 语言中常用的技术来处理不同平台之间的差异代码。
2.2.3 build tag 条件编译
build tag 是在 Go 或 cgo 环境下的 C/C++ 文件开头的一种特殊的注释。条件编译类似于前面通过 #cgo
指令针对不同平台定义的宏,只有在对应平台的宏被定义之后才会构建对应的代码。但是通过 #cgo
指令定义宏有个限制,它只能是基于 Go 语言支持的 windows、darwin 和 linux 等已经支持的操作系统。如果我们希望定义一个 DEBUG 标志的宏,#cgo
指令就无能为力了。而 Go 语言提供的 build tag 条件编译特性则可以简单做到。
比如下面的源文件只有在设置 debug 构建标志时才会被构建:
// +build debug
package main
var buildMode = "debug"
可以用以下命令构建:
go build -tags="debug"
go build -tags="windows debug"
我们可以通过 -tags
命令行参数同时指定多个 build 标志,它们之间用空格分隔。
当有多个 build tag 时,我们将多个标志通过逻辑操作的规则来组合使用。比如以下的构建标志表示只有在”linux/386“或”darwin 平台下非 cgo 环境 “才进行构建。
// +build linux,386 darwin,!cgo
其中 linux,386
中 linux 和 386 用逗号链接表示 AND 的意思;而 linux,386
和 darwin,!cgo
之间通过空白分割来表示 OR 的意思。
本文是全系列中第13 / 62篇:Go语言高级编程
- Go语言高级编程-序言致谢
- Go语言高级编程-Advanced Go Programming
- Go语言高级编程-第 1 章 语言基础
- Go语言高级编程-1.1 Go 语言创世纪
- Go语言高级编程-1.2 Hello, World 的革命
- Go语言高级编程-1.3 数组、字符串和切片
- Go语言高级编程-1.4 函数、方法和接口
- Go语言高级编程-1.5 面向并发的内存模型
- Go语言高级编程-1.6 常见的并发模式
- Go语言高级编程-1.7 错误和异常
- Go语言高级编程-第 2 章 CGO 编程
- Go语言高级编程-2.1 快速入门
- Go语言高级编程-2.2 CGO 基础
- Go语言高级编程-2.3 类型转换
- Go语言高级编程-2.4 函数调用
- Go语言高级编程-2.5 内部机制
- Go语言高级编程-2.6 实战: 封装 qsort
- Go语言高级编程-2.7 CGO 内存模型
- Go语言高级编程-2.8 C++ 类包装
- Go语言高级编程-2.9 静态库和动态库
- Go语言高级编程-2.10 编译和链接参数
- Go语言高级编程-第 3 章 Go 汇编语言
- Go语言高级编程-3.1 快速入门
- Go语言高级编程-3.2 计算机结构
- Go语言高级编程-3.3 常量和全局变量
- Go语言高级编程-3.4 函数
- Go语言高级编程-3.5 控制流
- Go语言高级编程-3.6 再论函数
- Go语言高级编程-3.7 汇编语言的威力
- Go语言高级编程-3.8 例子:Goroutine ID
- Go语言高级编程-3.9 Delve 调试器
- Go语言高级编程-第 4 章 RPC 和 Protobuf
- Go语言高级编程-4.1 RPC 入门
- Go语言高级编程-4.2 Protobuf
- Go语言高级编程-4.3 玩转 RPC
- Go语言高级编程-4.4 gRPC 入门
- Go语言高级编程-4.5 gRPC 进阶
- Go语言高级编程-4.6 gRPC 和 Protobuf 扩展
- Go语言高级编程-4.7 pbgo: 基于 Protobuf 的框架
- Go语言高级编程-4.8 grpcurl 工具
- Go语言高级编程-第 5 章 go 和 Web
- Go语言高级编程-5.1 Web 开发简介
- Go语言高级编程-5.2 router 请求路由
- Go语言高级编程-5.3 中间件
- Go语言高级编程-5.4 validator 请求校验
- Go语言高级编程-5.5 Database 和数据库打交道
- Go语言高级编程-5.6 Ratelimit 服务流量限制
- Go语言高级编程-5.7 layout 常见大型 Web 项目分层
- Go语言高级编程-5.8 接口和表驱动开发
- Go语言高级编程-5.9 灰度发布和 A/B test
- Go语言高级编程-5.10 补充说明
- Go语言高级编程-第 6 章 分布式系统
- Go语言高级编程-6.1 分布式 id 生成器
- Go语言高级编程-6.2 分布式锁
- Go语言高级编程-6.3 延时任务系统
- Go语言高级编程-6.4 分布式搜索引擎
- Go语言高级编程-6.5 负载均衡
- Go语言高级编程-6.6 分布式配置管理
- Go语言高级编程-6.7 分布式爬虫
- Go语言高级编程-6.8 补充说明
- Go语言高级编程-附录B:有趣的代码片段
- Go语言高级编程-附录A:Go语言常见坑