首页 > 其他分享 >Go 静态编译及在构建 docker 镜像时的应用

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

时间:2024-08-04 21:05:33浏览次数:14  
标签:编译 静态 链接库 go int Go 镜像 docker

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.c

gcc -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 hello
	not 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 server
	linux-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 server
	not 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 server
	not 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 的多阶段构建实现了减小镜像体积的效果。

标签:编译,静态,链接库,go,int,Go,镜像,docker
From: https://www.cnblogs.com/caipi/p/18342199

相关文章

  • Airflow vs. Luigi vs. Argo vs. MLFlow vs. KubeFlow
    Airflowvs.Luigivs.Argovs.MLFlowvs.KubeFlowhttps://www.datarevenue.com/en-blog/airflow-vs-luigi-vs-argo-vs-mlflow-vs-kubeflow Airflowisthemostpopularsolution,followedbyLuigi.Therearenewercontenderstoo,andthey’reallgrowingfast......
  • docker入门学习
    docker入门学习前言一、简介1.docker是什么2.docker特点及应用3.docker与虚拟机的区别4.docker相关概念理解5.docker网络二、docker安装1.安装2.配置镜像三、docker命令1.服务相关命令2.容器相关命令3.镜像相关命令四、搭建服务测试1.搜索tomcat镜像2.pulltomcat3.查......
  • Go中使用Zap日志库与Lumberjack日志切割
    Go中使用Zap日志库与Lumberjack日志切割Go中使用Zap日志库与Lumberjack日志切割原创 何泽丰 ProgrammerHe  2024年06月11日20:15 广东 听全文Go中使用Zap日志库与Lumberjack日志切割概述在项目中使用日志记录有助于快速定位和修复问题,能帮助我们监控系统健康状......
  • Go必知必会:深入剖析Go语言中的结构体
    Go必知必会:深入剖析Go语言中的结构体原创王中阳王中阳 2024年07月24日06:03北京1人听过文末有面经共享群本文来自极客学院专栏,欢迎订阅:Go入门进阶实战专栏:其实学Go很简单。 Go语言以其清晰的语法和强大的内置类型系统,为构建高效且易于维护的软件程序提供了坚实的基础......
  • Jquery正确发送headers值,Django后台request.Meta取值
    jquery发送请求$.ajax({method:"POST",headers:{"Auth_xxx":"yes"},data:{},url:"",success:function(response){console.log("respons......
  • 为什么我在 html 页面的格式化段落中没有收到 google gemini 响应
    我在我的django中使用googlegeminiapi,一切都很顺利,在终端中生成的Gemini响应非常完美,两个段落和所有段落之间都有空格,但是当我将此响应传递到html页面时,所有格式都消失了,那里两段之间没有空格,我不知道为什么它在响应中产生不必要的星星**,请告诉我如何修复它。......
  • go高并发之路——消息中间件kafka(中)
    接着上篇,我们继续聊聊kafka的那些事儿。一、消费者组消费者组,即ConsumerGroup,是Kafka的一大亮点设计。一个组内可以有多个消费者或消费者实例(ConsumerInstance),它们共享一个公共的ID,这个ID被称为GroupID。组内的所有消费者协调在一起来消费订阅主题(topic)的所有分区(Part......
  • 在 Python 中从 HTML 中抓取嵌入的 Google Sheet
    这对我来说相对棘手。我正在尝试提取来自python中的google工作表的嵌入表。这是链接我不拥有该工作表,但它是公开可用的。这是迄今为止我的代码,当我输出标题时,它向我显示“”。任何帮助将不胜感激。最终目标是将此表转换为pandasDF。多谢你们importlx......
  • 使用 django 的 EmailMessage 发送波斯语电子邮件时出现问题
    我对django相当陌生,并尝试使用django.core.mail.EmailMessage发送包含波斯语字母的电子邮件。这是我的代码:fromdjango.core.mailimportEmailMessagefromdjango.confimportsettingsdefcustom_sender(subject:str,body:str,recipient_list:list[str],......
  • 如何使用 Python 在 Google 或 DuckDuckGo 中快速获取答案
    我有一个人工智能助手项目,我希望它在互联网上搜索。我想使用适用于Python的GoogleQuickAnswerBox或DuckDuckGoInstantAnswerAPI。我看到了其他问题,但它们对我没有多大帮助。这是我想要实现的一个示例:问题:什么是长颈鹿?Google的答案:DuckDuckGo的......