CGO
CGO
由于go需要GC,对于一些性能要求更高的场景,go可能不太适合处理,c作为传统的系统编程语言性能是非常优秀的,而cgo可以将两者联系起来,相互调用,让go调用c,将性能敏感的任务交给c去完成,go负责处理上层逻辑,cgo同样支持c调用go,不过这种场景比较少见,也不太建议这么做。
提示
文中代码演示的环境是win10,命令行用的是gitbash
,windows用户建议提前安装好mingw。
关于cgo,官方有一个简单的介绍:C? Go? Cgo! - The Go Programming Language,如果想要更详细的介绍,可以在标准库cmd/cgo/doc.go
中获取更加详细的信息,或者也可以直接看文档cgo command - cmd/cgo - Go Packages,两者内容是完全一样的。
代码调用
看下面一个例子
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("hello, cgo!"))
}
想要使用cgo特性,通过导入语句import "C"
即可开启,需要注意的是C
必须是大写字母,且导入名称无法被重写,同时需要确保环境变量CGO_ENABLED
是否设置为1
,在默认情况下该环境变量是默认启用的。
$ go env | grep CGO
$ go env -w CGO_ENABLED=1
除此之外,还需要确保本地拥有C/C++
的构建工具链,也就是gcc
,在windows平台就是mingw
,这样才能确保程序正常通过编译。执行如下命令进行编译,开启了cgo以后编译时间是要比纯go要更久的。
$ go build -o ./ main.go
$ ./main.exe
hello, cgo!
另外要注意的一个点就是,开启cgo以后,将无法支持交叉编译。
go嵌入c代码
cgo支持直接把c代码写在go源文件中,然后直接调用,看下面的例子,例子中编写了一个名为printSum
的函数,然后在go中的main
函数进行调用。
package main
/*
#include <stdio.h>
void printSum(int a, int b) {
printf("c:%d+%d=%d",a,b,a+b);
}
*/
import "C"
func main() {
C.printSum(C.int(1), C.int(2))
}
输出
c:1+2=3
这适用于简单的场景,如果c代码非常多,跟go代码糅杂在一起十分降低可读性,就不太适合这么做。
错误处理
在go语言中错误处理以返回值的形式返回,但c语言不允许有多返回值,为此可以使用c中的errno
,表示在函数调用期间发生了错误,cgo对此做了兼容,在调用c函数时可以像go一样用返回值来处理错误。要使用errno
,首先引入errno.h
,看下面的一个例子
package main
/*
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
int32_t sum_positive(int32_t a, int32_t b) {
if (a <= 0 || b <= 0) {
errno = EINVAL;
return 0;
}
return a + b;
}
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
sum, err := C.sum_positive(C.int32_t(0), C.int32_t(1))
if err != nil {
fmt.Println(reflect.TypeOf(err))
fmt.Println(err)
return
}
fmt.Println(sum)
}
输出
syscall.Errno
The device does not recognize the command.
可以看到它的错误类型是syscall.Errno
,errno.h
中还定义了其它很多错误代码,可以自己去了解。
go引入c文件
通过引入c文件,就可以很好的解决上述的问题,首先创建一个头文件sum.h
,内容如下
int sum(int a, int b);
然后再创建sum.c
,编写具体的函数
#include "sum.h"
int sum(int a, int b) {
return a + b;
}
然后在main.go
中导入头文件
package main
//#include "sum.h"
import "C"
import "fmt"
func main() {
res := C.sum(C.int(1), C.int(2))
fmt.Printf("cgo sum: %d\n", res)
}
现在进行编译的话,必须要指定当前文件夹,否则找不到c文件,如下
$ go build -o sum.exe . && ./sum.exe
cgo sum: 3
代码中res
是go中的一个变量,C.sum
是c语言中的函数,它的返回值是c语言中的int
而非go中的int
,之所以能成功调用,是因为cgo从中做了类型转换。
c调用go
c调用go,指的是在cgo中c调用go,而非原生的c程序调用go,它们是这样一个调用链go-cgo-c->cgo->go
。go调用c是为了利用c的生态和性能,几乎没有原生的c程序调用go这种需求,如果有的话也建议通过网络通信来代替。
cgo支持导出go函数让c调用,如果要导出go函数,需在函数签名上方加上//export func_name
注释,并且其参数和返回值都得是cgo支持的类型,例子如下
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}
改写刚刚的sum.c
文件为如下内容
#include <stdint.h>
#include <stdio.h>
#include "sum.h"
#include "_cgo_export.h"
extern int32_t sum(int32_t a, int32_t b);
void do_sum() {
int32_t a = 10;
int32_t b = 10;
int32_t c = sum(a, b);
printf("%d", c);
}
同时修改头文件sum.h
void do_sum();
然后在go中导出函数
package main
/*
#include <stdio.h>
#include <stdint.h>
#include "sum.h"
*/
import "C"
func main() {
C.do_sum()
}
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}
现在c中使用的sum
函数实际上是go提供的,输出结果如下
20
关键点在于sum.c
文件中导入的_cgo_export.h
,它包含了有关所有go导出的类型,如果不导入的话就无法使用go导出的函数。另一个注意点是_cgo_export.h
不能在go文件导入,因为该头文件生成的前提是所有go源文件能够通过编译。因此下面这种写法是错误的
package main
/*
#include <stdint.h>
#include <stdio.h>
#include "_cgo_export.h"
void do_sum() {
int32_t a = 10;
int32_t b = 10;
int32_t c = sum(a, b);
printf("%d", c);
}
*/
import "C"
func main() {
C.do_sum()
}
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}
编译器会提示头文件不存在
fatal error: _cgo_export.h: No such file or directory
#include "_cgo_export.h"
^~~~~~~~~~~~~~~
compilation terminated.
倘若go函数具有多个返回值,那么c调用时将返回一个结构体。
顺带一提,我们可以把go指针通过c函数参数传递给c,在c函数调用期间cgo会尽量保证内存安全,但是导出的go函数返回值不能带指针,因为在这种情况下cgo没法判断其是否被引用,也不好固定内存,如果返回的内存被引用了,然后在go中这段内存被GC掉了或者发生偏移,将导致指针越界,如下所示。
//export newCharPtr
func newCharPtr() *C.char {
return new(C.char)
}
上面的写法默认是不允许通过编译的,如果想要关闭这个检查,可以如下设置。
GODEBUG=cgocheck=0
它有两种检查级别,可以设为1
,2
,级别越高检查造成运行时开销越大,可以 前往cgo command - passing_pointer了解细节。
类型转换
cgo对c与go之间的类型做了一个映射,方便它们在运行时调用。对于c中的类型,在go中导入import "C"
之后,大部分情况下可以通过
C.typename
这种方式来直接访问,比如
C.int(1)
C.char('a')
但c语言类型可以由多个关键字组成,比如
unsigned char
这种情况就没法直接访问了,不过可以使用c中的typedef
关键字来给类型取个别名,其功能等同于go中的类型别名。如下
typedef unsigned char byte;
这样一来,就可以通过C.byte
来访问类型unsigned char
了。例子如下
package main
/*
#include <stdio.h>
typedef unsigned char byte;
void printByte(byte b) {
printf("%c\n",b);
}
*/
import "C"
func main() {
C.printByte(C.byte('a'))
C.printByte(C.byte('b'))
C.printByte(C.byte('c'))
}
输出
a
b
c
大部分情况下,cgo给常用类型(基本类型之类的)已经取好了别名,也可以根据上述的方法自己定义,不会冲突。
char
c中的char
对应go中的int8
类型,unsigned char
对应go中的uint8
也就是byte
类型。
package main
/*
#include <stdio.h>
#include<complex.h>
char ch;
char get() {
return ch;
}
void set(char c) {
ch = c;
}
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
C.set(C.char('c'))
res := C.get()
fmt.Printf("type: %s, val: %v", reflect.TypeOf(res), res)
}
输出
type: main._Ctype_char, val: 99
如果将set
的参数换成C.char(math.MaxInt8 + 1)
,那么编译就会失败,并提示如下错误
cannot convert math.MaxInt8 + 1 (untyped int constant 128) to type _Ctype_char
字符串
cgo提供了一些伪函数用于在c和go之间传递字符串和字节切片,这些函数实际上并不存在,你也没法找到它们的定义,就跟import "C"
一样,C
这个包也是不存在的,只是为了方便开发者使用,在编译后它们会被转换成其它的操作。
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer
// C string to Go string
func C.GoString(*C.char) string
// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string
// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
go中的字符串本质上是一个结构体,里面持有着一个底层数组的引用,在传递给c函数时,需要使用C.CString()
在c中使用malloc
创建一个“字符串”,为其分配内存空间,然后返回一个c指针,因为c中没有字符串这个类型,通常会使用char*
来表示字符串,也就是一个字符数组的指针,使用完毕后记得使用free
释放内存。
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cstring := C.CString("this is a go string")
C.printfGoString(cstring)
C.free(unsafe.Pointer(cstring))
}
也可以是char
数组类型,两者其实都一样,都是指向头部元素的指针。
void printfGoString(char s[]) {
puts(s);
}
也可以传递字节切片,由于C.CBytes()
会返回一个unsafe.Pointer
,在传递给c函数之前要将其转换为*C.char
类型。
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cbytes := C.CBytes([]byte("this is a go string"))
C.printfGoString((*C.char)(cbytes))
C.free(unsafe.Pointer(cbytes))
}
上面的例子输出都是一样的
this is a go string
上述这几种字符串传递方法涉及到了一次内存拷贝,在传递过后实际上是在c内存和go内存中各自保存了一份,这样做会更安全。话虽如此,我们依然可以直接传递指针给c函数,也可以在c中直接修改go中的字符串,看下面的例子
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
ptr := unsafe.Pointer(unsafe.SliceData([]byte("this is a go string")))
C.printfGoString((*C.char)(ptr))
}
输出
this is a go string
例子通过unsafe.SliceData
直接获取了字符串底层数组的指针,并将其转换为了c指针后传递给c函数,该字符串的内存是由go进行管理的,自然也就不再需要free,这样做的好处就是传递的过程不再需要拷贝,但有一定的风险。下面的例子演示了在c中修改go中的字符串
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s, int len) {
puts(s);
s[8] = 'c';
puts(s);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var buf []byte
buf = []byte("this is a go string")
ptr := unsafe.Pointer(unsafe.SliceData(buf))
C.printfGoString((*C.char)(ptr), C.int(len(buf)))
fmt.Println(string(buf))
}
输出
this is a go string
this is c go string
this is c go string
整数
go与c之间的整数映射关系如下表所示,关于整数的类型映射还在可以在标准库cmd/cgo/gcc.go
看到一些相关信息。
go | c | cgo |
---|---|---|
int8 | singed char | C.schar |
uint8 | unsigned char | C.uchar |
int16 | short | C.short |
uint16 | unsigned short | C.ushort |
int32 | int | C.int |
uint32 | unsigned int | C.uint |
int32 | long | C.long |
uint32 | unsigned long | C.ulong |
int64 | long long int | C.longlong |
uint64 | unsigned long long int | C.ulonglong |
示例代码如下
package main
/*
#include <stdio.h>
void printGoInt8(signed char n) {
printf("%d\n",n);
}
void printGoUInt8(unsigned char n) {
printf("%d\n",n);
}
void printGoInt16(signed short n) {
printf("%d\n",n);
}
void printGoUInt16(unsigned short n) {
printf("%d\n",n);
}
void printGoInt32(signed int n) {
printf("%d\n",n);
}
void printGoUInt32(unsigned int n) {
printf("%d\n",n);
}
void printGoInt64(signed long long int n) {
printf("%ld\n",n);
}
void printGoUInt64(unsigned long long int n) {
printf("%ld\n",n);
}
*/
import "C"
func main() {
C.printGoInt8(C.schar(1))
C.printGoInt8(C.schar(1))
C.printGoInt16(C.short(1))
C.printGoUInt16(C.ushort(1))
C.printGoInt32(C.int(1))
C.printGoUInt32(C.uint(1))
C.printGoInt64(C.longlong(1))
C.printGoUInt64(C.ulonglong(1))
}
cgo同时也对<stdint.h>
的整数类型提供了支持,这里的类型内存大小更为清晰明确,而且其命名风格也与go非常相似。
go | c | cgo |
---|---|---|
int8 | int8_t | C.int8_t |
uint8 | uint8_t | C.uint8_t |
int16 | int16_t | C.int16_t |
uint16 | uint16_t | C.uint16_t |
int32 | int32_t | C.int32_t |
uint32 | uint32_t | C.uint32_t |
int64 | int64_t | C.int64_t |
uint64 | uint64_t | C.uint64_t |
在使用cgo时,建议使用<stdint.h>
中的整数类型。
浮点数
go与c的浮点数类型映射如下
go | c | cgo |
---|---|---|
float32 | float | C.float |
float64 | double | C.double |
代码示例如下
package main
/*
#include <stdio.h>
void printGoFloat32(float n) {
printf("%f\n",n);
}
void printGoFloat64(double n) {
printf("%lf\n",n);
}
*/
import "C"
func main() {
C.printGoFloat32(C.float(1.11))
C.printGoFloat64(C.double(3.14))
}
切片
切片的情况的实际上跟上面讲到的字符串差不多,不过区别在于cgo没有提供伪函数来对切片进行拷贝,想让c访问到go中的切片就只能把切片的指针传过去。看下面的一个例子
package main
/*
#include <stdio.h>
#include <stdint.h>
void printInt32Arr(int32_t* s, int32_t len) {
for (int32_t i = 0; i < len; i++) {
printf("%d ", s[i]);
}
}
*/
import "C"
import (
"unsafe"
)
func main() {
var arr []int32
for i := 0; i < 10; i++ {
arr = append(arr, int32(i))
}
ptr := unsafe.Pointer(unsafe.SliceData(arr))
C.printInt32Arr((*C.int32_t)(ptr), C.int(len(arr)))
}
输出
0 1 2 3 4 5 6 7 8 9
这里将切片的底层数组的指针传递给了c函数,由于该数组的内存是由go管理,不建议c长期持有其指针引用。反过来,将c的数组作为go切片的底层数组的例子如下
package main
/*
#include <stdio.h>
#include <stdint.h>
int32_t s[] = {1, 2, 3, 4, 5, 6, 7};
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
l := unsafe.Sizeof(C.s) / unsafe.Sizeof(C.s[0])
fmt.Println(l)
goslice := unsafe.Slice(&C.s[0], l)
for i, e := range goslice {
fmt.Println(i, e)
}
}
输出
7
0 1
1 2
2 3
3 4
4 5
5 6
6 7
通过unsafe.Slice
函数可以将数组指针转换为切片,按照直觉来说,c中的数组就是一个指向头部元素的指针,按照常理来说应该这样使用
goslice := unsafe.Slice(&C.s, l)
通过输出可以看到,如果这样做的话,除了第一个元素,剩下的内存全都越界了。
0 [1 2 3 4 5 6 7]
1 [0 -1 0 0 0 3432824 0]
2 [0 0 -1 -1 0 0 -1]
3 [0 0 0 255 0 0 0]
4 [2 0 0 0 3432544 0 0]
5 [0 3432576 0 3432592 0 3432608 0]
6 [0 0 3432624 0 0 0 1422773729]
即便c中的数组只是一个头指针,经过cgo包裹了一下就成了go数组,有了自己的地址,所以应该对数组头部元素取址。
goslice := unsafe.Slice(&C.s[0], l)
结构体
通过C.struct_
前缀加上结构体名称,就可以访问c结构体,c结构体无法被当作匿名结构体嵌入go结构体。下面是一个简单的c结构体的例子
package main
/*
#include <stdio.h>
#include <stdint.h>
struct person {
int32_t age;
char* name;
};
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
var p C.struct_person
p.age = C.int32_t(18)
p.name = C.CString("john")
fmt.Println(reflect.TypeOf(p))
fmt.Printf("%+v", p)
}
输出
main._Ctype_struct_person
{age:18 name:0x1dd043b6e30}
如果c结构体的某些成员包含bit-field
,cgo就会忽略这类结构体成员,比如将person
修改为下面这种
struct person {
int32_t age: 1;
char* name;
};
再次执行就会报错
p.age undefined (type _Ctype_struct_person has no field or method age)
c和go的结构体字段的内存对齐规则并不相同,如果开启了cgo,大部分情况下会以c为主导。
联合体
使用C.union_
加上名称就可以访问c中的联合体,由于go并不支持联合体,它们在go中会以字节数组的形式存在。下面是一个简单的例子
package main
/*
#include <stdio.h>
#include <stdint.h>
union data {
int32_t age;
char ch;
};
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
var u C.union_data
fmt.Println(reflect.TypeOf(u), u)
}
输出
[4]uint8 [0 0 0 0]
通过unsafe.Pointer
可以进行访问和修改
func main() {
var u C.union_data
ptr := (*C.int32_t)(unsafe.Pointer(&u))
fmt.Println(*ptr)
*ptr = C.int32_t(1024)
fmt.Println(*ptr)
fmt.Println(u)
}
输出
0
1024
[0 4 0 0]
枚举
通过前缀C.enum_
加上枚举类型名就可以访问c中的枚举类型。下面是一个简单的例子
package main
/*
#include <stdio.h>
#include <stdint.h>
enum player_state {
alive,
dead,
};
*/
import "C"
import "fmt"
type State C.enum_player_state
func (s State) String() string {
switch s {
case C.alive:
return "alive"
case C.dead:
return "dead"
default:
return "unknown"
}
}
func main() {
fmt.Println(C.alive, State(C.alive))
fmt.Println(C.dead, State(C.dead))
}
输出
0 alive
1 dead
指针
谈到了指针避不开内存,cgo之间相互调用最大的问题就是两门语言的内存模型并不相同,c语言的内存完全是由开发者手动管理,用malloc()
分配内存,free()
释放内存,如果不去手动释放,它是绝对不会自己释放掉的,所以c的内存管理是非常稳定的。而go就不一样了,它带有GC,并且Goroutine的栈空间是会动态调整的,当栈空间不足时会进行增长,那么这样一来,内存地址就可能发生了变化,跟上图一样(图画的并不严谨),指针可能就成了c中常见的悬挂指针。即便cgo在大多数情况可以避免内存移动(由runtime.Pinner来固定内存),但go官方也不建议在c中长期引用go的内存。但是反过来,go中的指针引用c中的内存的话,是比较安全的,除非手动调用C.free()
,否则这块内存是不会被自动释放掉的。
如果要在c和go之间传递指针,就需要先将其转为unsafe.Pointer
,然后再转换成对应的指针类型,就跟c中的void*
一样。看两个例子,第一个是c指针引用go变量的例子,而且还对变量做了修改。
package main
/*
#include <stdio.h>
#include <stdint.h>
void printNum(int32_t* s) {
printf("%d\n", *s);
*s = 3;
printf("%d\n", *s);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var num int32 = 1
ptr := unsafe.Pointer(&num)
C.printNum((*C.int32_t)(ptr))
fmt.Println(num)
}
输出
1
3
3
第二个是go指针引用c变量,并对其修改的例子。
package main
/*
#include <stdio.h>
#include <stdint.h>
int32_t num = 10;
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println(C.num)
ptr := unsafe.Pointer(&C.num)
iptr := (*int32)(ptr)
*iptr++
fmt.Println(C.num)
}
输出
10
11
顺带一提,cgo不支持c中的函数指针。
链接库
c语言并没有像go这样的依赖管理,想要直接使用别人写好的库除了直接获取源代码之外,还有个办法就是静态链接库和动态链接库,cgo也支持这些,得益于此,我们就可以在go程序中导入别人写好的库,而不需要源代码。
动态链接库
动态链接库无法单独运行,它在运行时会与可执行文件一起加载到内存中,下面演示制作一个简单的动态链接库,并使用cgo进行调用。首先准备一个lib/sum.c
文件,内容如下
#include <stdint.h>
int32_t sum(int32_t a, int32_t b) {
return a + b;
}
编写头文件lib/sum.h
#include <stdint.h>
int sum(int32_t a, int32_t b);
接下来使用gcc
来制作动态链接库,首先编译生成目标文件
$ cd lib
$ gcc -c sum.c -o sum.o
然后制作动态链接库
$ gcc -shared -o libsum.dll sum.o
制作完成后,然后在go代码中引入sum.h
头文件,并且还得通过宏告诉cgo去哪里寻找库文件
package main
/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"
*/
import "C"
import "fmt"
func main() {
res := C.sum(C.int32_t(1), C.int32_t(2))
fmt.Println(res)
}
CFLAGS: -I
指的是搜索头文件的相对路径,-L
指的是库搜索路径,${SRCDIR}
代指当前路径的绝对路径,因为它的参数必须是绝对路径-l
指的是库文件的名称,sum就是sum.dll
。
CFFLAGS
和LDFLAGS
这两个都是gcc的编译选项,出安全考虑,cgo禁用了一些参数,前往cgo command了解细节。
把动态库放到exe
的同级目录下
$ ls
go.mod go.sum lib/ libsum.dll* main.exe* main.go
最后编译go程序并执行
$ go build main.go && ./main.exe
3
到此动态链接库调用成功。
静态链接库
不同于动态链接库,使用cgo导入静态链接库时,它会与go的目标文件最终链接成一个可执行文件。还是拿sum.c
举例,先将源文件编译成目标文件
$ gcc -o sum.o -c sum.c
然后将目标文件打包成静态链接库(必须是lib
前缀开头,不然会找不到)
$ ar rcs libsum.a sum.o
go文件内容
package main
/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"
*/
import "C"
import "fmt"
func main() {
res := C.sum(C.int32_t(1), C.int32_t(2))
fmt.Println(res)
}
编译
$ go build && ./main.exe
3
到此,静态链接库调用成功。
最后
虽然使用cgo的出发点是为了性能,但在c与go之间切换也会不小的造成性能损失,对于一些十分简单的任务,cgo的效率并不如纯go。看一个例子
package main
/*
#include <stdint.h>
int32_t cgo_sum(int32_t a, int32_t b) {
return a + b;
}
*/
import "C"
import (
"fmt"
"time"
)
func go_sum(a, b int32) int32 {
return a + b
}
func testSum(N int, do func()) int64 {
var sum int64
for i := 0; i < N; i++ {
start := time.Now()
do()
sum += time.Now().Sub(start).Nanoseconds()
}
return sum / int64(N)
}
func main() {
N := 1000_000
nsop1 := testSum(N, func() {
C.cgo_sum(C.int32_t(1), C.int32_t(2))
})
fmt.Printf("cgo_sum: %d ns/op\n", nsop1)
nsop2 := testSum(N, func() {
go_sum(1, 2)
})
fmt.Printf("pure_go_sum: %d ns/op\n", nsop2)
}
这是一个非常简单的测试,分别用c和go编写了一个两数求和的函数,然后各自运行100w次,求其平均耗时,测试结果如下
cgo_sum: 49 ns/op
pure_go_sum: 2 ns/op
从结果可以看到,cgo的平均耗时是纯go的二十几倍,倘若执行的不是单纯的两数相加,而是一个比较耗时的任务,cgo的优势会更大一些。除此之外,使用cgo还有以下缺点
- 许多go配套工具链将无法使用,比如gotest,pprof,上面的测试例子就不能使用gotest,只能自己手写。
- 编译速度变慢,自带的交叉编译也没法用了
- 内存安全问题
- 依赖问题,如果别人用了你的库,等于也要开启cgo。
在没有考虑周全之前,不要在项目中引入cgo,对于一些十分复杂的任务,使用cgo确实可以带来好处,但如果只是一些简单的任务,还是老老实实用go吧。