Go 静态编译及在构建 docker 镜像时的应用

news/2025/3/17 17:54:39/文章来源:https://www.cnblogs.com/caipi/p/18342199

Go 语言具有跨平台和可移植的特点,同时还支持交叉编译,可以在一个系统上编译出运行在另一个系统上的二进制可执行文件,这是因为 Go 在编译时支持将依赖的库文件与源代码一起编译链接到二进制文件中,所以在实际运行时不再需要依赖运行环境中的库,而只需要一个二进制文件就可以运行,在构建 docker 镜像时就可以利用这个特点,实现减小镜像大小的目的,下面逐步介绍这中间涉及到的关键点。

链接库

什么是链接库,为什么要有链接库

链接库是高级语言经过编译后得到的二进制文件,其中包含有函数或数据,可以被其他应用程序调用,链接库根据链接方式的不同分为静态链接库和动态链接库。
以 C 语言标准 ISO C99 为例,它定义了一组广泛的标准 I/O、字符串操作和整数数学函数,例如 atoiprintfscanfstrcpyrand。它们在 libc.a 库中,对每个 C 程序来说都是可用的。ISO C99 还在 libm.a 库中定义了一组广泛的浮点数学函数,例如 sincossqrt
如果没有链接库,那么当开发者需要用到上述标准函数时有下面几种方式实现,第一种是开发者自己实现一遍,可想而知这样开发效率很低,而且容易出错;第二种是编译器解析到使用了标准函数时自动生成相应的代码实现,这种方式将给编译器增加显著的复杂性,而且每次添加、删除或修改一个标准函数时,就需要一个新的编译器版本,比较繁琐。第三种则是将标准函数的实现打包到一个标准函数目标文件中,例如 libx.o,开发者可以在编译时自行指定使用哪个标准函数目标文件。
相较而言第三种的思路更好一些,因为这种方式将编译器和标准函数的实现分离开,降低了编译器的复杂度,同时又能在标准函数的实现发生变化时以较低成本实现替换,链接库就是基于这种方式而来的。

链接库的两种类型

编译过程中编译器将源代码编译成目标文件,一般以 .o(object) 作为扩展名,之后链接器将多个目标文件链接成可执行文件或链接库文件,链接库根据被使用时的方式的不同分为静态链接库动态链接库
Linux 平台上静态库一般以 .a(archive) 为扩展名,动态库一般以 .so(shared object) 为扩展名;
Windows 平台上静态库一般以 .lib 为扩展名,动态库一般以 .dll(dynamic link library) 为扩展名;
静态链接库是将相关函数编译为独立的目标模块,然后封装成一个单独的静态库文件。编译程序时可以通过指定单独的文件名来使用这些在库中定义的函数。比如,使用 C 标准库和数学库中函数的程序可以用如下的命令行来编译和链接:

gcc main.c /usr/lib/libm.a /usr/lib/libc.a

而在链接时,链接器只会复制被用到的目标模块,而并不会复制整个库的内容,这就减少了可执行文件在磁盘和内存中的大小。
静态链接库也有一些缺点,首先是静态链接库是在编译链接过程中被复制到可执行文件中的,当静态链接库有更新时,应用程序必须重新执行编译链接得到新的可执行文件。第二是几乎每个 C 程序都会用到标准 I/O 函数,比如 printfscanf,这些函数的代码被重复的复制到每个运行进程的文本段中,这对于内存来说是一种浪费。
动态链接库避免了上述问题,应用程序在编译时只记录一些动态链接库的基础信息,在加载应用程序但还没有运行时会将依赖的动态链接库中的函数与内存中的程序链接起来形成一个完整的程序;所有引用同一个动态链接库的可执行文件共用这个库中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。

使用链接库

使用静态链接库
下面用 C 语言编写两个函数,并分别生成静态链接库和动态链接库,最后在另一个程序中使用生成的链接库。
addvec.c 文件,其中 addvec 函数实现两个向量数组的相加

int addcnt = 0;  void addvec(int *x, int *y,  int *z, int n)  
{  int i;  addcnt++;  for ( i = 0; i < n; i++)  z[i] = x[i] + y[i];  
}

multvec.c 文件,其中 multvec 函数实现两个数组向量的相乘

int multcnt = 0;  void multvec(int *x, int *y, int *z, int n)  
{  int i;  multcnt++;  for (i = 0; i < n; i++) {  z[i] = x[i] * y[i];  }  
}

定义头文件 vector.h

