Go函数不能直接作为C函数指针使用,因其调用约定、栈管理和GC机制与C不兼容;必须通过#cgo export声明导出符合C签名的包级函数,并借助uintptr ID映射机制注册/注销任意Go函数(含闭包),配合显式生命周期管理确保安全。

Go函数不能直接当C函数指针用
Go函数的调用约定、栈管理、GC机制和C完全不同,C.func 无法直接指向 func(int) void 这类Go函数。试图强制转换(如 (*C.callback_t)(unsafe.Pointer(&myGoFunc)))会导致崩溃或未定义行为——这不是类型转换问题,是运行时模型冲突。
必须用#cgo export声明+固定签名包装
cgo只允许通过 //export 注释暴露给C的函数,且必须满足:无参数或仅含C兼容类型(C.int, *C.char, C.size_t 等),返回值也必须是C类型,且不能有Go runtime依赖(比如不能调用fmt.Println或分配堆内存)。
- 在
/* #include ... */块之后、Go代码之前写://export go_callback_handler func go_callback_handler(val C.int) { // 这里只能做轻量操作:存入全局channel、设标志位、或调用已注册的Go闭包(见下节) } - 导出函数名必须与C头文件中声明的函数类型签名完全一致,包括参数名(即使C端不使用)
- 导出函数不能是方法,不能带接收者,不能是闭包,必须是包级函数
如何把任意Go函数(含闭包/捕获变量)传给C回调
核心思路是「注册-查表-跳转」:先在Go侧维护一个map[uintptr]func(...),把Go函数转为uintptr存起来;C调用导出函数时,用传入的ID查表并执行对应Go逻辑。
- 定义注册函数:
var callbacks = make(map[uintptr]func(int)) var cbCounter uintptr = 1 <p>func RegisterCallback(f func(int)) uintptr { ptr := cbCounter cbCounter++ callbacks[ptr] = f return ptr } - 导出函数中查表执行:
//export go_callback_handler func go_callback_handler(id C.uintptr_t, val C.int) { if cb, ok := callbacks[uintptr(id)]; ok { cb(int(val)) } } - C侧调用时传入注册返回的
uintptr作为上下文ID,而不是函数指针本身 - 注意:map访问非并发安全,多线程C回调需加
sync.RWMutex保护
释放注册回调时务必清理map防止内存泄漏
每次RegisterCallback都会在map中驻留一个Go函数引用,若C库长期持有该ID并反复触发回调,而Go侧又没主动注销,就会导致函数及其捕获变量永远无法被GC回收。
立即学习“go语言免费学习笔记(深入)”;
- 提供注销函数:
func UnregisterCallback(id uintptr) { delete(callbacks, id) } - 在C完成使用后(比如
lib_destroy()调用前),从Go侧主动调用UnregisterCallback - 如果C库不提供注销时机,可考虑用
runtime.SetFinalizer做兜底,但不可依赖——Finalizer执行时机不确定,且可能根本不会触发 - 更稳妥的做法是:把回调生命周期绑定到某个Go对象(如结构体)上,在其
Close()方法里统一注销
真正麻烦的不是导出函数,而是让Go函数能安全、可控、可销毁地参与C回调链——ID映射+显式生命周期管理,缺一不可。


















