go 不支持直接 dlopen 加载 .so,仅可通过 plugin.open 加载 go 自建插件或 cgo 封装 dlopen 调用 c 库;c-shared 用于导出供外部调用,不可被 go 加载。

Go 本身不支持 dlopen 动态加载 .so 文件
Go 编译默认生成静态链接的可执行文件,运行时不依赖 libc 的 dlopen/dlsym,也没有内置的 runtime.LoadPlugin 以外的动态符号加载机制。你不能像 C 那样用 dlopen("libfoo.so", RTLD_LAZY) + dlsym 直接调用 Go 编译出的 .so 中的函数——Go 的导出符号不是 C ABI 兼容的,且默认不暴露 C 可调用符号。
常见错误现象:undefined symbol: _cgo_dummy、cannot open shared object file: No such file(即使文件存在)、或调用后 panic:「call to C function with Go pointer argument」。
- Go 1.8+ 提供了
plugin.Open(),但仅支持 Linux/macOS,且要求目标 .so 是用go build -buildmode=plugin构建的插件,不是普通 C-style .so - 若想从 Go 调用 C 编写的 .so(如 libcurl、自定义 C 库),必须通过
cgo声明并链接,而非运行时dlopen - 若想让其他语言(如 Python/C)调用 Go 函数,需用
buildmode=c-shared导出 C ABI 符号,反过来才成立
plugin.Open() 是 Go 唯一原生支持的“动态加载”方式
它加载的是 Go 自己编译的插件(.so 文件),内部基于 ELF 解析和符号重定位,不经过 libc dlopen。本质是 Go 运行时自己实现的一套受限插件机制。
使用场景:需要热插拔业务逻辑(如策略、解析器),且所有模块都由同一 Go 版本、相同构建参数编译。
- 插件必须用
go build -buildmode=plugin -o plugin.so main.go构建,且main.go中至少有一个可导出变量(如var PluginVersion = "1.0")或函数 - 宿主程序必须启用
CGO_ENABLED=1(即使不用 cgo,plugin 机制底层依赖它) - 插件与宿主必须使用完全相同的 Go 版本、GOOS/GOARCH、且不能有不兼容的依赖(例如插件用了
github.com/some/pkg v2,宿主用 v1,会 panic:「plugin was built with a different version of package」) - 示例:
plug, err := plugin.Open("./handler.so") if err != nil { panic(err) } sym, err := plug.Lookup("Process") if err != nil { panic(err) } processFn := sym.(func(string) error) processFn("data")
想真正用 libc dlopen 加载 .so?只能走 cgo + C 函数桥接
如果你坚持要用 dlopen(比如集成遗留 C 库、或加载非 Go 构建的动态库),唯一可行路径是写一小段 C 代码封装 dlopen/dlsym,再通过 cgo 暴露给 Go 调用。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
关键点在于:C 层负责打开、查找、调用;Go 层只传入库路径、符号名、参数指针,并处理返回值。不能把 Go 函数指针传给 C 的 dlsym 结果去调用。
- C 封装示例(保存为
dlwrap.c):#include <dlfcn.h> void* my_dlopen(const char* path, int flag) { return dlopen(path, flag); } void* my_dlsym(void* handle, const char* name) { return dlsym(handle, name); } void my_dlclose(void* handle) { dlclose(handle); }</dlfcn.h> - Go 中调用:
/* #cgo LDFLAGS: -ldl #include "dlwrap.c" */ import "C" handle := C.my_dlopen(C.CString("./libmath.so"), C.RTLD_LAZY) defer C.my_dlclose(handle) add := C.my_dlsym(handle, C.CString("add")) // 注意:此处 add 是 *C.void,需强制转换为对应 C 函数类型,再调用 - 性能影响:每次
dlsym查找都有开销,建议缓存函数指针;dlopen后未dlclose会导致句柄泄漏 - 兼容性风险:不同系统
dlfcn.h行为略有差异(如 macOS 的RTLD_GLOBAL语义),Windows 完全不支持(得换LoadLibrary)
buildmode=c-shared 导出 Go 函数给外部调用,不是“加载”,但常被混淆
这是单向的:Go 编译成 .so 或 .dll,供 C/Python 等调用,不是 Go 主程序去加载它。名字里带“shared”容易让人误以为能被 Go 自己 dlopen。
典型错误:用 go build -buildmode=c-shared -o libgo.so main.go 生成库,然后在另一个 Go 程序里 plugin.Open("libgo.so") —— 必然失败,因为这不是 plugin 格式。
-
c-shared输出包含 C ABI 符号(如GoFunction),但没有 Go 插件所需的元信息(如 types、gcinfo、moduledata) - 若硬要混用,会出现 panic:「plugin.Open: not a plugin / invalid ELF header」
- 正确做法:C 程序用
dlopen打开libgo.so,再dlsym("GoFunction")调用;Go 程序若想“加载”,只能改用plugin模式重新编译那个库
Go 的动态加载边界很清晰:plugin 是封闭生态内的可控加载,cgo+dlopen 是对外部 C 生态的有限桥接。两者不互通,也不能绕过 Go 类型系统和内存模型做任意符号调用。最容易被忽略的是版本锁死问题——plugin 加载失败往往不是代码写错,而是 go version 或 GOEXPERIMENT 不一致导致的静默不兼容。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