#ifndef VECTOR_OPS_H  
#define VECTOR_OPS_H  extern int addcnt;  
extern int multcnt;  void addvec(int *x, int *y, int *z, int n);  
void multvec(int *x, int *y, int *z, int n);  #endif

main2.c 用来测试使用链接库

#include <stdio.h>  
#include "vector.h"  int x[2] = {1, 2};  
int y[2] = {3, 4};  
int z[2];  int main()  
{  addvec(x, y, z, 2);  printf("z = [%d %d]\n", z[0], z[1]);  return 0;  
}

首先编译出两个库函数的目标文件

gcc -c addvec.c multvec.c

得到两个目标文件 addvec.o 和 multvec.o,接着将两个目标文件链接成静态库,ar 命令是用来处理静态链接库的,也就是归档文件 archive

ar rcs libvector.a addvec.o multvec.o

得到静态链接库 libvector.a,最后编译链接应用程序和动态链接库生成可执行文件,其中 -static 参数用来生成静态链接程序

gcc -c main2.cgcc -static -o prog2c main2.o ./libvector.a
或者
gcc -static -o prog2c main2.o -L. -lvector

最后得到可执行文件 prog2c 并运行

> ./prog2c
z = [4 6]

当链接器运行时,它判定 main2.o 引用了 addvec.o 定义的 addvec 函数符号,所以复制 addvec.o 到可执行文件。因为程序不引用任何由 multvec.o 定义的符号,所以链接器就不会复制这个模块到可执行文件。链接器还会复制 libc.a 中的 printf.o 模块,以及许多 C 运行时系统中的其他模块。
下面是使用静态链接库生成可执行文件的图示:

使用动态链接库
再看一个动态链接库的例子,代码还是一样,只是在生成链接库和编译链接的时候不太一样。使用 gcc 生成动态链接库,其中 -shared 参数表明生成共享的链接库,-fpic 参数表明生成位置无关代码(position-independent code),位置无关代码可以理解为是库中的函数都没有确定下来在内存中的具体的绝对位置,而是使用相对位置表示,只有在被链接到应用程序中才被确定最终在内存中的位置。

gcc -shared -fpic -o libvector.so addvec.c multvec.c

得到动态链接库 libvector.so,之后编译链接生成可执行文件

gcc -o prog2l main2.c ./libvector.so

得到可执行文件 prog2l 并运行

> ./prog2l
z = [4 6]

创建完可执行文件后,其实并没有任何 libvector.so 的代码和数据节真的被复制到可执行文件 prog2l 中。链接器仅仅是复制了一些重定位和符号表信息,它们使得运行时可以解析对 libvector.so 中代码和数据的引用,在程序加载时动态链接才真正完成。
下面是动态链接库的图示:

在程序运行中加载链接库
此外还可以在应用程序运行过程中加载指定动态链接库,但这里不展开,只列出一个典型的例子,下面例子是在应用程序运行中加载调用 libvector.so 库:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];int main()
{void *handle;void (*addvec)(int *, int *, int *, int);char *error;/* Dynamically load the shared library containing addvec() */handle = dlopen("./libvector.so", RTLD_LAZY);if (!handle) {fprintf(stderr, "%s\n", dlerror());exit(1);}/* Get a pointer to the addvec() function we just loaded */addvec = dlsym(handle, "addvec");if ((error = dlerror()) != NULL) {fprintf(stderr, "%s\n", error);exit(1);}/* Now we can call addvec() just like any other function */addvec(x, y, z, 2);printf("z = [%d %d]\n", z[0], z[1]);/* Unload the shared library */if (dlclose(handle) < 0) {fprintf(stderr, "%s\n", dlerror());exit(1);}return 0;
}

编译

gcc -rdynamic -o prog2r dll.c -ldl

动态编译与静态编译

编译应用程序时如果使用静态链接库则被称为静态编译,如果使用动态链接库则被称为动态编译。静态编译是在编译时就将依赖的静态链接库复制到可执行文件中,这样在应用程序运行起来后无需依赖外部的库,只需要单一的可执行文件即可运行,但缺点是应用程序体积相对较大,程序运行的越多重复占用的内存浪费越多。
动态编译则相当于按需加载,动态编译有好处也有弊端,好处是应用程序只需要链接用到的目标模块,这使得应用程序的体积更小,运行起来之后内存占用更低。而弊端则是如果应用程序所在的运行环境中缺少依赖的动态链接库则会导致无法正常运行。

Go 静态编译和动态编译例子

静态编译
Go 支持跨平台和可移植特性,默认使用静态编译

