Go 调用 C/C++ 函数全攻略

简介: Go 调用 C/C++ 函数全攻略

一、Go 语言调用 C/C++函数

1. cgo 基础及工作原理

Go 语言通过 cgo 和 C 语言的 ABI(Application Binary Interface)进行交互。

cgo 会生成相应的 C 代码,与 Go 代码一起编译成可执行文件或动态库。

cgo 的工作流程主要分为 3 步:

(1) 预处理:将 Go 源码中的 C 代码块提取出来,生成 .c 和 .h 文件

(2) 编译:调用 C 编译器,分别编译 C 文件和 Go 文件

(3) 链接:将 C 文件和 Go 文件链接生成最终的可执行程序

通过这种方式, Go 代码就可以直接调用 C 代码中的函数,两个语言实现了无缝衔接。

2. Go 调用 C 动态库示例

下面是一个 Go 程序调用 C 语言编写的动态库 libadd.so 的示例


package main
//#include <stdio.h>//int add(int a, int b);import "C"import "fmt"
func main() {    res := C.add(10, 20)    fmt.Println(res)}

libadd.c


int add(int a, int b) {  return a + b; }

编译运行


$ gcc -fPIC -shared -o libadd.so libadd.c$ go build demo.go -ldflags="-L." -gccgoflags="-Wl,-rpath,."$ ./demo30

通过 cgo 和 ABI,Go 程序可以无障碍调用 C 语言编译而成的动态库。

3. Go 调用 C++方法示例

Go 调用 C++函数与调用 C 函数基本类似,也需要通过 cgo 进行 ABI 层的适配。

主要不同在于 C++编译器的支持和名称修饰(name mangling)问题。

示例代码


package main
//#include "add.h"import "C"import "fmt"
func main() {    res := C.add(10, 20)      fmt.Println(res)}

add.h


#ifdef __cplusplusextern "C" {#endif
int add(int a, int b);
#ifdef __cplusplus}#endif

add.cpp


int add(int a, int b) {  return a + b;}

编译运行


$ g++ -fPIC -shared -o libadd.so add.cpp$ go build demo.go -ldflags="-L." -gccgoflags="-Wl,-rpath,."  $ ./demo30

通过外部 "C" 包装,解决了 C++名称修饰的问题,使得 Go 可以正确调用 C++函数。

4. 数据类型映射及类型不安全

Go 和 C/C++ 中的基础数据类型可以通过 cgo 自动映射,如:

Go int、uint 对应 C/C++ int、unsigned int

Go byte 对应 C/C++ char

Go string 对应 C/C++ char*

但是也存在一些类型无法精确映射的情况,例如 Go 的 string 和 slice 类型,会映射为 C 语言的指针,这种转换是类型不安全的,需要开发者自行处理。

此外,复杂数据结构的自定义类型也需要开发者自行转换处理。


 

二、C/C++调用 Go 函数

1. cgo 生成头文件供 C/C++使用

cgo 可以为 Go 包生成对应的 C 语言头文件,供 C/C++ 代码调用。

示例


package main
//export Addfunc Add(a, b int) int {    return a + b}
func main() {}

生成头文件


$ go tool cgo example.go

example.h


extern int Add(int a, int b);

2. C/C++主调 Go 函数示例

main.c


#include "example.h"#include <stdio.h>
int main() {  int res = Add(10, 20);  printf("%d\n", res);  return 0;  }

编译链接


$ gcc -o app main.c example.o

输出


30

通过 cgo 生成的头文件,C/C++ 代码可以无缝调用 Go 语言编写的函数。

3. Go 函数性能考量

与 Go 相比, C/C++ 函数调用性能更高,而 Go 函数调用会有一定的性能损耗。

所以在混合编程时,对性能敏感的功能应优先使用 C/C++实现,非核心流程可以使用 Go 实现。


 

三、Go 与 C/C++的数据交换

1. 数组及切片传递为 C 指针

Go 语言中的数组和切片可以直接转换为 C 语言中的指针进行传递。

示例代码


package main
//#include <stdio.h>//void print_array(int* arr, int len);import "C"import "unsafe"
func main() {    arr := []int{1, 2, 3, 4, 5}
    C.print_array    ((*C.int)(unsafe.Pointer(&arr[0])), C.int(len(arr)))}

print_array 函数定义


void print_array(int* arr, int len) {  for (int i = 0; i < len; i++) {    printf("%d ", arr[i]);   }}

输出


1 2 3 4 5

Go Slice 直接转换为 C 指针,两种语言实现了无缝对接。

2. 字符串处理差异及转换

Go 中的字符串为 UTF-8 编码,而 C/C++使用空终止符结束。字符串处理有一定区别。

简单示例


cs := C.CString("Hello")defer C.free(unsafe.Pointer(cs))
cstr := C.GoString(cs)

