CGO 使用指南
CGO 是 Go 语言的一个特性,它允许 Go 代码与 C/C++ 代码进行互操作。通过 CGO,可以调用 C 函数、访问 C 结构体和数组,还可以将 Go 函数导出为 C 函数供 C 代码调用。本文将详细介绍如何在 Go 项目中使用 CGO,包括调用 C/C++ 函数、类型转换、内存管理、常见的陷阱等。
1. 调用 C 函数
使用 CGO 调用 C 函数需要以下步骤:
- 导入 C 包: 使用
import "C"
语句。 - 编写 C 代码: 在
import "C"
之前的注释块中编写 C 代码。 - 调用 C 函数: 使用
C.<function_name>
语法调用 C 函数。
示例:
package main
/*
#include <stdio.h>
void myPrint(char* s) {
printf("%s\n", s);
}
*/
import "C"
import "unsafe"
func main() {
str := "Hello from Go!"
cstr := C.CString(str)
defer C.free(unsafe.Pointer(cstr))
C.myPrint(cstr)
}
2. 类型转换
Go 类型转换为 C 类型
- 字符串: 使用
C.CString
将 Go 字符串转换为 C 字符串。 - 指针: 使用
unsafe.Pointer
进行指针转换。
str := "Hello"
cstr := C.CString(str)
defer C.free(unsafe.Pointer(cstr))
C 类型转换为 Go 类型
- C 字符串: 使用
C.GoString
将 C 字符串转换为 Go 字符串。 - 指针: 使用
unsafe.Pointer
和类型转换。
cstr := C.CString("Hello")
defer C.free(unsafe.Pointer(cstr))
gostr := C.GoString(cstr)
3. 结构体访问
定义 C 结构体并在 Go 中访问:
package main
/*
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void printPoint(Point* p) {
printf("Point(%d, %d)\n", p->x, p->y);
}
*/
import "C"
import "fmt"
func main() {
p := C.Point{x: 10, y: 20}
C.printPoint(&p)
fmt.Println(p.x, p.y)
}
4. 数组访问
定义 C 数组并在 Go 中访问:
package main
/*
#include <stdio.h>
void printArray(int* arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
*/
import "C"
import "unsafe"
func main() {
arr := []C.int{1, 2, 3, 4, 5}
C.printArray((*C.int)(unsafe.Pointer(&arr[0])), C.int(len(arr)))
}
5. 调用 C++ 代码
在 C++ 代码中定义头文件:
myclass.h:
#ifdef __cplusplus
extern "C" {
#endif
typedef struct MyClass MyClass;
MyClass* NewMyClass();
void MyClass_sayHello(MyClass* obj);
#ifdef __cplusplus
}
#endif
myclass.cpp:
#include <iostream>
#include "myclass.h"
class MyClass {
public:
void sayHello() {
std::cout << "Hello from C++" << std::endl;
}
};
extern "C" {
MyClass* NewMyClass() {
return new MyClass();
}
void MyClass_sayHello(MyClass* obj) {
obj->sayHello();
}
}
Go 代码:
package main
/*
#cgo CXXFLAGS: -std=c++11
#cgo LDFLAGS: -lstdc++
#include "myclass.h"
*/
import "C"
func main() {
obj := C.NewMyClass()
C.MyClass_sayHello(obj)
}
编译命令:
go build -o myprogram
./myprogram
6. C 调用 Go 函数
可以通过导出 Go 函数使 C 代码调用它。使用 //export
指令将 Go 函数导出为 C 函数。
示例:
Go 代码:
package main
/*
#include <stdio.h>
// 声明一个C函数,稍后将由Go实现
extern void goHello();
void callGoHello() {
goHello();
}
*/
import "C"
//export goHello
func goHello() {
println("Hello from Go!")
}
func main() {
C.callGoHello()
}
编译命令:
go build -o myprogram
./myprogram
7. 内存管理
为了在 Go 项目中保证 C 的内存释放,需要注意以下几点:
- 手动释放内存: 对于通过 C 代码分配的内存,需要手动调用相应的释放函数,例如
C.free
。 - 使用
defer
: 在 Go 中,可以使用defer
语句来确保在函数返回时释放内存。
示例:
package main
/*
#include <stdlib.h>
*/
import "C"
import "unsafe"
func main() {
str := C.CString("Hello")
defer C.free(unsafe.Pointer(str))
}
8. 常见的类型转换
Go 提供了一些与 C 类型转换的内置定义,主要在 C
包中。以下是一些常见类型转换:
Go to C
C.int
对应int
C.char
对应char
C.long
对应long
C.float
对应float
C.double
对应double
C.CString(s)
将 Go 字符串s
转换为 C 字符串(char*
),需要使用C.free
释放内存
C to Go
C.GoString(cstr)
将 C 字符串cstr
转换为 Go 字符串C.GoBytes(ptr, length)
将 C 字节数组ptr
转换为 Go 字节切片
9. 引用外部库
通常情况下,项目中会引用外部静态库或动态库。以下是如何在 CGO 中使用外部库的示例。
使用外部静态库
假设我们有一个静态库 libmylib.a
,可以如下方式使用:
C 代码(mylib.h):
void myFunction();
Go 代码:
package main
/*
#cgo LDFLAGS: -L. -lmylib
#include "mylib.h"
*/
import "C"
func main() {
C.myFunction()
}
编译命令:
go build -o myprogram
./myprogram
使用外部动态库
假设我们有一个动态库 libmylib.so
,可以如下方式使用:
C 代码(mylib.h):
void myFunction();
Go 代码:
package main
/*
#cgo LDFLAGS: -L. -lmylib
#include "mylib.h"
*/
import "C"
func main() {
C.myFunction()
}
编译命令:
go build -o myprogram
LD_LIBRARY_PATH=. ./myprogram
10. 常见的坑
- 跨语言调用开销: 频繁的 Go 和 C 之间调用会有性能开销。
- 内存管理: 必须注意内存的分配和释放,避免内存泄漏。
- 并发问题: Go 的并发模型和 C 的线程模型不同,需要小心处理并发问题。
- 平台依赖: CGO 代码可能依赖于特定的平台和编译器,降低了可移植性。
- 调试困难: CGO 代码混合了 Go 和 C,调试可能更加复杂。
- 错误处理: 在 Go 和 C 之间传递错误信息可能会比较复杂。
通过合理的设计和谨慎的编码,可以利用 CGO 的强大功能,同时避免一些常见的陷阱。