package main  import "fmt"func main() {  fmt.Println("Hello World!")
}

编译后可以通过 ldd(List Dynamic Dependencies) 命名查看可执行程序所依赖的动态链接库:

> go build -o hello hello.go
> ./hello
Hello World!> ldd hellonot a dynamic executable

not a dynamic executable 表示没有依赖任何的动态链接库。

动态编译
但并不是所有情况下都不需要依赖外部库,例如对于很多经典的 C 语言函数库来说,编程语言没必要自己重新实现一遍,需要用到时直接调用 C 语言函数库即可。 下面的 Go 程序中使用了 net/http 包,其中关于网络的处理很多都是依赖 C 语言的动态链接库:

package main  import (  "log"  "net/http"
)  func main() {  http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { writer.Write([]byte("Hello"))  log.Println("get request")  })  http.ListenAndServe(":8000", nil)  
}

编译后用 ldd 查看

> go build server.go
> ldd serverlinux-vdso.so.1 =>  (0x00007ffd8e8b4000)/$LIB/libonion.so => /lib64/libonion.so (0x00007f3837d14000)libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f38379e1000)libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f38377c5000)libc.so.6 => /lib64/libc.so.6 (0x00007f38373f7000)libdl.so.2 => /lib64/libdl.so.2 (0x00007f38371f3000)/lib64/ld-linux-x86-64.so.2 (0x00007f3837bfb000)

能看到输出了一些动态链接库,例如 libresolv.so.2 就是用于域名解析的库,而 libc.so.6 则是标准 C 库,含有大部分 C 函数。
下面介绍 Go 如何指定进行静态编译

Go 强制进行静态编译

如果希望将上述代码编译后运行在另一个系统中,为了保证可移植性,应该尽量使用静态编译,如果想要强制使用静态编译有两种方式。

通过关闭 CGO 实现静态编译

先介绍 CGO
CGO 是 Go 开发工具链中自带的工具,CGO 是使 Go 语言和 C 语言相互调用的桥梁。如果在 Go 代码中包含 import "C" 并且开启 CGO,那么在 go build 编译时就会通过 CGO 来处理 C 代码,生成能够让 Go 调用 C 的桥接代码,然后交给 gcc 编译得到 C 语言的目标文件,之后再编译 Go 代码得到 Go 语言的目标文件,最终将 Go 和 C 目标文件通过链接器链接到一起得到最终的可执行文件。
CGO 通过环境变量 CGO_ENABLED 控制是否启用,默认为 1 表示启用,0 表示关闭。
下面一段代码展示利用 CGO 实现 Go 调用 C 函数的功能,主要有两个文件 hello.go 和 hello.c
hello.go

package main  /*  
#include "hello.c"  
*/  
import "C"  
import "fmt"  func main() {  fmt.Println("Hello from Go")  C.SayHello(C.CString("Hello from C!"))  
}

hello.c

#include <stdio.h>  void SayHello(const char* s) {  printf("%s\n", s);  
}

查看环境变量

> go env | grep CGO_ENABLED
CGO_ENABLED="1"

编译运行

> go build -o hello hello.go
>./hello
Hello from Go!
Hello from C!

可以看到用 Go 调用 C 语言函数的运行效果。

通过关闭 CGO 间接实现静态编译
按照这个思路,如果关闭 CGO 之后再编译之前的 server.go 的应用代码,Go 编译器由于无法启用 CGO 也就无法生成 Go 和 C 之间的桥接代码,无法利用 C 函数库,只能使用纯 Go 实现的函数,从而实现静态编译效果。下面就是关闭 CGO 后编译的 server.go

> CGO_ENABLED=0 go build server.go
> ldd servernot a dynamic executable

go build 前指定 CGO_ENABLED=0 来关闭 CGO,最后得到的可执行文件可以看到不再依赖动态链接库,实现静态编译。

通过链接参数实现静态编译

假如我希望在代码中调用 C 函数,但又希望执行静态编译应该怎么做?也就是说我必须开启 CGO 但又希望进行静态编译。go build 有一个 -ldflags 参数表示传给链接器的参数,参数中 -linkmode 控制使用 Go 内部自己实现的链接器 internal(默认值),还是外部链接器 external,例如使用 gcc clang 等。如果代码中只需要 net, os/user, runtime/cgo 等包则使用 internal,否则使用 external。-extldflags 表示传给外部链接器的参数,这里是 -static 表示使用静态链接方式。

go build -ldflags '-linkmode external -extldflags "-static"' server.go