通过 C.CString 和 C.GoString 可以安全地在两种字符串表示之间转换。

3. 结构体互相映射机制

Go 语言中的结构体可以与 C 结构体互相映射,前提是两个结构体的定义是兼容的。

示例代码


package main
/*struct MyData {  int a;  char b;  float c;  };*/import "C"import "unsafe"import "fmt"
type MyData struct {    a int    b byte    c float32}
func main() {    v := MyData{1, 'a', 2.5}    cv := (*C.struct_MyData)(unsafe.Pointer(&v))    
    fmt.Println(cv.a) // 1    fmt.Println(cv.b) // 'a'    fmt.Println(cv.c) // 2.5  }

Go 与 C 结构体直接转换为指针使用,成员按内存分布顺序访问。这实现了两种语言自定义类型字段的无缝映射。


 

四、Go 与 C/C++的内存管理

1. Go 自动内存回收机制

Go 语言采用高效的垃圾回收机制来自动管理内存。不需要开发者手动分配释放内存,避免了常见的内存错误,例如泄漏、重用已释放内存等。

Go GC 主要采用标记清除算法,由垃圾回收器在后台定期自动执行,不会暂停应用程序线程。可以实现很低的 GC 延迟。

2. C/C++手动内存管理

C/C++要求开发者手动动态分配和释放堆内存。容易出现内存泄漏和重用悬空指针等严重 Bug。

智能指针等技术可部分缓解问题。

手动内存管理的好处是可以细粒度控制内存使用,优化性能。且不存在 GC 停顿的问题。

3. 内存安全和性能的权衡

Go 自动 GC 实现了内存安全防护,降低了开发难度,但是会有一定的性能损耗。

而 C/C++手动内存管理可以利用复杂手段优化内存和性能,但开发难度大且易出错。

综合来说,Go 更适合互联网服务端开发,而 C/C++更擅长一些对性能要求极高的场景,如游戏、图像处理等。


 

五、Go 与 C/C++进程间通信

1. 基于文件/管道的 IPC

Go 和 C/C++可以通过文件、命名管道(FIFO)、匿名管道实现进程间通信。

文件可实现数据持久化,管道支持全双工通信。都存在一定的时间和空间开销。

2. 基于 Socket 的网络通信

Socket 套接字可实现高效的网络数据传输,支持任意倍扩展。是跨语言混合编程的理想 IPC 方式。

Go 语言实现 socket 编程很高效,与 C/C++可完美配合。

3. 基于 RPC 的跨语言调用

Go 语言和 C/C++都有成熟的 RPC 框架,可以实现高性能的进程/网络间通信,构建分布式系统。

例如 Go 语言的 gRPC 框架,已经成为 CNCF 基金会的毕业项目,在云原生领域得到广泛应用。


 

六、Go 与 C/C++混合编程案例分析

1. 异步 IO 网络服务

服务端可以用 Go 实现,利用 Go 协程实现高并发。再通过 C 优化底层 IO 性能。这样既保证了易用性,又兼顾了性能。

2. 图形图像处理

图像处理算法利用 C/C++编写,并行计算由 Go 实现,两种语言各司其职,共同完成任务。


 

七、优缺点和注意事项分析

Go 与 C/C++混合编程,综合了两者的优势,但也需要注意一些问题。

优点包括:

提高性能: 计算密集型任务用 C/C++,业务逻辑用 Go

复用代码: 大量成熟 C/C++代码可复用

降低难度: Go 易用性提高整体开发效率

缺点和注意事项:

类型转换: 类型映射不完全,需要处理

调试复杂: 跨语言代码调用链导致 debug 难度增大

内存安全: C/C++代码中可能出现各类内存问题

并发处理: C/C++部分需要注意线程安全

总体而言,Go 与 C/C++互补优势多于竞争关系,谨慎适当混合可以取得最好效果。


 

八、总结

通过 cgo 和 ABI,Go 语言可以非常便利地与 C/C++ 进行交互调用,实现两种语言的优势互补。

数据类型基本可以无缝映射,通过指针和 RPC 可高效通信。

Go 自动内存管理提高了安全性,C/C++手动优化可取得更高性能。

在网络服务、计算任务等场景,混合编程可以充分发挥各自语言的长处。

Go 与 C/C++的交互坐标,是当前跨语言编程的重要内容。

目录
相关文章
|
16天前
|
JSON 安全 网络协议
go语言使用内置函数和标准库
【10月更文挑战第18天】
13 3
|
27天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
45 6
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
23 0
C++ 多线程之线程管理函数
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
154 1
|
1月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
30 1
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
41 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
Go
go函数
go函数
33 10
|
2月前
|
编译器 C++
【C++核心】函数的应用和提高详解
这篇文章详细讲解了C++函数的定义、调用、值传递、常见样式、声明、分文件编写以及函数提高的内容,包括函数默认参数、占位参数、重载等高级用法。
22 3