工作原理
CGO 的编译链路分两段:go build 先用 cgo 工具读入 import "C" 上方注释里的 C 代码,生成 _cgo_gotypes.go、_cgo_export.c 等桩文件;再由 C 编译器编译 C 部分,最后由 Go 工具链把两边链接起来。想看完整步骤可以用 go build -x,输出里能找到 cgo 的调用。
Go 调 C:runtime.cgocall 接管这次调用——当前 M 进入系统调用状态(和阻塞在 syscall 上语义相同),C 代码执行完才回到 Go 栈继续。所以一次跨边界调用比普通函数调用贵,贵在调度器介入和栈的切换。
C 调 Go:在 Go 函数上用 //export 标记,cgo 会为它生成 C 侧的导出桥;C 代码用对应的 extern 声明就能回调。回调跑在 C 发起它的那个线程上,不保证和之前的调用者同一个线程。
性能
- 跨边界调用有固定开销:进入/退出系统调用状态、保存上下文。单次调用不值得优化,热点路径上的做法是把边界挪出循环——一次调用传一批数据。
- 字符串互转有分配成本:
C.CString每次都在 C 堆上 malloc,用完要C.free;批量传数据优先用unsafe.Pointer配长度,不做逐个转换。 - 高频小调用考虑长期方案:把 C 侧改成服务(进程间通信),或把热循环整体移到 C 侧执行。
指针规则(安全与限制)
cgo 对指针的传递有明确规则,违反会在运行时报错:
- Go 指针可以在调用期间作为参数传给 C,但 C 侧不得在调用返回后继续持有它。
- Go 内存里可以存 C 指针;C 内存里不得存 Go 指针。
- 指向 Go 堆的 Go 指针传给 C 时,Go 侧要确保这块内存在那次调用期间不被移动——当前 GC 不移动堆对象,但这条规则写进了语言约定,写
unsafe代码时心里要有数。
检查手段:GODEBUG=cgocheck=1(默认开启,查第 2、3 类违规)和 GODEBUG=cgocheck=2(更严,能查出 C 侧返回后仍用 Go 指针,开销更大,调试期用)。go vet 也会提示常见的 CGO 指针用法问题。
并发
CGO 调用期间 goroutine 阻塞在系统调用上,P 会让给其他 goroutine 用,不占线程。C 代码回调 Go 时,回调 goroutine 在 C 线程上运行,结束后可以被调度到任意 M——不要假设回调和原 goroutine 在同一线程;回调里要锁的共享数据用 sync 包保护。
调试
gdb/lldb能同时看到 Go 栈和 C 栈;用bt看跨边界的完整调用链。go tool nm <二进制>可以核对//export的符号有没有生成。- 排查指针违规先跑
GODEBUG=cgocheck=2的测试,边界上的问题清了再调性能或并发。
交叉阅读:使用指南 讲调用步骤、类型转换和常见坑;命令文档 有指针规则的原文。