得到编译后的可执行文件 server,通过 ldd 查看表明这是一个静态链接的可执行文件。

> ldd servernot a dynamic executable

利用静态编译减小 docker 镜像体积

静态编译后二进制文件可移植性较好,只需要一个单独的文件便可以运行,并且由于编译时的环境要求与运行时的环境要求不同,运行时环境中不要求有编译链接等工具,所以可以利用这个区别在构建 docker 镜像时只需要保留能够支持可执行文件运行的最少资源即可,从而缩小镜像体积。

使用两个 Dockerfile 分别构建

下面有两个 Dockerfile,第一个是 build.Dockerfile,主要是执行静态编译指令编译出可执行文件 server:

FROM golang:1.16  
WORKDIR /code  
COPY server.go ./  # go静态编译  
RUN go build -ldflags '-linkmode external -extldflags "-static"' server.go  ENTRYPOINT ["./server"]

构建镜像

docker build -t go_web_build -f build.Dockerfile .

之后创建一个容器,测试功能正常:

> docker run -p 8000:8000 --name builder go_web_build:latest > curl localhost:8000
Hello

此时查看一下镜像大小为 796MB。

> docker images
REPOSITORY     TAG             IMAGE ID       CREATED          SIZE
go_web_build   latest          0bb4d390b4d3   10 minutes ago   796MB

现在测试将可执行文件转移到另一个容器环境中单独执行,首先把在第一个镜像中编译好的 server 复制出来到宿主机上。

> docker cp builder:/code/server .

然后在第二个名为 run.Dockerfile 的 Dockerfile 中把 server COPY 进去

FROM alpine:latest  
WORKDIR /code  
COPY ./server ./  
ENTRYPOINT ["./server"]

构建镜像

docker build -t go_web_run -f run.Dockerfile .

启动容器并测试功能正常:

> docker run -p 8000:8000 go_web_run:latest     > curl localhost:8000
Hello

此时对比一下两个镜像,go_web_build 有 796MB,而 go_web_run 仅有 15.4MB,大幅缩小了镜像的大小。

> docker images
REPOSITORY     TAG             IMAGE ID       CREATED          SIZE
go_web_run     latest          6b982ff82499   10 minutes ago   15.4MB
go_web_build   latest          0bb4d390b4d3   10 minutes ago   796MB

不过这样做还是有点繁琐,需要编写两个 Dockerfile 同时还要手动复制可执行文件,而 docker 的多阶段构建可以简化这个过程。

使用 docker 的多阶段构建

docker 多阶段构建(multi-stage build)可以在一个 Dockerfile 中编写上述两个镜像构建过程,使用 FROM 指令表示开始一个阶段的构建,第一阶段构建用来编译得到可执行文件,在第二阶段构建时可以将上一个阶段中产出的可执行文件 COPY 到当前构建的镜像中,从而实现与上述效果相同的减少镜像体积的目的。
现在使用多阶段构建结合 Go 的静态编译做一个实验,下面是名为 mutil_stage.Dockerfile 的 Dockerfile 文件:

# 第一阶段用来编译链接生成可执行文件
FROM golang:1.16 AS builder  
WORKDIR /code  
COPY server.go ./  # go静态编译  
RUN go build -ldflags '-linkmode external -extldflags "-static"' server.go  ENTRYPOINT ["./server"]  # 第二阶段构建,从第一阶段中 COPY 出 main 来
FROM alpine:latest AS prod  
WORKDIR /code  
COPY --from=builder /code/server ./  
ENTRYPOINT ["./server"]

构建镜像

docker build -t go_web_mstage -f multi_stage.Dockerfile .

启动容器运行测试正常:

> docker run -p 8000:8000 go_web_mstage:latest> curl localhost:8000
Hello

查看镜像可以看到 go_web_mstage 也是 15.4MB,这样就实现了在一个 Dockerfile 中声明两个镜像并且保持镜像体积相对较小。

REPOSITORY      TAG             IMAGE ID       CREATED           SIZE
go_web_mstage   latest          f22146675fb7   10 minutes ago    15.4MB
go_web_run      latest          6b982ff82499   10 minutes ago    15.4MB
go_web_build    latest          0bb4d390b4d3   10 minutes ago    796MB

小结

文中涉及到的相关概念比较多,这里做一个要点总结。首先介绍了链接库的概念以及静态链接库和动态链接库的区别,接着介绍了 Go 的静态编译和动态编译以及如何实现静态编译,最后举了一个实际例子,使用 Go 的静态编译结合 docker 的多阶段构建实现了减小镜像体积的效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/777881.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JDK与JRE区别

