2.1 快速入门

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

2.1.1 最简CGO程序

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

// hello.go
package main

import "C"

func main() {
    println("hello cgo")
}
import "C"go build

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

第一章那个CGO程序还不够简单,我们现在来看看更简单的版本:

// hello.go
package main

//#include <stdio.h>
import "C"

func main() {
    C.puts(C.CString("Hello, World\n"))
}
import "C"C.CStringC.puts
C.CStringputsfputs
C.CString

2.1.3 使用自己的C函数

SayHelloSayHello
// hello.go
package main

/*
#include <stdio.h>

static void SayHello(const char* s) {
    puts(s);
}
*/
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}
SayHello
SayHello.cstatic
// hello.c

#include <stdio.h>

void SayHello(const char* s) {
    puts(s);
}
SayHello
// hello.go
package main

//void SayHello(const char* s);
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}
go run hello.gogo build hello.gogo run "your/package"go build "your/package"go run .go build
SayHelloSayHello

2.1.4 C代码的模块化

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

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

// hello.h
void SayHello(const char* s);

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

// hello.c

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

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

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

// hello.cpp

#include <iostream>

extern "C" {
    #include "hello.h"
}

void SayHello(const char* s) {
    std::cout << s;
}
std::coutextern "C"

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

2.1.5 用Go重新实现C函数

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

// hello.h
void SayHello(/*const*/ char* s);

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

// hello.go
package main

import "C"

import "fmt"

//export SayHello
func SayHello(s *C.char) {
    fmt.Print(C.GoString(s))
}
//export SayHelloSayHelloSayHello

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

package main

//#include <hello.h>
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

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

2.1.6 面向C接口的Go编程

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

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

package main

//void SayHello(char* s);
import "C"

import (
    "fmt"
)

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

//export SayHello
func SayHello(s *C.char) {
    fmt.Print(C.GoString(s))
}
SayHello_GoString_
// +build go1.10

package main

//void SayHello(_GoString_ s);
import "C"

import (
    "fmt"
)

func main() {
    C.SayHello("Hello, World\n")
}

//export SayHello
func SayHello(s string) {
    fmt.Print(s)
}
mainSayHelloSayHello

思考题: main函数和SayHello函数是否在同一个Goroutine里执行?