# Go语言之交叉编译
文章目录
前言
当初学习 go 语言的原因之一就是看中了 go 可以直接编译成机器码运行,并且支持跨操作系统的交叉编译,这对开发跨操作系统软件提供了极大的便利,这篇文章目的就是记录下 go 是如何交叉编译的。
交叉编译
go 语言里交叉编译支持非常多的操作系统,可以通过go tool dist list命令来查看支持的操作系统列表。
$ go tool dist listaix/ppc64android/386android/amd64android/armandroid/arm64darwin/386darwin/amd64darwin/armdarwin/arm64dragonfly/amd64freebsd/386freebsd/amd64freebsd/armillumos/amd64js/wasmlinux/386linux/amd64linux/armlinux/arm64linux/mipslinux/mips64linux/mips64lelinux/mipslelinux/ppc64linux/ppc64lelinux/s390xnacl/386nacl/amd64p32nacl/armnetbsd/386netbsd/amd64netbsd/armnetbsd/arm64openbsd/386openbsd/amd64openbsd/armopenbsd/arm64plan9/386plan9/amd64plan9/armsolaris/amd64windows/386windows/amd64windows/arm编译的时候只需要指定环境变量GOOS(系统内核)和GOARCH(CPU 架构)即可进行交叉编译。
示例
- Windows 上编译 Mac 和 Linux 上 64 位可执行程序
SET CGO_ENABLED=0SET GOOS=darwinSET GOARCH=amd64go build main.go
SET CGO_ENABLED=0SET GOOS=linuxSET GOARCH=amd64go build main.go- Linux 上编译 Mac 和 Windows 上 64 位可执行程序
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.goCGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go- Mac 上编译 Linux 和 Windows 上 64 位可执行程序
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.goCGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.gocgo 程序交叉编译
上面的示例都是基于程序里没有使用cgo的情况下进行的,可以看到CGO_ENABLED=0这个选项就是关闭cgo,因为 go 的交叉编译是不支持cgo的,如果程序里使用到了cgo时要进行交叉编译就没这么简单了,需要安装一个跨平台的 C/C++ 编译器才可能实现交叉编译。
好在已经有大佬把常用的编译环境都做成到 docker 镜像了,并且提供命令行工具让我们很方便的进行交叉编译,这个工具就是https://github.com/karalabe/xgo,但是此仓库作者好像不怎么更新了,并且不支持go mod,于是我找到了另一位大佬的 fork: https://github.com/techknowlogick/xgo,支持go mod并且支持最新的go 1.13版本。
xgo 示例
首先要保证机器上有安装 golang 和 docker,接着按照教程来进行。
- 拉取镜像
镜像比较大,1 个多 G,拉取要一点时间
docker pull techknowlogick/xgo:latest- 安装 xgo
go get src.techknowlogick.com/xgo- 准备代码
这里引用了go-sqlite3这个库,里面用到了cgo。
package main
import ( "database/sql" "fmt" "log" "os"
_ "github.com/mattn/go-sqlite3")
func main() { os.Remove("./foo.db")
db, err := sql.Open("sqlite3", "./foo.db") if err != nil { log.Fatal(err) } defer db.Close()
sqlStmt := ` create table foo (id integer not null primary key, name text); delete from foo; ` _, err = db.Exec(sqlStmt) if err != nil { log.Printf("%q: %s\n", err, sqlStmt) return }
tx, err := db.Begin() if err != nil { log.Fatal(err) } stmt, err := tx.Prepare("insert into foo(id, name) values(?, ?)") if err != nil { log.Fatal(err) } defer stmt.Close() for i := 0; i < 100; i++ { _, err = stmt.Exec(i, fmt.Sprintf("こんにちわ世界%03d", i)) if err != nil { log.Fatal(err) } } tx.Commit()
rows, err := db.Query("select id, name from foo") if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var id int var name string err = rows.Scan(&id, &name) if err != nil { log.Fatal(err) } fmt.Println(id, name) } err = rows.Err() if err != nil { log.Fatal(err) }
stmt, err = db.Prepare("select name from foo where id = ?") if err != nil { log.Fatal(err) } defer stmt.Close() var name string err = stmt.QueryRow("3").Scan(&name) if err != nil { log.Fatal(err) } fmt.Println(name)
_, err = db.Exec("delete from foo") if err != nil { log.Fatal(err) }
_, err = db.Exec("insert into foo(id, name) values(1, 'foo'), (2, 'bar'), (3, 'baz')") if err != nil { log.Fatal(err) }
rows, err = db.Query("select id, name from foo") if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var id int var name string err = rows.Scan(&id, &name) if err != nil { log.Fatal(err) } fmt.Println(id, name) } err = rows.Err() if err != nil { log.Fatal(err) }}- 编译
在项目目录下运行,编译 Mac,Windows,Linux 下的 64 位可执行程序,-ldflags="-w -s"选项可以减小编译后的程序体积。
xgo -targets=darwin/amd64,windows/amd64,linux/amd64 -ldflags="-w -s" .这样使用xgo轻松就完成了多操作系统的交叉编译,并且xgo还有很多的特性,可以自行去 github 上看看。
使用 xgo 碰到的问题
上面提到使用techknowlogick/xgo可以解决 go mod 交叉编译的问题,但是默认只会编译项目根目录下的 main.go 文件,例如:
.├── main.go└── README.md但是我需要编译指定目录下的main.go文件,例如:
.├── cmd│ └── main.go├── main.go└── README.md需要编译 cmd/main.go,通过查看 xgo 源码可以看到有个-pkg的参数就是用于指定编译路径的,遂尝试使用-pkg进行交叉编译:
xgo -targets=darwin/amd64,windows/amd64,linux/amd64 -ldflags="-w -s" -pkg=cmd/main.go .然而并没有编译成功,然后去看了一下源码,最后在build.sh:154 这行代码发现,如果启用了 go mod 的话,pkg 参数就会失效,为什么要这样做我也没太明白,不过既然知道原因了那就 fork 一份修复吧。
最终代码为:
# Go module-based builds error with 'cannot find main module'# when $PACK is definedif [[ "$USEMODULES" = true ]]; then NAME=`sed -n 's/module\ \(.*\)/\1/p' /source/go.mod`fi
# Support go module packagePACK_RELPATH="./$PACK"顺便把所有的 Dockerfile 更新了一遍,把过时的MAINTAINER替换成LABEL MAINTAINER="",接着就开始构建 docker 镜像:
docker build -t liwei2633/xgo:base docker/base但是由于国内的网络问题,经常有请求超时导致构建失败,所以一次都没构建成功过,然后想起来前几天申请的github actions内测资格通过了,可以试试github actions,因为github actions用的国外网络环境,应该构建不成问题,于是按照官方文档写了一个构建配置.github/workflows/main.yml:
name: CI
# 当master分支有push时,并且docker/base目录下文件发生变动时触发构建on: push: branches: - master paths: - "docker/base/*"
jobs: build: runs-on: ubuntu-latest
steps: # 拉取源码 - name: Checkout source uses: actions/checkout@v1 # 登录docker hub - name: Docker login run: docker login -u liwei2633 -p ${{ secrets.DOCKER_HUB_PWD }} - name: Docker build base run: | docker build -t liwei2633/xgo:base ./docker/base docker push liwei2633/xgo:base - name: Docker build other run: | docker build -t liwei2633/xgo:go-1.12.10 ./docker/go-1.12.10 docker push liwei2633/xgo:go-1.12.10 docker build -t liwei2633/xgo:go-1.12.x ./docker/go-1.12.x docker push liwei2633/xgo:go-1.12.x docker build -t liwei2633/xgo:go-1.13.1 ./docker/go-1.13.1 docker push liwei2633/xgo:go-1.13.1 docker build -t liwei2633/xgo:go-1.13.x ./docker/go-1.13.x docker push liwei2633/xgo:go-1.13.x docker build -t liwei2633/xgo:go-latest ./docker/go-latest docker push liwei2633/xgo:go-latest然后推送代码触发构建,这次构建成功了,但是非常的耗时大概要 30 多分钟,得想办法优化优化,由于github actions是不支持缓存的,即每次构建都是一个全新的虚拟机,没法入手,只能通过 docker 的缓存机制来进行优化了,通过 google 发现有一个--cache-from的构建参数,可以指定构建缓存镜像来源,于是构建流程改造成先pull历史镜像,再指定历史镜像作为构建缓存镜像来进行构建,具体脚本如下:
docker pull liwei2633/xgo:basedocker build --cache-from=liwei2633/xgo:base -t liwei2633/xgo:base ./docker/basedocker push liwei2633/xgo:base这样的话因为下载镜像的耗时远比 Dockerfile 里的各种apt-get install耗时要小,所以如果有了第一次构建的镜像之后,就可以通过 docker 的缓存机制来跳过许多耗时的步骤,就比如 xgo 中 Dockerfile 的一个RUN语句:
RUN \ apt-get update && \ apt-get install -y automake autogen build-essential ca-certificates \ gcc-5-arm-linux-gnueabi g++-5-arm-linux-gnueabi libc6-dev-armel-cross \ gcc-5-arm-linux-gnueabihf g++-5-arm-linux-gnueabihf libc6-dev-armhf-cross \ gcc-5-aarch64-linux-gnu g++-5-aarch64-linux-gnu libc6-dev-arm64-cross \ gcc-5-mips-linux-gnu g++-5-mips-linux-gnu libc6-dev-mips-cross \ gcc-5-mipsel-linux-gnu g++-5-mipsel-linux-gnu libc6-dev-mipsel-cross \ gcc-5-mips64-linux-gnuabi64 g++-5-mips64-linux-gnuabi64 libc6-dev-mips64-cross \ gcc-5-mips64el-linux-gnuabi64 g++-5-mips64el-linux-gnuabi64 libc6-dev-mips64el-cross \ gcc-5-multilib g++-5-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \ gcc-6-arm-linux-gnueabi g++-6-arm-linux-gnueabi libc6-dev-armel-cross \ gcc-6-arm-linux-gnueabihf g++-6-arm-linux-gnueabihf libc6-dev-armhf-cross \ gcc-6-aarch64-linux-gnu g++-6-aarch64-linux-gnu libc6-dev-arm64-cross \ gcc-6-mips-linux-gnu g++-6-mips-linux-gnu libc6-dev-mips-cross \ gcc-6-mipsel-linux-gnu g++-6-mipsel-linux-gnu libc6-dev-mipsel-cross \ gcc-6-mips64-linux-gnuabi64 g++-6-mips64-linux-gnuabi64 libc6-dev-mips64-cross \ gcc-6-mips64el-linux-gnuabi64 g++-6-mips64el-linux-gnuabi64 libc6-dev-mips64el-cross \ gcc-6-multilib gcc-7-multilib g++-6-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \ libtool libxml2-dev uuid-dev libssl-dev swig openjdk-8-jdk pkg-config patch \ make xz-utils cpio wget zip unzip p7zip git mercurial bzr texinfo help2man cmake \ --no-install-recommends这种情况下使用缓存无疑可以节省非常多的时间,修改之后提交,再看看构建记录:

可以看到已经走了缓存,最终耗时在 10 分钟左右节省了 2/3 的时间,github actions真香!!!
最后把xgo.go代码修改一下,顺带修复了个 BUG(docker image 检测问题),推到 github 上。
测试使用:
go get github.com/monkeyWie/xgoxgo -targets=windows/amd64,linux/amd64,darwin/amd64 -ldflags="-w -s" -pkg=cmd/main.go .成功编译,完结撒花!
后记
感谢 https://github.com/karalabe/xgo和https://github.com/techknowlogick/xgo为 go 交叉编译做出的贡献,然后就是github actions真香,希望可以早日推出正式版。