之前我也了解过 CGO 相关的知识,但是当时给我的印象全部都是 “CGO 性能差” “完全没有必要,实际根本用不到”,但是这次听了大佬的一些分享发现 CGO 其实就是黑科技啊,有了它你在使用 go 的时候有了更多的想象力。本文将带你初步了解和使用 CGO,本文只是抛砖头,因为有关 CGO 的文档其实蛮少的,在其中也有很多坑,所以今天来踩一次,不知道会不会留下什么坑….

有了 CGO,Go 就有机会继承 C/C++近半个世纪的遗产 by 曹大

CGO 使用案例分享

首先来看一下最近我看到使用 CGO 的两个案例

案例 1 mosn

mosn

https://github.com/mosn/mosn

其中 mosn 通过 CGO 的能力,想办法在 envoy 后面加了一层,使得其底层网络具备 C 同等的处理能力的同时,又能使上层业务可高效复用 mosn 的处理能力及 go 的高开发效率。

案例 2 主动式缓存

active-cache

这个 GopherChina 上一个学而思网校的分享,主要讲的是如何设计一个主动式内存缓存,其中提到了 Go 的 GC 导致当有大量内存缓存的时候,对象数量过多,GC 扫描的时间会很长,所以将缓存对象存储到 C 中,然后利用 CGO 去访问缓存的对象,因为当对象在 C 中的时候就不参与 GC 了。当时我听到这个思路的时候也是觉得有点意思,原来 CGO 还可以有这样的操作。

理论存在,实践开始

开胃小菜

首先先来一个简单 hello world 让你没有用过 CGO 的同学来体验一下

1
2
3
4
5
6
7
8
package main

//#include <stdio.h>
import "C"

func main() {
C.puts(C.CString("Hello, World\n"))
}

非常简单,这里有以下几点

  • import C,有了它我们就能通过 C. 来使用 C 的一些方法,还引入了 <stdio.h>
  • 我们通过 C.CString 将 go 的字符串转换为了 c 的 “字符串”
  • 调用 C 里面的 puts 函数打印了这个字符串

就是这么简单,一个 CGO 的代码就完成了,有了它你是不是觉得其实 CGO 很简单,可以为所欲为了?NoNo 其实还有下面几点需要注意

注意点

  1. 类型转换:Golang 里面类型不能拿出来直接给 C 使用,因为底层的存储方式不同,所以必须通过 C.CString 等类似的方法进行转换;同样的,C 返回的类型也无法在 Go 中直接使用,也需要做一次转换,如通过 C.GoString 将 c 的 *char 转换为 go 的 string
  2. 内存:C 是没有 GC 的,所以 C 的内存需要手动管理,比如这里构造的字符串,在 C 里面是需要手动释放的,通过 C.free(unsafe.Pointer(s)) 可以进行 free;当然,反过来,当 C 要访问 go 的内存的时候也需要注意,Go 是有 GC 的,而 Go 的 Gc 是不知道当前这个对象在 C 里面是否还有在使用的,所以如果使用不当,C 中访问 go 的对象,这个对象可能已经被 GC 了
  3. 性能损失:因为 Go 和 C 有着不同的内存模型和函数调用规约,所以显然在使用 CGO 的时候需要栈的切换工作,那么势必带来这性能的损失

其他细节可以还查看 https://golang.org/cmd/cgo/ https://golang.org/src/cmd/cgo/doc.go

正餐

基于我之前听到的分享案例二,主动式缓存,它想办法在 C 里面开辟了一片新天地,让它绕过了 GO 的 GC 扫描,于是我就想着实践一下,搞一个最小 demo 看看。

目标

  • 在 C 里面搞一个 map 当做缓存
  • Go 通过 CGO 去访问这个 map 进行操作

然后之前写 C++ 的时候就经常用到 STL 库嘛,那里面的 map 自然是耳熟能详,所以就想到了如果我能想办法搞定这个 STL 的库势必就能实现这个 demo 了,理论存在,实践开始。

my_map.h

1
2
3
void MmPut(const char* key, const char* value);
const char* MmGet(const char* key);
void MmDelete(const char* key);

