d.symbol("funcname") 返回 nil 不代表加载失败,因 nil 可能是合法零值(如 c 函数返回 void* 且本为 null),真正错误信号是 err != nil;仅凭 s == nil 判断会误判符号不存在或掩盖拼写/大小写错误。

为什么 d.Symbol() 返回 nil 不等于加载失败
调用 andlabs/dl 加载 .so 时,d.Symbol("FuncName") 返回 nil 很容易被误判为“符号不存在”,但实际它可能是合法零值——比如 C 函数返回 void* 且本就该是 NULL。真正可靠的错误信号只有 err != nil。
必须先检查 err,再判断 s 是否可安全调用:
-
if err != nil→ 真实加载失败(路径错、权限不足、ABI 不匹配) -
if s == nil && err == nil→ 符号存在但值为空(如函数返回空指针,或导出名拼写错误导致未匹配到任何符号) - 仅靠
s == nil就 panic 或跳过,会掩盖真实问题,也容易漏掉大小写/前缀差异(如期望Process,实际导出的是go_Process)
如何确认 .so 文件里真有你要的函数名
别依赖文档或猜测,直接查二进制。Linux 下用 readelf 或 nm 验证导出符号是否可见:
-
readelf -Ws libmy.so | grep "MyFunc"—— 推荐,显示所有符号及其绑定状态 -
nm -gD libmy.so | grep "MyFunc"—— 要求符号未被strip,否则无输出 - 如果输出为空,说明函数根本没导出:Go 侧漏了
//export注释,或 C 侧用了static或__attribute__((visibility("hidden"))) - 注意:Go 编译生成的
.so默认只导出带//export的函数,且名称不带包名前缀(main.Add导出后就是Add)
Go 调用 C 导出函数时的内存陷阱
Go 和 C 的内存生命周期不一致,最常见崩溃来自 C.CString 分配的内存未被释放,或把 Go 指针传给 C 后被 GC 回收。
-
C.CString返回的指针必须配对调用C.free,否则内存泄漏;C 函数若保存该指针并异步使用,需在 Go 侧用runtime.KeepAlive延长生命周期 - 不要直接传
&someGoVar给 C —— Go 的栈可能被移动,要用unsafe.Pointer(&someGoVar)+runtime.KeepAlive显式保活 - 导出函数参数尽量用 C 原生类型(
*C.char,C.int),避免传 Go 的string、slice或结构体——它们在 C ABI 中无定义 - 返回
*C.char时,确保内容已拷贝(C.CString),不要返回局部变量地址或C.GoString的底层指针
编译 Go 动态库时 -buildmode=c-shared 的硬约束
go build -buildmode=c-shared 不是“加个参数就能跑”,它强制要求代码满足 C ABI 兼容性:
- 必须有
import "C",且必须单独一行,前后不能有空行 - 所有
//export函数的参数和返回值类型只能是 C 兼容类型(C.int,*C.char,unsafe.Pointer),不能是 Go 的map、chan、func或带方法的 struct -
main()函数必须存在(哪怕为空),否则编译报错 - 不能依赖
init()顺序或全局变量初始化副作用——C 环境不保证 Go 的 init 流程执行时机 - 交叉编译时,
-buildmode=c-shared依赖目标平台的 GCC 工具链,不是纯 Go 编译器能独立完成的
init 函数,也不保证 main 里的逻辑被执行;所有初始化工作必须显式封装进某个导出函数(如 Init()),由调用方主动触发。否则看似正常的代码,在 Python 或 C 中加载后行为不可预测。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