JDK:是整个Java的核心,包含运行环境+一系列的开发工具包 JRE:java程序开发环境+核心工具类+JVM等。不能用于编译工作。 每天坚持,终会抵达!

mysql 两种重要的日志

前言 这个不用多说,两种重要的日志分别是redo log 和 binlog,这两种,下面分别来结束。 正文 先来redo log, redo log 是inno db 引擎特有的,运用了一种wal 技术,全称是: write-ahead logging. 它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。…

STM32H7 HAL库CubeMX 双重ADC模式同步采样详细配置+FFT计算相位差

前言 在电赛备赛期间琢磨了一下ADC同步采样的实现方式,本来是打算直接用AD7606来着,但是搞了半天也没把驱动整出来...考虑到AD7606本身采样率也拉不到太高,于是就花了几天时间把片上ADC配出来了。查资料的时候我发现关于STM32双重ADC模式的资料是真的少,用FFT算两路信号相位…

Python pymodbus类库使用学习总结

实践环境 Python 3.9.13 https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe pymodbus-3.6.8-py3-none-any.whl https://files.pythonhosted.org/packages/35/19/a9d16f74548d6750acf6604fa74c2cd165b5bc955fe021bf5e1fa04acf14/pymodbus-3.6.8-py3-none-any.w…

ThinkAdmin_v6两个简单漏洞(文件读取+信息泄露)

危险函数:file_get_contents()第二次遇到侵权声明 本文章中的所有内容(包括但不限于文字、图像和其他媒体)仅供教育和参考目的。如果在本文章中使用了任何受版权保护的材料,我们满怀敬意地承认该内容的版权归原作者所有。 如果您是版权持有人,并且认为您的作品被侵犯,请通…

ubuntu22.04容器安装ssh服务

除了特别说明外,否则以下命令均为ubuntu 22.04 容器内执行!!!安装 查看ubuntu22.04 有没有安装openssh-server,执行命令:sudo dpkg --list | grep ssh没有找到openssh-server的包,很显然,没有安装,那么就开始安装,执行:sudo apt install openssh-server安装完成后,…

3.Java基础语法

注释单行注释 //单行注释 //输入一个Hello World!多行注释:可以注释一段文字 /* 注释 */ //多行注释:可以注释一段文字 /* 注释 */ /* 多行注释 多行注释 多行注释 */文档注释:JavaDoc /** * @Description HelloWorld * @Author 爱吃麻辣烫的妹纸 */⭐️注意:书写注释是…

分享圣诞树+雪人+全屏动效

分享圣诞树+雪人+全屏动效 创建时间:2024年8月4号 分享之前学习老师发的几个小玩意 一、圣诞树 运行: 点击该exe即可出来一棵圣诞树。退出: 鼠标点击该圣诞树右键二、雪人 运行: 双击点开exe即可退出: 和圣诞树一样、 三、音乐:满满都是爱 运行:双击打开,该程序对鼠标的…

java:一键生成二维码工具类

前言:本工具选择了Zxing,他是一个开源的,使用java实现多种格式的1D/2D条码图像处理库, 1.pom添加依赖<!-- 二维码生成&识别组件 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>…

Luogu P10842 Piggy and Trees 题解 [ 绿 ] [ 拆边 ] [ 贡献思维 ]

Piggy and Trees:把路径拆成边的思维题。 思路 一看到这题的路径,就想到了 Luogu P3177 树上染色 这题化路径为边的贡献,分别计算的思维。 那么对于此题,先来观察题目里式子的意思:对于树上的每个无序点对,求出树上每个点 到这些点对之间的最短路径的 距离之和。枚举点对…

ZeRO:一种去除冗余的数据并行方案

ZeRO:一种去除冗余的数据并行方案 目前训练超大规模语言模型主要有两条技术路线:TPU + XLA + TensorFlow/JAX GPU + Pytorch + Megatron + DeepSpeed 前者由Google主导,由于TPU和自家云平台GCP深度绑定,对于非Googler来说并不友好 后者背后则有NVIDIA、Meta、MS等大厂加持,…

【攻防技术系列+权限维持】注册表运行键

在红队行动期间在网络中获得初步立足点是一项耗时的任务。因此,持久化是红队行动成功的关键,因为这将使团队能够专注于交战目标,而不会失去与指挥和控制服务器的通信。 创建将在 Windows 登录期间执行任意负载的注册表项是红队剧本中最古老的隐藏技巧之一。这种持久性技术需…