Cgo 的诞生是为了继承C/C++积累了半个世纪的软件财富,这样的话我们可以方便的在Go项目中使用这些财富!具体信息可以看官方文档 ,本文会介绍如何使用Cgo,如何将C++项目集成到Go中,有兴趣可以直接看我自己用Cgo写的一个项目,成熟度还可以: https://github.com/anthony-dong/protobuf
Cgo 真的完美吗
Cgo 顾名思义,是C与GO的一个桥梁,但是C与GO的调度模型、内存模型不太一样,就会导致这个桥梁会有一些性能、内存损耗,例如Go的用户代码都跑在goroutine(有栈协程)中,但是C跑在原生的线程中,就会导致要进行一次线程的切换,由 goroutine -> Native-Thread -> goroutine ,所以应该尽量避免使用一些耗时比较长的c程序!
TODO:后续补充JNI的性能开销!!
下面我们可以对比一下简单的Go和Cgo差异
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | package test
 
 
 
 
 
 import "C"
 
 func sum(x, y int) int {
 return x + y
 }
 
 func sum_c(x, y int) int {
 return int(C.sum_c(C.int(x), C.int(y)))
 }
 
 | 
来看下benchmark的结果,
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | goos: linuxgoarch: amd64
 pkg: github.com/anthony-dong/protobuf/internal/pb_gen
 cpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz
 BenchmarkSUM
 BenchmarkSUM-8     	1000000000	         0.3557 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM-8     	1000000000	         0.3567 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM-8     	1000000000	         0.3626 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM-8     	1000000000	         0.3588 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM-8     	1000000000	         0.3540 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM_C
 BenchmarkSUM_C-8   	14440388	        79.73 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM_C-8   	15093638	        85.74 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM_C-8   	14932076	        85.33 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM_C-8   	14808447	        79.42 ns/op	       0 B/op	       0 allocs/op
 BenchmarkSUM_C-8   	13486689	        78.92 ns/op	       0 B/op	       0 allocs/op
 PASS
 ok  	github.com/anthony-dong/protobuf/internal/pb_gen	8.531s
 
 | 
结论就是
- 大概调度上是3个数量级的损耗,差距近千倍!所以CGO不适合做那种简单的业务逻辑处理,如果代码可以很简单的通过Go程序实现,那么原则上不要用CGO去做,除非C性能要远高于GO或者GO去实现太过于麻烦!
- CGO使用原生的Native线程,如果你的C程序耗时比较严重,且并发较高,对于GO程序的影响也会很大!
- 注意GO里面可以通过 debug.SetMaxThreads(10)来设置最大的线程数,但是假如CGO调度的线程不够了,那么会直接程序挂掉,所以不要使用 限制最大线程数的函数,可以通过channel等工具来限制最大并发数量 !
| 12
 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
 
 | 
 
 
 
 
 
 
 import "C"
 
 func sum_c(x, y int) int {
 return int(C.sum_c(C.int(x), C.int(y)))
 }
 
 
 
 func TestCThread(t *testing.T) {
 t.Log("pid: ", os.Getpid())
 currentLock := make(chan bool, 10)
 
 wg := sync.WaitGroup{}
 wg.Add(100)
 for x := 0; x < 100; x++ {
 cloneX := x
 go func() {
 currentLock <- true
 defer func() {
 <-currentLock
 defer wg.Done()
 }()
 t.Log("sum-start: ", cloneX)
 s := sum_c(cloneX, cloneX+1)
 t.Log("sum-done: ", cloneX, s)
 }()
 }
 wg.Wait()
 }
 
 
 
 | 
熟悉Cgo基本写法
例子
代码分为两部分,一部分是C代码(注意: 必须是C,不能是C++),一部分是Go代码,其次一定要 import "C"
| 12
 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
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 
 | package main
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 import "C"
 import (
 "fmt"
 "unsafe"
 )
 
 func main() {
 {
 
 gstr := "hello world\u00001111"
 
 
 cstr := C.CString(gstr)
 defer C.free(unsafe.Pointer(cstr))
 
 
 C.c_print_str(cstr)
 C.c_print_str_size(cstr, C.int(len(gstr)))
 
 
 fmt.Println("int(C.c_str_len(cstr)): ", int(C.c_str_len(cstr)))
 fmt.Println("len(gstr): ", len(gstr))
 }
 
 {
 
 cstr := C.c_new_str()
 defer C.free(unsafe.Pointer(cstr))
 
 
 printStr("C.GoString(cstr)", C.GoString(cstr))
 
 printStr("C.GoStringN(cstr, 5)", C.GoStringN(cstr, 5))
 
 
 var data []byte = C.GoBytes(unsafe.Pointer(cstr), C.int(5))
 for _, elem := range data {
 fmt.Printf("char: %U\n", elem)
 }
 
 
 
 var data2 = data[:1]
 printStr("data2", string(data2))
 }
 
 {
 var ss C.CStruct
 ss.name = C.CString("tom")
 ss.age = C.int(1)
 C.print_CStruct(&ss)
 }
 }
 
 func printStr(name, value string) {
 fmt.Printf(`%s: "%s", len: %d`+"\n", name, value, len(value))
 }
 
 | 
执行 CGO_ENABLED=1 go run -v main.go ,输出:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | c_print_str: hello worldc_print_str_size: hello world1111
 int(C.c_str_len(cstr)):  11
 len(gstr):  16
 C.GoString(cstr): "a", len: 1
 C.GoStringN(cstr, 5): "abc", len: 5
 char: U+0061
 char: U+0000
 char: U+0062
 char: U+0000
 char: U+0063
 data2: "a", len: 1
 name: tom, age: 1
 
 | 
