裸容器化 Go 程序

本文的示例代码在 https://github.com/landzero/bare-golang-container

使用 Go 的工程师都知道,Go 代码的编译输出文件体积要大于等价的 C 代码。

以最简单的 “hello, world” 为例:

// hello.go
package main

import "fmt"

func main() {
  fmt.Printf("hello, %s", "world")
}
// hello.c
#include <stdio.h>

int main(int argc, char** argv)
{
  printf("hello, %s", "world");
  return 0;
}
# 编译 hello.go,输出 hello-go
go build -o hello-go hello.go

# 编译 hello.c,输出 hello-c
gcc -g -o hello-c hello.c

在 Ubuntu (18.04) 下,hello-go 文件体积为 2.0 MB,hello-c 为 11 KB。

为什么同为编译型语言,如此简单的一段代码,Go 的编译产物文件体积要比 C 大两个数量级呢?

因为 Go 的编译产物是静态链接的,包含了 Go 的运行时和所有依赖库。而默认状态下,C 的编译产物会动态链接 libc.so,不包含 C 运行时。

使用 ldd 命令可以查看编译产物的动态链接信息

> ldd hello-go 
        not a dynamic executable
> ldd hello-c
        linux-vdso.so.1 (0x00007ffee0132000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0cf67a1000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0cf6d94000)

我们可以使用 gcc 编译参数强制要求静态链接编译 hello.c

# 编译并强制静态连接 hello.c,输出 hello-c-static
gcc -g -static -static-libgcc -o hello-c-static hello.c

再次使用 ldd 检查动态链接信息

> ldd hello-c-static
        not a dynamic executable

在 Ubuntu (18.04) 下,hello-c-static 文件体积为 828 KB,是不是和 hello-go 接近了很多?

Go 的这种特性保证了编译产物可以直接在相同系统(Linux / Windows / BSD)相同构架的环境上运行,而不需要考虑当前操作系统是否安装了依赖库,是否有 Go 运行时,甚至连最基础的 C 运行时都不需要,哪怕发起系统调用也是在编译产物内完成的。

理想状态下,只要有一个运行中的操作系统内核,哪怕整个根目录只有一个 Go 程序,它照样可以正常运行。

当然,没人愿意在物理机或者虚拟机里运行这样一个操作系统,但是在容器里,就另当别论了。

常见的 Dockerfile 通常都会以 FROM debian 或者 FROM centos开头,因为运行 PHP,Java, Python 等程序需要各种库,可执行文件和配置文件,而安装这些东西需要包管理器和一大堆下载,复制,解压,删除命令。最稳妥的方式就是基于一个现有的 Linux 发行版来处理这些事情。

而 Go,在理想状态下,你只需要把它编译好,它就能运行了,它什么都不依赖。

因此,我们可以构建一个裸镜像,里面只有一个 Go 程序。

// server.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "hello, %s", "world")
	})
	http.ListenAndServe(":8080", nil)
}
# 编译 server.go,输出 server
go build -o server server.go
# Dockerfile
FROM scratch

ADD server /

EXPOSE 8080

CMD ["/server"]
# 构建 docker 镜像
docker build -t bare-go .

这样,就构建了一个只有一个 Go 程序 /server 的镜像 bare-go,并且它能正常工作

docker run --rm -p 8080:8080 bare-go

别忘了,Go 自带交叉编译功能,你可以在 macOS 上编译一个 Linux 可执行程序,并打包 Docker 镜像,不需要专门的编译环境

GOOS=linux GOARCH=amd64 go build -o server server.go

怎么样,是不是很 Cooooool™ ?