go完全静态编译

go完全静态编译

文本将简单介绍go静态编译。

前提

在使用 go 编写的代码且将其打包为二进制文件后,尽管机器的架构相同,但是不同操作系统(如 UbuntuCentOS)可能无法运行,例如:

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)
$

所以当helloubuntu挪到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
$ 

此时再将helloubuntu 拷贝到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 类型的 ab 相加,最后再加上 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,即可完成静态编译需求。最后,功能完全实现了,且没有依赖任何动态运行库。

交叉编译

交叉编译的前提是静态编译,只有静态编译的前提下,才能进行交叉编译,而使用交叉编译非常简答,进需要指定GOOSGOARCH即可,比如:

交叉编译的前提是静态编译,只有在静态编译的基础上,才能进行交叉编译,否则动态依赖非常难以解决。使用交叉编译非常简单,只需要指定 GOOSGOARCH 环境变量即可。比如:

ini 复制代码
GOOS=linux GOARCH=arm64 go build

即可编译linux下的arm64架构的可执行文件。

目前GOOSGOARCH支持的值如下:(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"' 来进行静态编译,可以确保生成的可执行文件不依赖外部的动态库,从而实现跨平台运行或独立部署。

文章转载:wangli2025.github.io/2025/02/28/...

相关推荐
没逻辑16 小时前
Go 内存逃逸分析详解
后端·go
凉凉的知识库21 小时前
搞懂常见Go ORM系列-开篇
后端·go·orm
川Princess1 天前
【问题记录】Go项目Docker中的consul访问主机8080端口被拒绝
docker·go·consul
竹等寒2 天前
Go红队开发—编解码工具
开发语言·笔记·后端·安全·golang·go
沐风ya2 天前
golang介绍,特点,项目结构,基本变量类型与声明介绍(数组,切片,映射),控制流语句介绍(条件,循环,switch case)
开发语言·go
寻月隐君2 天前
Go语言错误处理全攻略:从基础到优雅实践
后端·go·github
一个热爱生活的普通人3 天前
浅谈 Go 的 Web 框架 Echo 是如何处理 RESTful 调用的
后端·go
一个热爱生活的普通人4 天前
深入解析Go语言Channel的底层实现与高效使用实践
后端·go
吃汤圆的抹香鲸4 天前
GoLand 安装包 绿色版 Win,Mac,Linux 包含IntelliJ全家桶 专为Go语言设计的集成开发环境(IDE)
linux·windows·macos·go·intellij-idea·go1.19