总结
- func C.CString(string) *C.char这个函数转换成C的字符串的时候,没有考虑- \0结尾符号的问题,所以这点一定要注意!
- func C.GoString(*C.char) string的实现考虑了- \0结尾符号的问题,因此它实际上就是拷贝了- strlen长度的C字符串到Go的字符串
- func C.GoStringN(*C.char, C.int) string解决了- \0结尾符号的问题,需要显示指定 C语言中字符串的长度!
- func C.GoBytes(unsafe.Pointer, C.int) []byte和- func C.CBytes([]byte) unsafe.Pointer可以实现数组的转换
- 其他基础类型都支持转换,具体看文档:官方文档
如何降低开销
使用原生的API,go->c 和 c->go 都需要涉及到数据的拷贝!
| 12
 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
 
 | import "C"const cStrEnd = string('\u0000')
 
 
 func unsafeCString(str string) *C.char {
 
 if index := strings.IndexByte(str, '\u0000'); index == -1 {
 str = str + cStrEnd
 }
 header := (*reflect.StringHeader)(unsafe.Pointer(&str))
 return (*C.char)(unsafe.Pointer(header.Data))
 }
 
 
 
 func unsafeCBytes(str []byte) *C.char {
 return (*C.char)(unsafe.Pointer(&str[0]))
 }
 
 
 func unsafeGoBytes(arr *C.char, arrSize C.int) []byte {
 header := reflect.SliceHeader{
 Data: uintptr(unsafe.Pointer(arr)),
 Len:  int(arrSize),
 Cap:  int(arrSize),
 }
 return *(*[]byte)(unsafe.Pointer(&header))
 }
 
 | 
调试工具
可以使用 go tool cgo main.go 查看cgo生成的文件, 其实我们用注释写C代码,编译器并不会识别,而是GO编译期间有个预处理的阶段 生成了 go tool cgo 的产物!
大概会生成一份 C -> GO 转换的代码,具体可以自己调试一下!
如何集成C++
C++ 与 C的关系
我们知道C++ 实际上是完全兼容 C的,其次C++与C是可以相互调用的,那么建立这些前提的就是 要明确告诉 c/c++ 编译器,我这个代码是C语言的,因此需要 extern "C" 来告诉 C++ 我这个代码是C语言的,编译器就会按照C语言的规范去链接!
返过来,C语言他没有 extern "C" 这个关键词,那么C++引用了C函数的代码,因此需要 extern "C" 可以修饰 #include ${c的头文件},也就是说告诉编译器,这些申明用C语言的规范去链接!
其实上面非常的绕,需要大家亲自体会一下!其次C与C++语法不完全一样,有些时候在做这种集成开发的时候容易混了!
下面这里有个例子,就是集成libprotobuf 实现解析 protobuf 文件,目前应该Go开源社区里面没有做集成的!
前置准备
- 下载 protobuf, 具体如何本地构建protobuf的链接库,可以直接看我们的这个项目
- 学会用CMake等工具构建代码
- 掌握C/C++/Go的基本语法
- 项目地址: https://github.com/anthony-dong/protobuf
项目结构
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | ├── CMakeLists.txt├── README.md
 ├── cgo.go # cgo go语言实现
 ├── cgo.h # cgo c头文件
 ├── deps # 依赖
 │   ├── README.md
 │   ├── darwin_x86_64
 │   ├── include # 引用的第三方头文件
 │   └── linux_x86_64 # 静态依赖
 │       ├── libprotobuf.a
 │       └── vendor.go # 解决go vendor 问题
 ├── errors.go
 ├── go.mod
 ├── go.sum
 ├── option.go
 ├── pb_include.h
 ├── pb_parser.cpp # 核心业务逻辑
 ├── pb_parser.go # 对外接口
 ├── pb_parser.h # 核心业务逻辑
 ├── utils.go
 └── vendor.go  # 解决go vendor 问题
 
 | 
大概就是C++写业务逻辑,然后 C++ 的接口 转成 C接口, C->GO的翻译!
实现功能
| 12
 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
 
 | package main
 import (
 "fmt"
 
 "github.com/anthony-dong/protobuf"
 )
 
 func main() {
 tree, err := protobuf.NewProtobufDiskSourceTree("internal/test/idl_example")
 if err != nil {
 panic(err)
 }
 idlConfig := new(protobuf.IDLConfig)
 idlConfig.IDLs = tree
 idlConfig.Main = "service/im.proto"
 idlConfig.IncludePath = []string{"desc", "."}
 
 desc, err := protobuf.ParsePBMultiFileDesc(idlConfig,
 protobuf.WithJsonTag(),
 protobuf.WithSourceCodeInfo(),
 protobuf.WithGoogleProtobuf(),
 protobuf.WithRequireSyntaxIdentifier(),
 )
 if err != nil {
 panic(err)
 }
 fmt.Println(protobuf.MessageToJson(desc, true))
 }
 
 
 
 | 
注意点
- 尽可能的使用 unsafe 操作避免内存拷贝,尤其是数据大的情况,效果优秀
- 简单函数尽可能的用GO实现
- 注意内存管理和回收,避免直接暴露给使用者
- C++的技巧可以参考我的这篇文章: https://anthony-dong.github.io/2023/04/06/fd8e40efcdb71f2be44fb720dc582d67/
- C 语言实际上没有太多要学习的,是最简单的语言了,没啥难度,无非注意内存分配!
- C++ 翻译 C 会存在有些类对象转换不来或者拷贝代价太高,尽可能的使用void指针避免拷贝!
参考