Go 语言天生支持跨平台编译,并且其标准库几乎不依赖系统动态库,所以在大多数场景下,它编译出来的二进制文件几乎可以直接丢到任何机器运行。
但实际开发中,我们经常遇到两个问题:
这篇文章会从基础概念讲起,逐步深入,并附带一个一键多平台静态编译脚本,让你少踩坑。
1 2 3 |
• 静态编译 = 把所有依赖库都编进一个二进制,丢到任何机器都能跑 • 动态编译 = 程序运行时还需要宿主机的动态库(如 libc.so.6) • 交叉编译 = 在 A 平台上编译 B 平台的程序(比如 Mac 编译 Linux 版) |
Go 天生适合静态编译,因为:
1 2 |
• 纯 Go 代码不依赖外部 libc • 关闭 CGO 后编译结果天然是静态的 |
只有当项目用了 CGO(如 sqlite、openssl)才会出现动态依赖,需要额外处理。
纯 Go 项目(最简单)
1 |
CGO_ENABLED=0 go build -ldflags="-s -w" -o app . |
1 2 |
• CGO_ENABLED=0 关闭 C 依赖 → 天然静态 • -ldflags="-s -w" 去掉符号表,减小体积 |
验证:
1 |
ldd app # not a dynamic executable ? |
有 CGO 依赖(sqlite、openssl 等)
默认会动态链接 glibc,要用 musl 完全静态化:
1 |
CC=musl-gcc CGO_ENABLED=1 go build -ldflags="-linkmode external -extldflags -static" -o app . |
1 2 |
• musl-gcc 是轻量 libc,适合静态链接 • -extldflags -static 让外部链接器打包所有依赖 |
验证:
1 |
ldd app # not a dynamic executable ? |
Go 内置交叉编译能力,只需 GOOS/GOARCH:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Linux AMD64 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o app-linux-amd64 .
# Linux ARM64(树莓派) GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o app-linux-arm64 .
# Windows GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o app-windows-amd64.exe
# macOS ARM64 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o app-mac-arm64
?? 如果必须用 CGO,交叉编译就需要额外交叉工具链(如 aarch64-linux-musl-gcc)。 |
1 2 |
• 动态编译的程序 → 容器镜像必须带 libc(debian、alpine) • 静态编译的程序 → 直接放 FROM scratch,镜像只有几 MB |
推荐:
1 2 3 4 5 6 7 8 9 |
FROM golang:1.22-alpine AS builder RUN apk add --no-cache build-base musl-dev WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app .
FROM scratch COPY --from=builder /out/app /app ENTRYPOINT ["/app"] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#!/usr/bin/env bash set -e
APP="myapp" # 你的程序名 OUT="dist" # 输出目录 PLATFORMS=("linux/amd64" "linux/arm64" "darwin/arm64" "windows/amd64")
# 是否启用 CGO(0=纯Go,1=需要C依赖) USE_CGO=${USE_CGO:-0}
echo "???? Building $APP for: ${PLATFORMS[*]}" echo "???? CGO Mode: ${USE_CGO}"
rm -rf "$OUT" && mkdir -p "$OUT"
for p in "${PLATFORMS[@]}"; do GOOS=${p%/*} GOARCH=${p#*/}
BIN="$OUT/$APP-$GOOS-$GOARCH" [[ $GOOS == "windows" ]] && BIN="$BIN.exe"
echo -e "\n==> ???? Building for $GOOS/$GOARCH ..."
if [[ "$USE_CGO" == "1" && "$GOOS" == "linux" ]]; then echo " ???? CGO enabled + musl static build" CC=musl-gcc \ CGO_ENABLED=1 \ GOOS=$GOOS GOARCH=$GOARCH \ go build -ldflags="-linkmode external -extldflags -static -s -w" -o "$BIN" . else echo " ? Pure Go build (CGO disabled)" CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH \ go build -ldflags="-s -w" -o "$BIN" . fi
# ? 验证是否静态(仅 Linux) if [[ "$GOOS" == "linux" && -x "$BIN" ]]; then echo " ???? Checking binary type:" if command -v ldd >/dev/null; then ldd "$BIN" || echo "? Not a dynamic executable" else echo " (ldd not found, skip check)" fi fi
echo " ? $BIN built." done
echo -e "\n???? All binaries are in $OUT/" |
执行:
1 2 |
chmod +x build-all.sh ./build-all.sh # or USE_CGO=1 ./build-all.sh |
最终你会得到:
1 2 3 4 5 6 |
dist/ ├── myapp-linux-amd64 ├── myapp-linux-arm64 ├── myapp-darwin-amd64 ├── myapp-darwin-arm64 ├── myapp-windows-amd64.exe |
可以分发给对应的二进制平台即可
musl 是 一个轻量的 C 标准库实现,主要用来替代传统的 glibc。
Go 编译器在用 CGO 时,需要链接 C 运行库(libc),默认是 glibc,但 glibc 的动态库在不同 Linux 发行版版本不同,容易产生兼容性问题。
musl 的特点是:
1 2 3 4 |
• 体积小(适合嵌入式和容器) • 设计简洁、依赖少 • 支持完整静态链接,方便做“丢哪都能跑”的程序 • 常用在 Alpine Linux 这种极简系统中 |
所以,如果你想让一个含 CGO 的 Go 程序 完全静态,就得用 musl-gcc 替代 gcc,这样 libc 也能被编进二进制里。
glibc vs musl 直观对比
项目 | glibc | musl |
---|---|---|
体积 | 大 | 小 |
兼容性 | 最通用,几乎所有 Linux 默认用 | 轻量,偏向容器/嵌入式 |
默认是否动态链接 | ? | ? |
是否易做静态编译 | ? 麻烦 | ? 非常容易 |
适用场景 | 桌面、服务器 | Alpine、scratch 镜像、IoT |