Go 语言除了语法精炼、并发支持好外,还有一个优点就是可以调用 C 代码。可以直接在 Go 源代码里写 C 代码,也可以引 C 语言的外部库。这样在性能遇到瓶颈的地方可以重写,或者某些功能 Go 和第三方还缺失,但 C 语言有现成的库就可以直接用了。官方 Cgo 这块目前有一篇博客 https://blog.golang.org/c-go-cgo 和 命令行文档 https://golang.org/cmd/cgo/ 对 Cgo 进行了说明,其中某些地方还不够明确或者没有提到的地方。
内联 C 代码
下面例子是直接将 C 代码写在 Go 源代码里,引入了一个不存在的包 “C”, 然后将 C 代码写在了引入上面,注意只能写在 “C” 包上面。定义了一个方法 add
, 然后通过 C.add 在 Go 代码里使用。
package main
/* 下面是 C 代码 */
// int add(int a, int b) {
// return a + b;
// }
import "C"
import "fmt"
func main() {
a := C.int(1)
b := C.int(2)
value := C.add(a, b)
fmt.Printf("%v\n", value)
fmt.Printf("%v\n", int(value))
}
go run main.go
独立的 C 源代码
小巧的 C 代码可以方便的通过内联使用,但是代码比较庞大的话可以将 C 代码独立写。
有下面目录结构:
example/
foo.h
foo.c
main.go
foo.h
int add(int, int);
foo.c
int add(int a, int b) {
return a + b;
}
main.go
package main
// #include "foo.h"
import "C"
import "fmt"
func main() {
a := C.int(1)
b := C.int(2)
value := C.add(a, b)
fmt.Printf("%v\n", value)
}
上面例子将 C 代码拆成了 foo.h 和 foo.c , Go 代码只需要将 foo.h
引入即可调用 add
方法。
main 包引用多个文件后就不能使用 go run 命令运行了,只能使用 go build。go build 会检测 cgo 引用的文件,.c .h 等文件也会一起被编译。
cd example/
go build -o out
./out
调用外部库
目录结构:
example2/
main.go
foo/
foo.h
foo.c
C 代码和之前的一样,只是移到了 foo 目录下,目的是强制让 Go 不编译他们。假定 foo 就是我们需要用到的外部库。
先编译静态库, foo 目录下生成 libfoo.a
静态链接库。
cd foo
gcc -c foo.c -o foo.o
ar -crs libfoo.a foo.o
main.go
package main
// #cgo CFLAGS: -I./foo
// #cgo LDFLAGS: -L./foo -lfoo
// #include "foo.h"
import "C"
import "fmt"
func main() {
value := C.add(C.int(1), C.int(2))
fmt.Printf("%v\n", value)
}
调用外部库需要用到 #cgo
伪指令, 他可以指定编译和链接参数,如 CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS and LDFLAGS。 CFLAGS 可以配置 C 编译器参数,其中 -I 可以指定头文件目录,例如 “-I/path/to/include”。LDFLAGS 可以配置引用库的参数,其中 -L 指定引用库的目录,-l 指定库名称。如果引用的头文件或者库在系统默认的目录下(例如 /usr/include, /usr/local/include 和 /usr/lib, /usr/local/lib)则可以不用指定目录。
#cgo
指令中可以添加限制平台的参数,例如只针对 linux 或者 darwin 平台,详情参考 https://golang.org/pkg/go/build/#hdr-Build_Constraints 。同时可以使用 ${SRCDIR}
代替源代码目录。
// #cgo linux CFLAGS: -I$./foo
// #cgo darwin CFLAGS: -I$./another_foo
// #cgo LDFLAGS: -L${SRCDIR}/foo -lfoo
数组指针的两个小技巧
C 语言中经常使用数组指针作为参数和返回值,下面例子展示了 Go 语言创建数组指针和转换数组指针到 slice 的过程。
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// int pass_array_pointer(int *in, int n) {
// int sum = 0;
// for(int i = 0; i < n; i++) {
// sum += in[i];
// printf("%d\n" , in[i]);
// }
// return sum;
// }
//
// int *return_array_pointer(int n) {
// int *out;
// out = (int *)(calloc(3, sizeof(int)));
// for(int i = 0; i < n; i++) {
// out[i] = i * 2;
// }
// return out;
// }
import "C"
import (
"fmt"
"reflect"
"unsafe"
)
// 将 slice 转换为 C 数组指针
func pass() {
in := []C.int{1, 2, 3, 4}
inPointer := unsafe.Pointer(&in[0])
inC := (*C.int)(inPointer)
value := C.pass_array_pointer(inC, 4)
fmt.Println(value)
}
// 将 C 数组指针转换为 slice
func get() {
n := 4
outC := C.return_array_pointer(4)
defer C.free(unsafe.Pointer(outC))
// 参考 https://github.com/golang/go/issues/13656#issuecomment-165867188
sh := reflect.SliceHeader{uintptr(unsafe.Pointer(outC)), n, n}
out := *(*[]C.int)(unsafe.Pointer(&sh))
for _, v := range out {
fmt.Println(v)
}
}
func main() {
pass()
get()
}
限于篇幅就先到这,下篇说下如果通过 Go 代码调用 C++ 代码。