go完全静态编译
文本将简单介绍go
静态编译。
前提
在使用 go
编写的代码且将其打包为二进制文件后,尽管机器的架构相同,但是不同操作系统(如 Ubuntu
和 CentOS
)可能无法运行,例如:
在Ubuntu 22.04.4 LTS
机器下编写如下代码:
bash
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("hello world")
net.ResolveIPAddr("tcp", "127.0.0.1")
}
编译完成后,在本机可以成功运行。
bash
$ go build
$ ./hello
hello world
$
当将该二进制hello
拷贝到CentOS Linux 7 (Core)
上运行的时候,却会报错。
bash
# ./hello
./hello: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./hello)
./hello: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by ./hello)
#
其根本原因是由于程序依赖了第三方运行库,而这些运行库在目标机器上并不存在或不兼容,但是运行出现异常。
静态编译基本操作
对于二进制文件,在linux
中可以使用ldd
查看其依赖的运行库,例如上面的hello
可执行文件。
bash
$ ldd hello
linux-vdso.so.1 (0x00007fff037bf000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f72f1955000)
/lib64/ld-linux-x86-64.so.2 (0x00007f72f1b88000)
$
所以当hello
从ubuntu
挪到centos
的时候,虽然其架构都是一致的,但是centos
没有这些依赖库,所以会报错。
所以所谓的静态编译,就是去除这些动态依赖,在go
中,可以使用CGO_ENABLED=0
来禁用cgo
配置,目的是生成完全静态的二进制文件,摆脱对动态库的依赖。
在使用中,一般语法为:
bash
CGO_ENABLED=0 go build -o hello
上述先设置了一个环境变量
CGO_ENABLED
,其值为0
,且仅对该条命令生效,这是bash
特性,在其他环境下需要注意切换设置方式。
编译完成后,再次使用ldd
进行查看该二进制文件,会发现其已经没有动态依赖库了。
bash
$ ldd hello
not a dynamic executable
$
此时再将hello
从ubuntu
拷贝到centos
也可以同样运行了。
bash
# ./hello
hello world
#
静态编译是如何实现的
通过上述例子,我们可以看出静态编译似乎是一项简单的事情,然而,实际情况却并非如此。如果不启用 cgo
的话,Go
就需要为相关功能提供 Go
版本的实现,才能确保静态编译成功,否则依然会遇到错误。
这里举个简单的例子。当我们只有 C
代码实现的功能时,如果关闭 cgo
进行编译,就会出现问题。例如,假设有如下代码:
go
package main
import "fmt"
/*
# include <stdio.h>
int Add(int a,int b) {
return a + b + 2;
}
*/
import "C"
func Add(a int, b int) int {
cIntA := C.int(a)
cintB := C.int(b)
cIntAddNum := C.Add(cIntA, cintB)
return int(cIntAddNum)
}
func main() {
result := Add(2, 8)
fmt.Println("Result:", result)
}
上述代码非常简单,使用 C
编写了一个 Add
函数,它将传入的 int
类型的 a
和 b
相加,最后再加上 2。
当使用默认方式进行编译(启用 cgo
)时,可以顺利执行,例如:
bash
➜ cgoTests go build -o hello
➜ cgoTests ./hello
Result: 12
➜ cgoTests ldd hello
linux-vdso.so.1 (0x00007ffd4d139000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5ea7f4d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5ea8180000)
➜ cgoTests
当使用静态编译时(即禁用 cgo
),则会报错,因为代码中使用了 cgo
,但编译时却禁用了它。
bash
➜ cgoTests CGO_ENABLED=0 go build -o hello
package hello: build constraints exclude all Go files in /data/code/golang/cgoTests
➜ cgoTests
所以说,要实现这一功能,我们需要在 Go
代码中重新实现 Add
函数。这样,如果启用了 cgo
编译,程序将调用 C
代码;否则,则会调用 Go
代码实现。
因此,我们将上述代码中的 cgo
部分分离出来,组织成多个文件,例如有如下几个文件:
bash
➜ cgoTests tree | grep '\.go'
├── add_c.go
├── add_go.go
└── main.go
➜ cgoTests
其中文件中,add_c.go
文件包含使用 C
代码实现的 Add
函数,而 add_go.go
文件则实现了使用 Go
代码的 Add
函数。而main.go
则无脑调用Add()
函数即可。
具体的代码内容如下:
main.go
:
go
➜ cgoTests cat main.go
package main
import "fmt"
func main() {
result := Add(2, 8)
fmt.Println("Result:", result)
}
➜ cgoTests
上述代码仅调用了一个 Add
函数,并且将结果放入到result
中,然后输出它。
add_go.go
:
go
➜ cgoTests cat add_go.go
//go:build !cgo
// +build !cgo
package main
func Add(a int, b int) int {
return a + b
}
➜ cgoTests
上述代码使用 Go
实现了 Add
函数,并在文件最上方添加了 go:build !cgo
和 +build !cgo
。这表示在禁用 cgo
时,编译器会编译该文件中的代码。
add_c.go
:
go
//go:build cgo
// +build cgo
package main
/*
# include <stdio.h>
int Add(int a,int b) {
return a + b + 2;
}
*/
import "C"
func Add(a int, b int) int {
cIntA := C.int(a)
cintB := C.int(b)
cIntAddNum := C.Add(cIntA, cintB)
return int(cIntAddNum)
}
上述代码使用 C
编写了 Add
函数,并定义了一个 Go
版的 Add
函数来调用 C
代码的 Add
函数。同时,在文件最上方添加了 //go:build cgo
和 // +build cgo
,这表示在使用 cgo
编译时,编译器会编译该文件中的代码。
如上所述,已经有了 C
实现的 Add
函数和 Go
实现的 Add
函数,因此无论是采用静态编译还是动态编译,都可以成功编译。例如:
禁用cgo
编译:
bash
➜ cgoTests CGO_ENABLED=0 go build -o hello
➜ cgoTests ./hello
Result: 10
➜ cgoTests ldd hello
not a dynamic executable
➜ cgoTests
如上所述,当禁用 cgo
时,程序会执行 Go
编写的 Add
函数,该函数仅将传入的数值相加并返回 10
。同时,查看该可执行文件的依赖库时,会发现它是空的。
启用cgo
编译:
bash
➜ cgoTests go build -o hello
➜ cgoTests ./hello
Result: 12
➜ cgoTests ldd hello
linux-vdso.so.1 (0x00007ffcae6b8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdd3dd5c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdd3df8f000)
➜ cgoTests
默认情况下,go build
会启用 cgo
进行编译,编译后的结果也正如预期。C
编写的 Add
函数不仅将传入的数值相加,还加了一个 2
,因此得到的结果是 12
。同时,查看该可执行文件的依赖库时,会发现它并不为空,有动态依赖库。这意味着编译和执行都成功完成。
完全静态编译
上述操作确实比较麻烦,因为需要用 Go
实现与 C
相同的代码逻辑,相当于重写了一遍。其实,直接将 C
编写的代码进行静态编译也是可行的,方法是使用以下命令:
bash
CGO_ENABLED=1 go build -ldflags '-extldflags "-static"'
首先,需要开启 cgo
编译,其次需要指定 extldflags
的值为 static
,这表示链接器使用静态链接,避免依赖共享库,从而生成一个完全静态的可执行文件。
通过这种方式,C
代码会被静态链接到最终的可执行文件中,可以避免用go
重写代码逻辑的麻烦。
例如,回到最开始的问题,假设有如下带有cgo
的代码,您需要进行静态编译:
go
package main
import "fmt"
/*
# include <stdio.h>
int Add(int a,int b) {
return a + b + 2;
}
*/
import "C"
func Add(a int, b int) int {
cIntA := C.int(a)
cintB := C.int(b)
cIntAddNum := C.Add(cIntA, cintB)
return int(cIntAddNum)
}
func main() {
result := Add(2, 8)
fmt.Println("Result:", result)
}
可以使用 -extldflags "-static"
进行静态编译,例如:
bash
➜ cgoTests CGO_ENABLED=1 go build -ldflags '-extldflags "-static"' -o hello
➜ cgoTests ./hello
Result: 12
➜ cgoTests ldd hello
not a dynamic executable
➜ cgoTests
首先,这里需要开启 cgo
,因为静态编译需要借助 gcc
来完成。然后,添加 ldflags
参数,其中 extldflags
的值为 static
,即可完成静态编译需求。最后,功能完全实现了,且没有依赖任何动态运行库。
交叉编译
交叉编译的前提是静态编译,只有静态编译的前提下,才能进行交叉编译,而使用交叉编译非常简答,进需要指定GOOS
和GOARCH
即可,比如:
交叉编译的前提是静态编译,只有在静态编译的基础上,才能进行交叉编译,否则动态依赖非常难以解决。使用交叉编译非常简单,只需要指定 GOOS
和 GOARCH
环境变量即可。比如:
ini
GOOS=linux GOARCH=arm64 go build
即可编译linux
下的arm64
架构的可执行文件。
目前GOOS
和GOARCH
支持的值如下:(go.dev/doc/install...)
$GOOS |
$GOARCH |
---|---|
aix |
ppc64 |
android |
386 |
android |
amd64 |
android |
arm |
android |
arm64 |
darwin |
amd64 |
darwin |
arm64 |
dragonfly |
amd64 |
freebsd |
386 |
freebsd |
amd64 |
freebsd |
arm |
illumos |
amd64 |
ios |
arm64 |
js |
wasm |
linux |
386 |
linux |
amd64 |
linux |
arm |
linux |
arm64 |
linux |
loong64 |
linux |
mips |
linux |
mipsle |
linux |
mips64 |
linux |
mips64le |
linux |
ppc64 |
linux |
ppc64le |
linux |
riscv64 |
linux |
s390x |
netbsd |
386 |
netbsd |
amd64 |
netbsd |
arm |
openbsd |
386 |
openbsd |
amd64 |
openbsd |
arm |
openbsd |
arm64 |
plan9 |
386 |
plan9 |
amd64 |
plan9 |
arm |
solaris |
amd64 |
wasip1 |
wasm |
windows |
386 |
windows |
amd64 |
windows |
arm |
windows |
arm64 |
总结
所谓的动态编译和静态编译,实际上是指在编译过程中是否引用了动态运行库。只有当可执行文件不依赖于任何动态运行库时,它才能在同平台环境下独立执行,这也是交叉编译的基础(如果你热衷解决依赖则另说)。如果只有 cgo
实现部分功能代码,则需要指定 -ldflags '-extldflags "-static"'
来进行静态编译,可以确保生成的可执行文件不依赖外部的动态库,从而实现跨平台运行或独立部署。