最近在看runtime源码时,注意到程序启动的汇编代码中,有许多Cgo关键字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
nocpuinfo:
    // if there is an _cgo_init, call it.
    MOVQ    _cgo_init(SB), AX
    TESTQ   AX, AX
    JZ  needtls
    // g0 already in DI
    MOVQ    DI, CX  // Win64 uses CX for first parameter
    MOVQ    $setg_gcc<>(SB), SI
    CALL    AX
 ...

之前在写网关SDK时,曾经尝试关闭Cgo来减少程序体积(交叉编译时,Cgo默认已经是关闭的),然而并没有太明显的效果,只减少了大概100KB左右体积。

Cgo赋予了go语言调用C代码的能力(类似Swift调用C代码),在go自举前,go编译器及运行时都是使用C语言实现的,以至于到现在(go 1.12)标准库及运行时内仍旧遗留着许多C代码和汇编,自举后的代码也是C风格的。

对于C的使用,runtime里主要使用涉及操作系统相关参数定义,runtime以外的标准库则引用的比较多了,例如

  • cmd/dist
  • internal/x/route
  • net
  • os/signal/internal/pty
  • os/user
  • plugin
  • syscall

上述除了plugin外,主要也是使用操作系统相关的参数定义,在执行系统调用或者C方法后,做参数转换。

关于Cgo的文档,自然是官方的最为详尽:Command cgo

下面是执行go env获得的输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/root/go"
GOPROXY=""
GORACE=""
GOROOT="/opt/go"
GOTMPDIR=""
GOTOOLDIR="/opt/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build103599916=/tmp/go-build -gno-record-gcc-switches"

从上面可以看到,CGO_ENABLED默认是开启的,结合官方文档,记录以下几个要点

(1)go安装包默认设置CGO_ENABLED=1,以确保最大的兼容性,因为无法确保所有引用的第三方库都没有使用C代码,例如tensorflow官方go库也是通过Cgo调用C/C++库。

(2)native编译时,默认启用Cgo,可以认为本机上具备C环境(linux下为gcc及glibc),相关C代码的能够正常编译与运行,linux下使用ldd工具查看编译出的程序时,会发现动态链接到glibc库;交叉编译时,默认关闭Cgo,执行静态编译,如果已经配置好C交叉编译环境,并设置了Cgo相关的flag时,可以手动设置CGO_ENABLED启用Cgo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// go程序默认启用Cgo并动态链接到glibc的相关库
➜  ~ ldd /opt/go/bin/go
        linux-vdso.so.1 =>  (0x00007ffc949d5000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fc07a822000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fc07a455000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc07aa3e000)
        
// 未配置C交叉编译环境时,在amd64平台上启用Cgo编译arm平台程序报错
➜  test git:(gateway) CGO_ENABLED="1" make build
Compiling source for linux arm
GOOS=linux GOARCH=arm go build  -ldflags "-s -w -X main.version=0.0.1-g7992536" -o test main.go
# runtime/cgo
gcc: error: unrecognized command line option '-marm'
make: *** [build] Error 2

// 交叉编译出的程序默认无动态链接
➜  test git:(gateway) make build
Compiling source for linux arm
GOOS=linux GOARCH=arm go build  -ldflags "-s -w -X main.version=0.0.1-g7992536" -o test main.go
➜  lora-gateway-bridge git:(gateway) ldd test
        not a dynamic executable

(3)由于go默认启用Cgo,标准静态库也启用Cgo,如果禁用Cgo编译程序,所有引用C代码的go文件将被忽略,所有调用的标准库会重新编译,导致首次编译速度下降,后续的编译会利用缓存,恢复正常构建速度。

(4)go是具备垃圾回收的语言,而C的内存管理需要用户自行解决,go调用C代码时,运行时负责处理好C运行环境,给C代码一个非分段的栈空间并让它脱离与调度系统的交互,函数执行完毕后将返回值转换成go的值。

(5)go 1.5后可以使用c-shared模式构建动态库,生成C风格的API,C调用go代码时,通过链接动态库执行go方法,其他语言也可以使用调用C代码的方式引用go,例如wireguard跨平台协议实现使用go编写,iOS、安卓、windows等使用原生代码实现UI,通过链接库调用go代码。

(6)gccgo与Cgo是两个东西,gccgo是编译器,是一个新的GCC前端,支持更多的平台和优化(例如可以编译出更小的可执行程序,但实测没有明显的差异),开发进度没有gc快,一些新特性只有gc支持。