Go语言包一般什么时候定义 go语言 cgo_Go


过去的经验往往是走向未来的枷锁,因为在过时技术中投入的沉没成本会阻碍人们拥抱新技术。

——

chai2010

曾经一度因未能习得C++令人眼花缭乱的新标准而痛苦不已;Go语言“少即是多”的大道至简的理念让我重拾信心,寻回了久违的编程乐趣。

——Ending

C/C++经过几十年的发展,已经积累了庞大的软件资产,它们很多久经考验而且性能已经足够优化。Go语言必须能够站在C/C++这个巨人的肩膀之上,有了海量的C/C++软件资产兜底之后,我们才可以放心愉快地用Go语言编程。C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现。Go语言通过自带的一个叫CGO的工具来支持C语言函数调用,同时我们可以用Go语言导出C动态库接口给其他语言使用。

本章主要讨论CGO编程中涉及的一些问题。

2.1 快速入门

本节将通过一系列由浅入深的小例子来快速掌握CGO的基本用法。

2.1.1 最简CGO程序

真实的CGO程序一般都比较复杂,不过我们可以由浅入深。一个最简CGO程序该是什么样的呢?要构造一个最简CGO程序,首先要忽视一些复杂的CGO特性,同时要展示CGO程序和纯Go程序的差别来。下面是我们构建的最简CGO程序:

package mainimport "C"func main() { println("hello cgo")}

代码通过import "C"语句启用CGO特性,主函数只是通过Go内置的println()函数输出字符串,其中没有任何和CGO相关的代码。虽然没有调用CGO的相关函数,但是go build命令会在编译和链接阶段启动gcc编译器,这已经是一个完整的CGO程序了。

2.1.2 基于C标准库函数输出字符串

前面那个CGO程序还不够简单,现在来看看更简单的版本:

package main//#include import "C"func main() { C.puts(C.CString("Hello, World"))}

这个版本不仅通过import "C"语句启用CGO特性,还包含C语言的头文件。然后通过cgo包的C.CString()函数将Go语言字符串转换为C语言字符串,最后调用cgo包的C.puts()函数向标准输出窗口打印转换后的C字符串。

与1.2节中的CGO程序的最大不同是:我们改用C.Cstring来创建C语言字符串,而且改用puts()函数直接向标准输出打印,之前是采用fputs向标准输出打印。

没有释放使用C.CString创建的C语言字符串会导致内存泄漏。但是对这个小程序来说,这样是没有问题的,因为程序退出后操作系统会自动回收程序的所有资源。

2.1.3 使用自己的C函数

前面使用了标准库中已有的函数。现在我们先自定义一个叫作SayHello的C函数来实现打印,然后从Go语言环境中调用这个SayHello()函数:

package main/*#include static void SayHello(const char* s) { puts(s);}*/import "C"func main() { C.SayHello(C.CString("Hello, World"))}

除SayHello()函数是我们自己实现的之外,其他部分和前面的例子基本相似。

我们也可以将SayHello()函数放到当前目录下的一个C语言源文件中(扩展名必须是.c)。因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static修饰符。

// hello.c#include void SayHello(const char* s) { puts(s);}

然后在CGO部分先声明SayHello()函数,其他部分不变:

package main//void SayHello(const char* s);import "C"func main() { C.SayHello(C.CString("Hello, World"))}

既然SayHello()函数已经放到独立的C文件中了,我们自然可以将对应的C文件编译打包为静态库或动态库文件供使用。如果是以静态库或动态库方式引用SayHello()函数,需要将对应的C源文件移出当前目录(CGO构建程序会自动构建当前目录下的C源文件,从而导致C函数名冲突)。关于静态库等细节将在稍后章节讲解。

2.1.4 C代码的模块化

在编程过程中,抽象和模块化是将复杂问题简化的通用手段。当代码语句变多时,可以将相似的代码封装到一个个函数中;当程序中的函数变多时,将函数拆分到不同的文件或模块中。而模块化编程的核心是面向程序接口编程(这里的接口并不是Go语言的interface,而是API的概念)。

在前面的例子中,我们可以抽象一个名为hello的模块,模块的全部接口函数都在hello.h头文件定义:

// hello.hvoid SayHello(const char* s);

其中只有一个SayHello()函数的声明。但是作为hello模块的用户,可以放心地使用SayHello()函数,而无须关心函数的具体实现。而作为SayHello()函数的实现者,函数的实现只要满足头文件中函数的声明的规范即可。下面是SayHello()函数的C语言实现,对应hello.c文件:

// hello.c#include "hello.h"#include void SayHello(const char* s) { puts(s);}

在hello.c文件的开头,实现者通过#include "hello.h"语句包含SayHello()函数的声明,这样可以保证函数的实现满足模块对外公开的接口。