首先定义一个头文件 my_map.h,里面包含三个函数分别是 put,get,delete 对 map 的相关操作

这里解释一下,因为在 C 里面你需要首先给出这个函数的定义,才能在下面使用这个函数并且实现它,所以就需要定这个。

my_map.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <string>
#include <map>

extern "C" {
#include "my_map.h"
using namespace std;
}

map<string, string> mm;

void MmPut(const char* key, const char* value) {
mm[key] = value;
}

const char* MmGet(const char* key) {
return mm[key].data();
}

void MmDelete(const char* key) {
mm.erase(key);
}

然后定义我们的具体方法 my_map.h 这里写的很粗糙,就直接定义了一个 map 然后对它进行三个操作的实现,其中需要注点的是:

  1. STL 库里面的 map 实现是红黑树,有序,这里是偷懒,如果没有必要的话,需要 hashmap 的话可以使用 unordered_map
  2. 这里 using namespace std; 也是偷懒,我不想每个都写一遍 std:: 懂的都懂
  3. 这里使用 char* 作为入参是因为将 go 的字符串转换过来的时候是这个类型

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

//void MmPut(const char* key, const char* value);
//const char* MmGet(const char* key);
//void MmDelete(const char* key);
/*
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
)

func main() {
Put2MyMap("author", "linkinstar")

s := GetFromMyMap("author")
fmt.Println("before delete:", s)

DeleteFromMyMap("author")

s = GetFromMyMap("author")
fmt.Println("after delete:", s)
}

func Put2MyMap(key, value string) {
k := C.CString(key)
v := C.CString(value)
defer func() {
C.free(unsafe.Pointer(k))
C.free(unsafe.Pointer(v))
}()
C.MmPut(k, v)
}

func GetFromMyMap(key string) (value string) {
k := C.CString(key)
defer func() {
C.free(unsafe.Pointer(k))
}()
v := C.MmGet(k)
value = C.GoString(v)
return value
}

func DeleteFromMyMap(key string) {
k := C.CString(key)
defer func() {
C.free(unsafe.Pointer(k))
}()
C.MmDelete(k)
}

最后就是我们的 GO 代码了,封装了一下三个方法,在 main 中测试一下,完成~

其中需要注意的是之前上面提到的手动 free 对应的内存,输出:

1
2
before delete: linkinstar
after delete:

至此我们就最简单的能通过 CGO 使用 STL 库了,那么相对应的,有了这个砖头,那么其他相关的 vector,set….你都可以使用了,甚至可以来个什么 algorithm 的 next_permutation 什么的,想想就有点刺激。

当然以上只是个 demo,如果需要真的当缓存来用,还有很多需要优化的地方,比如调用过程中减少 key,value 的拷贝,缓存的并发访问等等…..

也有库已经实现一个这样的 map,如果有需要可以尝试进行使用 https://github.com/glycerine/offheap

总结与延伸

其实看着代码很容易,但是当我第一次写的时候碰到一堆的问题,一方面是 CGO 的资料不多,代码也不多,所以参考资料比较少,很多代码需要猜测怎么样写,基本上是照猫画虎,用过之后就好了很多了,基本上能知道大体的使用,剩下的就是细节了。

CGO 的使用前提还需要你对 C 有一定的了解,如果完全没有接触过,可能也会觉得比较困难。很期待主动式缓存那个框架实现的开源,这样可以巴拉巴拉它的代码看看它是

那么其实除了 STL 一个特别有意思的事情,就是 OC,没错 ObjectC。我们知道 Cocoa 是苹果官方 macOS 出的一个接口,那么其实可以通过 cgo 来调用其中的接口来做一些 macos 原生做的事情,这就非常有意思了。github 上其实有很多相关的库,这里就不再列举了。

https://coderwall.com/p/l9jr5a/accessing-cocoa-objective-c-from-go-with-cgo

https://github.com/alediaferia/gogoa

总的来说,CGO 就像一座桥,不仅让 Go 继承了 C 的遗产,而且连接更加广阔的空间,给了你更多的想象力。我觉得它并不是很多人所说的是 C++ 程序迁移到 Go 程序的一个中间态,我觉得它会一直存在,给我们带来更多的黑魔法。