一、概述

Go语言可以通过自带的 cgo 工具进行 C+GO 混合编程,这个工具放在go安装目录的 pkg\tool 下,其源代码则在 src\runtime\cgo 里面,当然作为入门教程本文不打算对cgo的实现原理进行深入研究,仅从 Hello World 的角度来实际体验一下 cgo(文末有我收集的各种资源可供深入学习)。


二、从最简单开始 (内联C代码)

默认的 Go 编译器是关闭交叉编译功能的,因为开启了 cgo 会让 Go 程序的移植性变差且部署变得很麻烦。纯粹的 Go 代码固然更好,但有时候当需要用到的软件库找不到 Go 版本的时候则只能采用这种方式来应对。正面而言,没有cgo,Go 就不会有今天的地位,因为通过它可以继承 C/C++ 将近半个世纪的软件遗产,此外 cgo 也是在 Android 和 iOS 上运行 Go 程序的关键。

set CGO_ENABLED=1 export CGO_ENABLED=1

输入这段程序:

main.go:

package main

/*
int Add(int a, int b){
    return a+b;
}
*/
import "C"

import "fmt"

func main() {
	a := C.int(10)
	b := C.int(20)
	c := C.Add(a, b)
	fmt.Println(c) // 30
}
import "C"import "C" import "C"import "C" go build main.go go run main.go

三、引用C语言编写的库

大多数情况下,我们的目的是通过cgo来引用第三方软件库,无论开源闭源基本上都是以库的方式存在的,下面就来讲一下在Go代码中如何引用C语言编译好的静态链接库里的函数。为了简单起见我们先模拟一个C语言静态链接库,创建以下2个文件:

hello.c:

#include <stdio.h>
#include "hello.h"

void SayHello()
{
    printf("Hello, world!\n");
}

hello.h:

void SayHello();
gcc -c hello.car -crv libhello.a hello.o

PS:关于gcc和ar命令,是包含在MinGW软件里的,关于它的安装这里不展开讨论,可以翻阅我之前的博文,过程也很简单。

再创建1个Go文件:
main.go:

package main

/*
#cgo CFLAGS: -I${SRCDIR}
#cgo LDFLAGS: -L${SRCDIR} -lhello
#include "hello.h"
*/
import "C"

import (
	"fmt"
)

func main() {
	C.SayHello()
	fmt.Println("Succeed!")
}

CFLAGS和LDFLAGS 是两个C语言的编译和链接开关,在这里CFLAGS指明了头文件的路径,而LDFLAGS则指定了库文件路径和库文件名。

PS: 头文件就是指跟在 #include 后面的后缀为 .h的文件,库文件则是指以 .a 或 .so 结尾的文件,在Windows下则是 .lib 或 .dll 方式存在。${SRCDIR} 则表示当前目录,也就是我们通常使用的 “.” 。库文件不能使用相对路径是C/C++的历史遗留问题,通过${SRCDIR}则可以变相地采用相对路径,例如假设有一个绝对路径c:\test\hello,则${SRCDIR}\lib会自动展开为 c:\test\hello\lib。

CFLAGS 通过 -I 将在当前目录设为头文件(.h)检索路径。LDFLAGS 则通过 -L${SRCDIR} 在当前目录设为库文件(.a)检索路径,-lhello 表示具体链接的是 libhello.a 这个库。

注意:-lhello 表示链接 libhello.a这个库,其实是将 “lib" 和 后缀 “.a” 去掉之后简写为hello的,这是C语言的套路。此外,动态链接库(.so文件)的链接方式是一样的,假设现在我们提供的库文件是一个动态链接库 libhello.so ,在这里的设置是完全一样的都是 -lhello。

go build main.gogo run main.go

Hello, world!
Succeed!

尽管就只是打印了两行字,但这里的 “Hello, world!” 实际上是调用了 C语言的 libhello.a 库里的 SayHello() 函数的结果,而 “Succeed!” 则是调用普通 Go 标准库 fmt 的结果,两者有着本质区别。


四、引用C++库

相对而言,C++与Go的交叉编码显得要麻烦一些,cgo 是 C 语言和 Go 语言之间的桥梁,但原则上它无法直接支持 C++ 的类,只能增加一组 C 语言函数接口作为 C++ 类和 CGO 之间的桥梁,迂回地让Go和C++对接。这就是我们经常在开源Go项目里面经常看到 “xxx-bridge” 的原因,只要出现这种情况,多半是这个项目引用了C++库。

mkdir myLibcd myLib

hello.cpp:

#include "hello.h"
#include <iostream>

void hello() {
    std::cout << "Hello, World!\nThis message comes from a CPP function!" << std::endl;
}

hello.h:

void hello();
g++ -c hello.cpp ar crs libhello.a hello.ocd ..

hellobridge.cpp :

#include "hellobridge.h"
#include "mylib/hello.h"

void CallHello()
{
    hello(); // 调用库中的hello()函数
}

hellobridge.h :

#ifdef __cplusplus
extern "C" {
#endif

void CallHello();

#ifdef __cplusplus
}
#endif
extern "C" 

最后创建 Go主程序:

hello.go :

package main

/*
package main

/*
#cgo CXXFLAGS: -std=c++0x
#cgo LDFLAGS: -L${SRCDIR}/mylib -lhello
#cgo CPPFLAGS: -Wno-unused-result
#include "hellobridge.h"
*/
import "C"