接口文件hello.h是hello模块的实现者和使用者共同的约定,但是该约定并没有要求必须使用C语言来实现SayHello()函数。我们也可以用C++语言来重新实现这个C语言函数:

// hello.cpp#include extern "C" { #include "hello.h"}void SayHello(const char* s) { std::cout << s;}

在C++版本的SayHello()函数实现中,我们通过C++特有的std::cout输出流输出字符串。不过,为了保证C++语言实现的SayHello()函数满足C语言头文件hello.h定义的函数规范,需要通过extern "C"语句指示该函数的链接符号遵循C语言的规则。

在采用面向C语言API接口编程之后,我们彻底解放了模块实现者的语言枷锁:实现者可以用任何编程语言实现模块,只要最终满足公开的API约定即可。我们可以用C语言实现SayHello()函数,也可以使用更复杂的C++语言来实现SayHello()函数,当然也可以用汇编语言甚至Go语言来重新实现SayHello()函数。

2.1.5 用Go重新实现C函数

其实CGO不仅用于Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。在前面的例子中,我们已经抽象一个名为hello的模块,模块的全部接口函数都在hello.h头文件定义:

// hello.hvoid SayHello(/*const*/ char* s);

现在我们创建一个hello.go文件,用Go语言重新实现C语言接口的SayHello()函数:

// hello.gopackage mainimport "C"import "fmt"//export SayHellofunc SayHello(s *C.char) { fmt.Print(C.GoString(s))}

我们通过CGO的//export SayHello指令将Go语言实现的函数SayHello()导出为C语言函数。为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。需要注意的是,这里其实有两个版本的SayHello()函数:一个是Go语言环境的;另一个是C语言环境的。CGO生成的C语言版本的SayHello()函数最终会通过桥接代码调用Go语言版本的SayHello()函数。

通过面向C语言接口的编程技术,不仅解放了函数的实现者,同时也简化了函数的使用。现在我们可以将SayHello()当作一个标准库的函数使用(和puts()函数的使用方式类似):

package main//#include import "C"func main() { C.SayHello(C.CString("Hello, World"))}

一切似乎都回到了开始的CGO代码,但是代码内涵更丰富了。

2.1.6 面向C接口的Go编程

在开始的例子中,全部CGO代码都在一个Go文件中。然后,通过面向C接口编程的技术将SayHello()分别拆分到不同的C文件,而main依然是Go文件。再用Go函数重新实现了C语言接口的SayHello()函数。但是对目前的例子来说只有一个函数,要拆分到3个不同的文件确实有些烦琐了。

正所谓合久必分、分久必合,我们现在尝试将例子中的几个文件重新合并到一个Go文件。下面是合并后的结果:

package main//void SayHello(char* s);import "C"import ( "fmt")func main() { C.SayHello(C.CString("Hello, World"))}//export SayHellofunc SayHello(s *C.char) { fmt.Print(C.GoString(s))}

现在版本的CGO代码中C语言代码的比例已经很少了,但是我们依然可以进一步以Go语言的思维来提炼我们的CGO代码。通过分析可以发现SayHello()函数的参数如果可以直接使用Go字符串是最直接的。在Go 1.10中CGO新增加了一个_GoString_预定义的C语言类型,用来表示Go语言字符串。下面是改进后的代码:

// +build go1.10package main//void SayHello(_GoString_ s);import "C"import ( "fmt")func main() { C.SayHello("Hello, World")}//export SayHellofunc SayHello(s string) { fmt.Print(s)}

虽然看起来全部是Go语言代码,但是执行的时候是先从Go语言的main()函数到CGO自动生成的C语言版本SayHello()桥接函数,最后又回到Go语言环境的SayHello()函数。这段代码包含了CGO编程的精华,读者需要深入理解。

思考

main()函数和SayHello()函数是否在同一个Goroutine里

执行?

2.2 CGO基础

要使用CGO特性,需要安装C/C++构建工具链,在macOS和Linux下需要安装GCC,在Windows下需要安装MinGW工具。同时需要保证环境变量CGO_ENABLED被设置为1,这表示CGO是被启用的状态。在本地构建时CGO默认是启用的,在交叉构建时CGO默认是禁止的。例如要交叉构建ARM环境运行的Go程序,需要手工设置好C/C++交叉构建的工具链,同时开启CGO_ENABLED环境变量。然后通过import "C"语句启用CGO特性。

2.2.1 import "C"语句

如果在Go代码中出现了import "C"语句,则表示使用了CGO特性,紧临这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。当确保CGO启用的情况下,还可以在当前目录中包含C/C++对应的源文件。

举个最简单的例子:

package main/*#include void printint(int v) { printf("printint: %d