go语言自带的一个工具/特性,用来支持C语言函数调用,同时可以把go语言导出C动态库给其他语言使用。
默认情况下cgo是启用的,可以通过go env
命令查看CGO_ENABLED变量确认启用
通过import "C"
来启用cgo特性,同时包含C语言头文件
,来使用C.puts
,C.CString
。
import "C"
后的注释是C语言代码
把要调用的C和go写到一个文件,代码及执行效果:
把C代码和go代码分开,此时建立单独的hello.c文件,同时在项目目录运行go mod init gotest,新版本go mod默认启用,需要初始化后才可以编译包。
因为关注交叉编译,在此不再叙述如何用go导出C语言函数。
通过import "C"
前的注释语句设置
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include
import "C"
CGO在使用C/C++资源的时候三种形式:源码;静态链接库,动态链接库。使用源码就是第一部分讲的直接在源码中写C代码或者包含C源文件。
优势:生成的程序不会产生额外的运行时依赖
缺点:静态库包含了全部代码和符号信息,不同静态库可能出现符号冲突导致连接失败
目录结构:
(base) # susu @ susu-tools in ~/gotest [8:55:55]
$ tree
.
├── go.mod
├── main.go
└── number
├── libnumber.a
├── number.c
├── number.h
└── number.o
1 directory, 6 files
有一个库number,定义如下:
//number.h
int number_add_mod(int a, int b, int mod);
//number.c
#include "number.h"
int number_add_mod(int a, int b, int mod) {
return (a+b)%mod;
}
CGO使用gcc来编译连接C和Go桥接的代码,所以静态库需要用gcc来编译:
gcc -c -o number.o number.c
ar rcs libnumber.a number.o
生成的libnumber.a就是静态链接库。
main.go代码:
package main
//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR}/number -lnumber
//
//#include "number.h"
import "C"
import "fmt"
func main() {
fmt.Println(C.number_add_mod(10, 5, 12))
}
参数解释:
优点:节省内存和磁盘,隔离不同动态库减少符号冲突
缺点:运行时依赖对应的动态库
创建上述示例的动态库:
gcc -shared -o libnumber.so number.c
编译运行结果:
原因是没有把动态库文件放到默认的搜索路径去,所以执行的时候找不到。
执行sudo cp ./number/libnumber.so /usr/lib
拷贝到/usr/lib/
目录下,再运行就可以找到了
默认情况下有动态库有静态库,会选择动态连接
可以通过命令强制静态连接:go build --ldflags '-extldflags "-static"' ~/gotest
编译出的文件信息及运行结果:
internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时,cmd/link默认使用internal linking,而无需启动外部external linker(如:gcc、clang等),不过由于cmd/link功能有限,仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的,那么编译出来的最终二进制文件依旧是动态链接的,即便在go build时传入 -ldflags '-extldflags "-static"'
亦无用,因为根本没有使用external linker
这样就会出现下文中命令行带参数-ldflags '-extldflags "-static"'
,编译出来的还是会显示为动态连接。
而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中,再将其交给外部的链接器,比如gcc或clang去做最终链接处理。如果此时,我们在cmd/link的参数中传入 -ldflags '-linkmode "external" -extldflags "-static"'
,那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external
来强制cmd/link采用external linker
不用启用CGO的交叉编译,最简单
基础命令:
env CGO_ENABLED=0 GOOS=${os} GOARCH=${arch} GOMIPS=${gomips} go build -trimpath -ldflags "-s -w" -o ${bin_name} ${package_name}
其中编译参数-ldflags部分按需即可
${os},${arch},${gomips}
的可选组合,可以参考https://golang.org/doc/install/source#environment
$GOARM(默认为6),$GOMIPS,$GOPPC64
指定特定的架构版本
如何查看Go支持的系统和架构:go tool dist list
$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
举例编译windows x64的:GOOS=windows GOARCH=amd64 go build ~/gotest
文件信息:gotest.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
编译arm的:GOOS=linux GOARCH=arm go build ~/gotest
文件信息:gotest: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
纯go代码,禁用cgo,随便编译跨平台可执行文件,默认编译出的都是静态连接的
最早的版本:https://github.com/karalabe/xgo
已经不维护了,最后的go支持到1.13.15,且不支持go mod,在此不再介绍。
仍在维护,支持go mod,支持go 1.15,1.16版本,https://github.com/techknowlogick/xgo
安装之类的按照github项目主页的readme来进行就可以。
如果需要使用特定的go版本,在docker pull的时候进行指定,如:docker pull techknowlogick/xgo:go-1.16.3
以frp为例,因为frp算是一个典型的go项目,引用了很多github上的库,有助于测试网络问题,同时在一个项目中需要编译两个可执行文件,启用了go mod。选择的xgo docker的tag是latest。
编译命令:~/go/bin/xgo --pkg cmd/frps .
,参考这一节techknowlogick/xgo - Package selection
第一次编译,很明显因为网络问题挂了(注意这条命令不对,按上面写的命令输):
错误:Get "https://proxy.golang.org/github.com/armon/go-socks5/@v/v0.0.0-20160902184237-e75332964ef5.mod": dial tcp 172.217.160.81:443: i/o timeout
解决方案:
配置环境变量export GOPROXY=https://goproxy.io,direct
或者在执行时带上环境变量:GOPROXY=https://goproxy.io,direct ~/go/bin/xgo --pkg cmd/frps .
成功编译:
注意,默认编译出来的在当前目录,带包名目录的下面,也就是不在frp根目录下,而是在frp/github.com/fatedier/目录下生成可执行文件。
可以看到编译出的是动态连接到ld-linux.so的,那么是否可以编译出静态连接的呢?
我尝试在运行xgo时加上参数-ldflags '-s -w -extldflags "-static"'
但是会报警告:
/tmp/go-link-325119081/000004.o: In function `_cgo_26061493d47f_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:57: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
查阅资料显示会有一些依赖动态的加载扩展,但是并不是常用的功能,对于大多数程序可以安全的忽略该警告。(参考Statically compiling Go programs)
最终编译出的一些文件信息如下,不确定是否算是静态连接:
更新一下,运行xgo时加上参数
-ldflags '-linkmode "external" -extldflags "-static"'
,完整命令~/xgo -goproxy 'https://goproxy.io,direct' -ldflags '-linkmode "external" -extldflags "-static"' -pkg cmd/frps -x -docker-image crazymax/xgo:latest .
,编译出的可执行文件为静态连接。
文件信息:
github.com/fatedier/frp-linux-arm-5: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=71378ee03acfeb967920dda792808d587ca772f3, not stripped
项目主页:https://github.com/crazy-max/xgo
特性:支持命令行指定goproxy
按官方文档的说法是,交叉编译使用CGO的Go代码时,尤其是访问一些特定于操作系统的功能时(例如查询cpu负载),需要配置和维护单独的构建环境才可以,而xgo省略了这些事情。
编译命令:~/xgo -goproxy 'https://goproxy.io,direct' -dest ~/frp -pkg cmd/frps -docker-image crazymax/xgo:latest .
编译过程:
编译输出:
brew项目:https://github.com/FiloSottile/homebrew-musl-cross
编译器项目:https://github.com/richfelker/musl-cross-make
编译工具链:https://musl.cc/
mac下可以通过brew install FiloSottile/musl-cross/musl-cross
安装编译器,在Go编译的时候指定CC=x86_64-linux-musl-gcc
来选择需要的交叉编译器,通过CGO_LDFLAGS="-static"
来指定静态编译。
查看xgo的dockerfile构建时的build.sh,可以看到实际编译时使用的命令,如arm-5的编译命令:
CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 CGO_CFLAGS="-march=armv5" CGO_CXXFLAGS="-march=armv5" CGO_LDFLAGS="-static" go build -x -trimpath --ldflags='-s -w -extldflags "-static"' -o frps ./cmd/frps
上面的是我根据实际情况修改的,那么这样我们安装了交叉编译器sudo apt install gcc-6-arm-linux-gnueabi g++-6-arm-linux-gnueabi
,然后运行上述命令,可以得到编译后的文件信息
ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, stripped
这样也可以根据实际情况来手动编译了。
CGO_LDFLAGS="-static"
和--ldflags '-extldflags "-static"'
有何区别?《Go语言高级编程》 - 第2章 CGO编程
知乎专栏 - Go语言涉及CGO的交叉编译(跨平台编译)解决办法
知乎专栏 - 含有CGO代码的项目如何实现跨平台编译
Go官方文档 - Installing Go from source
Gist - Go (Golang) GOOS and GOARCH
Blog - Statically compiling Go programs
推荐 - CGO_ENABLED环境变量对Go静态编译机制的影响