Go 专家(11):cgo 深入 —— 调用 C 代码、类型转换、内存分配、编译链接
更新时间:2026-09-01。本文是
languages/go/expert/专家层第 11 篇,接 unsafe.Pointer 与 uintptr。cgo 是 Go 调用 C 代码的机制,类型转换、内存分配、编译链接都有讲究。理解 cgo,才能安全高效地调用 C 库。
本文要回答的问题
- 怎么用 cgo 调用 C 函数?基本结构是什么?
- Go 类型和 C 类型怎么转换?int、char*、struct 分别怎么转?
- C 分配的内存怎么释放?Go 的内存 C 能用吗?
- 性能开销多大?什么时候能用 cgo?什么时候不要用?
一、基本结构
package main
/*
#cgo CFLAGS: -I./include
#cgo LDFLAGS: -L./lib -lmylib
#include <stdlib.h>
#include "mylib.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// 调用 C 函数
ret := C.add(C.int(1), C.int(2))
fmt.Println(int(ret))
}#cgo CFLAGS:C 编译器选项(头文件路径)#cgo LDFLAGS:链接器选项(库路径、库名)import "C":引入 C 包
二、类型转换
| Go 类型 | C 类型 | 转换方式 |
|---|---|---|
int → | C.int | C.int(g) |
int32 → | C.int32_t | C.int32(g) |
string → | C.char* | cs := C.CString(g),用完 C.free(unsafe.Pointer(cs)) |
C.char* → | string | goString := C.GoString(cs) |
[]byte → | C.char* | cs := C.CBytes(b) → C.free |
C.char* → | []byte | gob := C.GoBytes(cs, C.int(len)) |
示例:
// Go string → C char*
gostr := "hello"
cstr := C.CString(gostr)
defer C.free(unsafe.Pointer(cstr))
C.myfunc(cstr)
// C char* → Go string
ret := C.get_string()
gostr = C.GoString(ret)struct 转换: C 的 struct 在 Go 里就是 C.struct_xxx:
// C 定义:
// typedef struct { int a; float b; } Foo;
cfoo := C.Foo{a: C.int(1), b: C.float(1.23)}
gfoo := (*C.Foo)(unsafe.Pointer(&cfoo))Go 的 struct 传给 C 要对齐正确,和 C 定义结构一致。
三、内存分配:C 和 Go
C 分配内存,Go 要用
cbuf := C.malloc(C.size_t(1024))
if cbuf == nil {
panic("malloc failed")
}
defer C.free(cbuf) // C 分配必须 C 释放!规则:
- C 分配 → C 释放:
C.free - Go 分配 → Go 释放:
runtimeGC
常见坑:C 分配忘记释放 → 内存泄漏。
Go 分配内存,C 能用吗?
Go 分配给 C,没问题,但:
- 不要把 Go 指针传给 C,存起来,GC 会移动地址
- C 也不能修改 Go 内存,Go 不需要也不期望
- 最好让 C 自己分配内存
四、C 回调 Go
C 库可能需要回调 Go 函数,注册的时候传函数指针:
// 定义 Go 回调
export void callback(int arg) {
fmt.Printf("callback got: %d\n", arg)
}
// 在 C 里注册:
// callback_register(callback);Go 需要用 //export xxx 导出函数给 C,cgo 会生成包装代码。
五、编译链接
cgo 编译过程:
- Go 代码中的
import "C"触发 cgo - C 代码由系统 C 编译器编译(gcc 或 clang)
- 链接器把 Go 目标文件和 C 目标文件链接成一个二进制
- 生成最终的可执行文件
交叉编译 cgo: 麻烦,需要跨工具链,所以纯 Go 项目开 CGO_ENABLED=0。
六、性能开销
每次 cgo 调用都有 用户态上下文切换:
- Go goroutine → OS 线程 → 调用 C → 返回
- 大约开销比纯 Go 函数多 ~100-1000 ns
- 如果调用频繁,开销占比很大
经验: 如果只是偶尔调用一次,没问题;如果每秒调用百万次,瓶颈就是 cgo。
七、什么时候用,什么时候不用
适合用 cgo:
- 成熟的 C 库,重写成本太高(比如 zlib、openssl、sqlite)
- 底层硬件交互,纯 Go 很难做
- 性能要求比 Go 还要高,关键算法 C 写
不适合用 cgo:
- 只是为了调用一个简单功能,纯 Go 实现简单
- 需要跨平台交叉编译
- 对 Go 的 GC 友好性要求高
- 想保持纯 Go 二进制
Go 社区原则:尽量纯 Go,不依赖 cgo。纯 Go 交叉编译简单,部署简单,性能稳定。
八、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| C 分配内存忘记释放 | 内存泄漏 | 每次 C.CString / C.malloc 后 defer C.free |
| 把 Go 指针传给 C 存起来 | GC 移动后 C 拿到野指针 | 不要这么做,让 C 自己分配 |
| 交叉编译不设置 CGO_ENABLED=0 | 编译报错 | 纯 Go 项目默认开 CGO_ENABLED=0 |
| 大量调用 cgo | 性能下降很多 | 能纯 Go 就纯 Go,减少 cgo 调用次数 |
相关与延伸
下一篇:Go vs C 内存模型对比 —— 手动 vs 自动、缓存友好性、性能对比;cgo 用到 unsafe 转换,见 unsafe.Pointer。
一句话总结
cgo 深入:基本结构是 /* #cgo CFLAGS / LDFLAGS ... */ 然后 import "C";Go 类型转 C 类型:基本类型直接 cast,string 用 C.CString,必须 C.free;C 分配内存必须 C 释放,Go 分配内存 C 最好不要存指针;每次调用有上下文切换开销,频繁调用性能比纯 Go 差;成熟 C 库可以用,否则尽量纯 Go;交叉编译 cgo 需要跨工具链,麻烦。