1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 04:22:22 +00:00

ch2.1: 回炉重炼

This commit is contained in:
chai2010 2018-04-23 14:32:28 +08:00
parent 82b23bedd3
commit 9badd9b82b

View File

@ -1,8 +1,22 @@
# 2.1. 快速入门 # 2.1. 快速入门
在第一章的“Hello, World 的革命”一节中我们已经见过一个CGO程序。这一节我们将重新给出三个版本的CGO实现来简单展示CGO的用法。 在第一章的“Hello, World 的革命”一节中我们已经见过一个CGO程序。本节我们将通过由浅入深的一系列小例子来快速掌握CGO的基本用法。
## 基于C标准库 ## 最简CGO程序
真实的CGO程序一般都比较复杂。不过我们可以反其道而行之一个最简的CGO程序该是什么样的呢要构造一个最简CGO程序首先要去掉一起复杂的CGO特性同时要展示CGO程序和纯Go程序的差别来。下面是我们构建的最简CGO程序
```go
import "C"
func main() {
println("hello cgo")
}
```
代码通过`import "C"`语句启用CGO特性主函数只是通过Go内置的println函数输出字符串其中并没有任何和CGO相关的代码。虽然没有调用CGO的相关函数但是go build命令会在编译和链接阶段启动gcc编译器这已经是一个完整的CGO程序了。
## 基于C标准库函数输出字符串
第一章那个CGO程序还不够简单我们现在来看看更简单的版本 第一章那个CGO程序还不够简单我们现在来看看更简单的版本
@ -17,7 +31,7 @@ func main() {
} }
``` ```
我们通过`import "C"`语句启用CGO特性同时包含C语言的`<stdio.h>`头文件。然后通过CGO包的`C.CString`函数将Go语言字符串转为C语言字符串最后调用C语言的`C.puts`函数向标准输出窗口打印转换后的C字符串。 我们不仅仅通过`import "C"`语句启用CGO特性同时包含C语言的`<stdio.h>`头文件。然后通过CGO包的`C.CString`函数将Go语言字符串转为C语言字符串最后调用C语言的`C.puts`函数向标准输出窗口打印转换后的C字符串。
相比“Hello, World 的革命”一节中的CGO程序最大的不同是我们没有在程序退出前释放`C.CString`创建的C语言字符串还有我们改用`puts`函数直接向标准输出打印,之前是采用`fputs`向标准输出打印。 相比“Hello, World 的革命”一节中的CGO程序最大的不同是我们没有在程序退出前释放`C.CString`创建的C语言字符串还有我们改用`puts`函数直接向标准输出打印,之前是采用`fputs`向标准输出打印。
@ -73,39 +87,95 @@ func main() {
既然`SayHello`函数已经放到独立的C文件中了我们自然可以将对应的C文件编译打包为静态库或动态库文件供使用。如果是以静态库或动态库方式引用`SayHello`函数的话需要将对应的C源文件移出当前目录CGO构建程序会自动构建当前目录下的C源文件从而导致C函数名冲突。关于静态库等细节将在稍后章节讲解。 既然`SayHello`函数已经放到独立的C文件中了我们自然可以将对应的C文件编译打包为静态库或动态库文件供使用。如果是以静态库或动态库方式引用`SayHello`函数的话需要将对应的C源文件移出当前目录CGO构建程序会自动构建当前目录下的C源文件从而导致C函数名冲突。关于静态库等细节将在稍后章节讲解。
## 用Go来实现C函数 ## C代码的模块化
其实CGO不仅仅用于Go语言中调用C语言函数还可以用于导出Go语言函数给C语言函数调用。 在编程过程中抽象和模块化是将复杂问题简化的通用手段。当代码语句变多时我们可以将相似的代码封装到一个个函数中当程序中的函数变多时我们将函数拆分到不同的文件或模块中。而模块化编程的核心是面向程序接口编程这里的接口并不是Go语言的interface而是API的概念
在前面的例子中我们可以抽象一个名为hello的模块模块的全部接口函数都在hello.h头文件定义
```c
// hello.h
void SayHello(const char* s);
```
其中只有一个SayHello函数的声明。但是作为hello模块的用户来说就可以放心地使用SayHello函数二无需关心函数的具体实现。而作为SayHello函数的实现者来说函数的实现只要满足头文件中函数的声明的规范即可。下面是SayHello函数的C语言实现对应hello.c文件
```c
// hello.c
#include "hello.h"
void SayHello(const char* s) {
puts(s);
}
```
在hello.c文件的开头实现者通过`#include "hello.h"`语句包含SayHello函数声明的签名这样可以保证函数的实现满足模块对外公开的接口。
接口文件hello.h是hello模块的实现者和使用者共同的约定但是该约定并没有要求必须使用C语言来实现SayHello函数。我们也可以用C++语言来重新实现这个C语言函数
```c++
// hello.cpp
#include <iostream>
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函数。
## 用Go重新实现C函数
其实CGO不仅仅用于Go语言中调用C语言函数还可以用于导出Go语言函数给C语言函数调用。在前面的例子中我们已经抽象一个名为hello的模块模块的全部接口函数都在hello.h头文件定义
```c
// hello.h
void SayHello(/*const*/ char* s);
```
现在我们创建一个hello.go文件来用Go语言重新实现C语言接口的SayHello函数:
```go
// hello.go
package main
//export SayHello
func 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函数的使用方式类似
```go ```go
package main package main
/* //#include <hello.h>
#include <stdio.h>
void cgoPuts(char* s);
static void SayHello(const char* s) {
cgoPuts((char*)(s));
}
*/
import "C" import "C"
func main() { func main() {
C.SayHello(C.CString("Hello, World\n")) C.SayHello(C.CString("Hello, World\n"))
} }
//export cgoPuts
func cgoPuts(s *C.char) {
fmt.Print(C.GoString(s))
}
``` ```
我们通过CGO的`//export cgoPuts`指令将Go语言实现的函数`cgoPuts`导出给C语言函数使用。然后在C语言版本的`SayHello`函数中,用`cgoPuts`替换之前的`puts`函数调用。在使用之前,同样要先声明`cgoPuts`函数。 一切似乎都回到了开始的CGO代码但是代码内涵更丰富了。
需要主要的是,这里其实有两个版本的`cgoPuts`函数一个Go语言环境的另一个是C语言环境的。在C语言环境中`SayHello`调用的也是C语言环境的`cgoPuts`函数这是CGO自动生成的桥接函数内部会调用Go语言环境的`cgoPuts`函数。因此我们也可以直接在Go语言环境中调用C语言环境的`cgoPuts`函数。 ## 面向C接口的Go编程
现在我们可以改用Go语言重新实现C语言接口的`SayHello`函数,然后在`main`函数中还是和之前一样调用`C.SayHello`实现输出: 在开始的例子中我们的全部CGO代码都在一个Go文件中。然后通过面向C接口编程的技术将SayHello分别拆分到不同的C文件而main依然是Go文件。再然后是用Go函数重新实现了C语言接口的SayHello函数。但是对于目前的例子来说只有一个函数要拆分到三个不同的文件确实有些繁琐了。
正所谓合久必分、分久必合我们现在尝试将例子中的几个文件重新合并到一个Go文件。下面是合并后的成果
```go ```go
package main package main
@ -127,8 +197,7 @@ func SayHello(s *C.char) {
} }
``` ```
现在版本的CGO代码中C语言代码的比例已经很少了但是我们依然可以进一步以Go语言的思维来提炼我们的CGO代码。通过分析可以发现`SayHello`函数的参数如果可以直接使用Go字符串是最直接的。在Go1.10中CGO新增加了一个`_GoString_`预定义的C语言类型用来表示Go语言字符串。下面是改进后的代码
现在中国版本的CGO代码中C语言代码的比例已经很少了但是我们依然可以进一步以Go语言的思维来提炼我们的CGO代码。通过分析可以发现`SayHello`函数的参数如果可以直接使用Go字符串是最直接的。在Go1.10中CGO新增加了一个`_GoString_`预定义的C语言类型用来表示Go语言字符串。下面是改进后的代码
```go ```go
// +build go1.10 // +build go1.10
@ -152,7 +221,6 @@ func SayHello(s string) {
} }
``` ```
虽然看起来全部是Go语言代码但是执行的时候是先从Go语言的`main`函数到CGO自动生成的C语言版本`SayHello`桥接函数最后又回到了Go语言环境的`SayHello`函数。虽然看起来有点绕但CGO确实是这样运行的 虽然看起来全部是Go语言代码但是执行的时候是先从Go语言的`main`函数到CGO自动生成的C语言版本`SayHello`桥接函数最后又回到了Go语言环境的`SayHello`函数。这个代码包含了CGO编程的精华读者需要深入理解
思考题: main函数和SayHello函数是否在同一个Goroutine只执行
需要注意的是CGO导出Go语言函数时函数参数中不再支持C语言中`const`修饰符。