func main() {
	C.CallHello()
}

这次我们设置了 CXXFLAGS 编译开关,告诉 cgo 现是 C++ 代码。

注意:go env 环境变量有 CC 和 CXX 之分,分别对应的是C和C++的可执行编译器(需要放到PATH命令里以在任何地方执行),当我们设置CFLAGS的时候,cgo 会自动开启C语言编译器进行工作(默认是gcc),而当CXXFLAGS开关被设置的时候,cgo 会自动选择使用C++编译器(默认是g++)。而我们不必纠结如何“指挥”cgo 用什么编译器去工作,它的逻辑很简单即通过两个编译开关来判定,我们不需要将 CC 设为 g++ 这种方法来实现强制 cgo 使用C++进行编译, 这将弄巧成拙。

-Wno-unused-result

上述文件全部创建完毕后,输入 go build -o hello.exe 或者 go run .

Hello, World!
This message comes from a CPP function!

 go build hello.go

五、使用pkg-config

本节并非本文的重点,但鉴于pkg-config这个工具使用很广泛并且 cgo 与它也有相应的对接参数,因此一并在这里告知。

gcc hello.cpp -I/usr/local/include -L/usr/local/lib -lhello -o hello.exe
whereisfind
gcc hello.cpp `pkg-config -cflags -libs hello` -o hello.exe 

其中用 ` ` 号包裹起来那段内容,在命令执行的时候会自动展开为

-I/usr/local/include -L/usr/local/lib -lhello

这样我们就只需要知道库的名字,而不需要关心这个库到底存放在哪了,省时又省心。

PKG_CONFIG_PATH
echo $PKG_CONFIG_PATH/mingw64/lib/pkgconfig:/mingw64/share/pkgconfigcd /mingw64/lib/pkgconfig
Name: Hello
Description: Hello World Cgo Test.
Version: 1.0.0
Libs: -Lc:/test/myLib -lhello
Cflags: -Ic:/test/myLib
pkg-config --list-allpkg-config --cflags --libs hello-Ic:/test/myLib -Lc:/test/myLib -lhello

现在,我们用gcc或者g++命令编译的时候就可以这样:

gcc hello.cpp `pkg-config -cflags -libs hello` -o hello.exe

它会自动展开成这样:

gcc hello.cpp -Ic:/test/myLib -Lc:/test/myLib -lhello -o hello.exe

坑提示:在Windows下的两个shell都不能识别 `pkg-config -cflags -libs hello`这种格式,无论是cmd还是powershell,我还尝试过使用 $(pkg-config -cflags -libs hello) 这样的格式,都不能实现展开。但对于已经安装好MSYS2的系统来说,这也不是什么大问题,只是进哪个Shell的差异而已。

回到我们的 Go 这边,采用了 pkg-config 之后,不再需要指定头文件和库文件路径,修改后代码如下:

package main

/*
#cgo pkg-config: hello
#cgo CXXFLAGS: -std=c++0x
#cgo CPPFLAGS: -Wno-unused-result
#include "hellobridge.h"
*/
import "C"

func main() {
	C.CallHello()
}
#cgo LDFLAGS: -L${SRCDIR}/mylib -lhellopkg-config: hello

六、后记

踩坑指南:

  1. 如果混杂了C代码的GO程序出现 go run 可以运行,但 go build 编译之后的 exe 文件无法执行的情况。具体而言就是 Windows 将产生一个貌似还挺严重的蓝屏错误:

此应用无法在你的电脑上运行。
或者:
This app can’t run on this PC
然后在 cmd 显示:
拒绝访问。

出现这个问题大多数和程序无关,尝试采用以下两种方法解决:

go build -o -buildmode=exe run.exe

附录:用MSYS2安装Go的方法

	先从 pacman 安装go:
	pacman -S mingw-w64-x86_64-go

	然后手动添加环境变量:
	export GOROOT=/mingw64/lib/go
	export GOPATH=/mingw64/lib/go/pkg

为了让这两个环境变量永久生效,可以把它添加到家目录的 .bashrc 文件里。
通过以上设置我们就可以在MSYS2环境里使用Go了。但是注意,MSYS2版本的 Go 其实就是Windows版本的Go,因此它也可以脱离MSYS2环境独立在 Windows 运行,只需要为 Windows 添加三个环境变量:
6. PATH 增加一行:C:\msys64\mingw64\lib\go\bin
7. 新建一个 GOROOT : C:\msys64\mingw64\lib\go
8. 新建一个 GOPATH : C:\msys64\mingw64\lib\go\pkg


cgo 门道非常深,本文仅作抛砖引玉之用,且只讲了go如何引用c,而未提及c引用go,以及变量、数组、结构体、指针等各种转换问题。我收集了一些学习资料可供大家深入研究:

官方手册:
https://pkg.go.dev/cmd/cgo
https://go.dev/blog/cgo

CGO:
https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-01-hello-cgo.html
https://www.cntofu.com/book/19/0.13.md
https://www.cnblogs.com/lidabo/p/6068448.html
https://bastengao.com/blog/2017/12/go-cgo-cpp.html
https://fasionchan.com/golang/practices/call-c/

C/C++:
https://blog.51cto.com/u_15091053/2652800
https://www.cnblogs.com/52php/p/5681711.html