
本文详解 cgo 中 `//export` 的正确用法,解决因头文件重复包含导致的 “undefined symbols” 和 “duplicate symbol” 链接错误,涵盖头文件分离、c 函数声明规范、导出函数签名要求及构建流程。
在 CGO 项目中,让 C 代码安全调用 Go 函数是常见需求,但极易因符号管理不当引发编译或链接失败。你遇到的两个核心错误——Undefined symbols for architecture x86_64 和后续的 duplicate symbol——本质源于同一根本问题:C 实现文件(test.c)被直接 #include 进 Go 源码的 C 块中,导致其定义被 cgo 构建系统重复解析和链接。
cgo 在构建时会自动生成 _cgo_export.o 目标文件,其中包含所有标记为 //export 的 Go 函数对应的 C 兼容包装器(如 _receiveC)。当你在 /* #include "test.c" */ 中直接嵌入 C 源文件时,cgo 会将其内容复制进生成的 example.cgo2.c,再与 test.c 原始编译单元(test.o)一同参与链接,从而造成符号重复定义(_myprint, _receiveC 等);而若未正确声明 receiveC,则又因 C 侧找不到该符号而报 Undefined symbols。
✅ 正确做法是严格遵循 C 语言模块化原则:头文件(.h)仅声明,源文件(.c)只实现,Go 侧仅通过头文件声明调用 C 函数,不参与 C 实现的编译。
✅ 推荐结构(完整可运行示例)
1. 创建头文件 test.h(声明接口)
#ifndef TEST_H_ #define TEST_H_ #include// 声明 C 函数,供 Go 调用 char* myprint(char *msg); #endif
2. 创建实现文件 test.c(定义逻辑,不包含 Go 导出函数)
#include#include "test.h" // 声明外部 Go 导出函数(必须与 Go 中 //export 签名完全一致) extern void receiveC(char *msg); char* myprint(char *msg) { receiveC(msg); // 安全调用 Go 函数 return msg; }
3. Go 文件 example.go(仅引用头文件,正确导出)
package main
/*
#cgo CFLAGS: -I.
#include "test.h"
*/
import "C"
import "fmt"
//export receiveC
func receiveC(msg *C.char) {
fmt.Println(C.GoString(msg))
}
func Example() {
fmt.Println("this is go")
result := C.GoString(C.myprint(C.CString("go!!")))
fmt.Println("C returned:", result)
}
func main() {
Example()
}⚠️ 关键注意事项:
- //export 必须紧贴函数定义前,// 与 export 之间绝对不可有空格;
- 导出函数必须是包级函数(非方法),且参数/返回值类型需为 C 兼容类型(如 *C.char, C.int);
- /* #include "...h" */ 中只能包含 .h 头文件,严禁 #include "xxx.c";
- 若需额外编译选项(如指定头文件路径),使用 // #cgo CFLAGS: -I.(注意 // 后紧跟空格和 #cgo);
- C.CString 分配的内存需手动释放(本例未释放,仅作演示;生产环境建议用 defer C.free(unsafe.Pointer(...)))。
✅ 构建与运行
go build -o cgo_example . ./cgo_example
输出应为:
this is go go!! C returned: go!!
? 总结:CGO 不是“把 C 代码粘贴进 Go”,而是构建跨语言 ABI 边界。坚持“头文件声明 + 源文件实现 + Go 仅声明调用”的三层隔离,是规避符号冲突、确保链接成功的黄金准则。










