go编译.so必须用-buildmode=c-shared,导出函数须在main包中、正上方带//export注释、签名仅含c类型,且需空main函数和cgo_enabled=1。

导出函数必须在 main 包且带 //export 注释
Go 生成 .so 文件时,go build -buildmode=c-shared 只认 main 包里、被 //export 显式标记的函数。这不是可选规范,是硬性限制——其他包里的函数哪怕导出(首字母大写),也不会出现在最终的符号表中。
常见错误现象:undefined reference to 'Add' 或 Python 调用时报 AttributeError: undefined symbol: Add,八成是因为函数没放对包,或漏了 //export 行。
-
//export必须紧贴在函数声明正上方,中间不能有空行 - 函数签名只能含 C 兼容类型:
C.int、*C.char、unsafe.Pointer,不能用string、slice、struct等 Go 原生类型 - 必须保留空的
func main() {},否则编译失败
CGO_ENABLED=1 是前提,且不能设为 0
只要涉及 c-shared 模式,CGO_ENABLED 就必须为 1。设成 0 会导致编译器直接忽略 import "C" 和所有 //export,生成的 .so 里根本没有导出符号,连 nm -D libxxx.so 都看不到函数名。
容易踩的坑:
- 在 CI/CD 脚本里全局设置了
CGO_ENABLED=0(比如为了 Alpine 镜像瘦身),结果本地能编译,CI 却静默失败 - 交叉编译时没同步指定目标平台的
CC,例如GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc缺一不可 -
go run不支持c-shared,只能用go build
内存管理必须由调用方负责,Go 不自动释放 C 字符串
用 C.CString() 返回的 *C.char,其内存由 C 堆分配,Go 的 GC 完全不感知。如果 Python 或 C 侧不调用 free(),就会内存泄漏——而且这种泄漏不会报错,只会在长时间运行后暴露。
典型场景:
-
//export Greet函数返回C.CString("hello"),Python 必须用ctypes.free()释放 - 若函数接收
*C.char,要用C.GoString()转成 Go 字符串后再处理,避免在 Go 里直接操作裸指针 - 涉及资源句柄(如数据库连接、文件描述符)时,必须配套提供
Close类型导出函数,并由调用方显式调用
跨语言调用时,头文件和 ABI 兼容性比代码逻辑更关键
Go 编译出的 .so 会附带一个同名 .h 文件,里面是 C 函数声明。这个头文件不是装饰品——Python 的 ctypes、C++ 的 dlopen 都依赖它来校验参数个数、类型大小和调用约定。一旦你手动改了 Go 函数签名但忘了更新头文件,或者用不同版本 GCC 编译,就可能触发段错误或值错乱。
实际影响:
- Linux 上不同 glibc 版本间
size_t宽度可能不同,导致C.size_t在 32/64 位混用时出问题 - Windows 下 MinGW 和 MSVC 生成的 DLL 对
__stdcall处理不一致,Go 默认用cdecl,需确认调用方匹配 - 不要把
.h文件和.so分开部署;版本升级时必须两者一起替换
最常被忽略的点:动态库的生命周期必须与调用方对齐。Go 运行时在 .so 加载时初始化,在卸载时不会自动清理 goroutine 或 finalizer——如果你在导出函数里启了后台 goroutine,它会一直跑,直到整个进程退出。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











