Go 的二进制大,主要因为它自带运行时、垃圾回收、类型信息与完整的函数表。这篇用真实构建量出:不同依赖带来多少、-s -w 去掉了什么、pclntab 是什么、体积都花在哪些包上,以及反射用法怎样让链接器没法丢掉不用的方法。
println 版(不导入 fmt):默认 1831778 字节(1.75 MiB);-ldflags="-s -w" 1195250 字节(1.14 MiB),小 35% fmt.Println 版:默认 2429714 字节(2.32 MiB);-ldflags="-s -w" 1587714 字节(1.51 MiB),小 35% 加 encoding/json:默认 3766466 字节(3.59 MiB);-ldflags="-s -w" 2477826 字节(2.36 MiB),小 34% 加 net/http 客户端:默认 8316338 字节(7.93 MiB);-ldflags="-s -w" 5612050 字节(5.35 MiB),小 33% 加 net/http 服务端:默认 8217154 字节(7.84 MiB);-ldflags="-s -w" 5544514 字节(5.29 MiB),小 33%
最小的 hello world 约 1.75 MiB(只用内建 println,不导入 fmt),fmt 带来 0.57 MiB,encoding/json 再加 1.27 MiB,net/http 把它推到 7.8–7.9 MiB——因为它拖进 TLS、x509、HTTP/2、net 等一大串包。体积主要是“导入了什么”,而不是你自己写了多少代码。
默认:2429714 字节(2.32 MiB)
-ldflags=-s(去掉符号表):1587714 字节(1.51 MiB)
-ldflags=-w(去掉 DWARF 调试信息):1686674 字节(1.61 MiB)
-ldflags="-s -w":1587714 字节(1.51 MiB)
-trimpath:2396642 字节(2.29 MiB)
-trimpath 加 -s -w:1571202 字节(1.50 MiB)
size -m(默认构建)里的段:
Segment __PAGEZERO: 4294967296 (zero fill)
Segment __TEXT: 1343488
Section __text: 632260
Section __symbol_stub1: 696
Section __rodata: 40578
Section __gopclntab: 650154
Segment __DATA_CONST: 144880
Section __rodata: 7296
Section __go_type: 136160
Section __go_func: 960
Section __got: 464
Segment __DATA: 213424
Section __go_buildinfo: 224
Section __go_fipsinfo: 120
Section __go_module: 568
Section __noptrdata: 19266-s 去掉符号表,-w 去掉 DWARF 调试信息;实测只写 -s 就和 -s -w 一样大(DWARF 随符号表一起没了),只写 -w 单独能省约 0.71 MiB(2429714 → 1686674 字节)。-trimpath 只是把路径换成相对形式,体积只小一点(省约 33 KB)。代价是 go tool nm 与调试器能看到的东西变少,不影响运行,也不影响 panic 的栈回溯。
nm 里占文件空间的符号(类型 T/R/D 等,不含 BSS)合计 5483407 字节(文件 8217154 字节);BSS 符号合计 33779728 字节,最大的是 crypto/internal/fips140/drbg.memory(33554432 字节)——BSS 不占文件空间,但运行时要映射内存
runtime 2319311 字节 42.3%
_type:* 535536 字节 9.8%
go:*(编译器生成的数据) 483400 字节 8.8%
vendor/golang 224122 字节 4.1%
crypto/tls 222035 字节 4.0%
crypto/internal/fips140/nistec 137648 字节 2.5%
net 136336 字节 2.5%
net/http/internal/http2 123867 字节 2.3%
net/http 107073 字节 2.0%
crypto/x509 92394 字节 1.7%
最大的 5 个符号:
1681680 runtime.pclntab
535536 _type:*
365592 go:func.*
116128 runtime.rodata
116128 go:string.*按符号统计,运行时(含它的类型与函数表)占了 4 成以上,最大的单个符号是 runtime.pclntab(程序计数器到函数、文件、行号的映射表,栈回溯、垃圾回收的栈扫描都要用它);_type:* 是类型描述符(反射与接口用)。BSS 里有一个 32 MiB 的 crypto/internal/fips140/drbg.memory:不占文件空间,但会占运行时的内存映射(我没有量它的实际驻留内存)。
默认:1831906 字节;go tool nm 可用,符号 1826 个;panic 回溯里的函数:main.boom(...) main.main() -ldflags="-s -w":1195362 字节;go tool nm 可用,符号 42 个;panic 回溯里的函数:main.boom(...) main.main()
剥离符号表后 go tool nm 从 1826 个符号变成 42 个,但 panic 的回溯照样显示 main.boom,因为回溯读的是 pclntab。所以生产构建加 -s -w 通常是安全的;想保留符号以便事后分析就别加。
不用反射,只直接调 M1:默认 2429698 字节、-s -w 1587714 字节;符号表里 main.T.M<数字> 的函数有 0 个
reflect.TypeOf(...).Name()(用了反射但不调方法):默认 2446258 字节、-s -w 1604258 字节;符号表里 main.T.M<数字> 的函数有 0 个
reflect.Value.MethodByName(动态名字):默认 3444562 字节、-s -w 2276322 字节;符号表里 main.T.M<数字> 的函数有 200 个
同一个类型的方法数 N 变化时(都用 MethodByName,默认构建):
N = 0:3349922 字节,比 N = 0 多 0 字节
N = 20:3367602 字节,比 N = 0 多 17680 字节(每个方法约 884 字节)
N = 100:3405378 字节,比 N = 0 多 55456 字节(每个方法约 555 字节)
N = 200:3444562 字节,比 N = 0 多 94640 字节(每个方法约 473 字节)链接器会丢掉没被引用的函数,但如果程序里出现 MethodByName 这样“按名字找方法”的反射调用,编译器无法知道会调哪个,所以把能被反射到的导出方法全部保留(实验里 200 个方法全留着,符号表里 200 个)。仅仅用 reflect.TypeOf 不会触发。这一点只看到现象,我没有读链接器里决定保留的那段代码。
darwin/arm64:默认 2429714 字节(2.32 MiB);-s -w 1587714 字节(1.51 MiB) linux/amd64:默认 2346705 字节(2.24 MiB);-s -w 1515680 字节(1.45 MiB) linux/arm64:默认 2342341 字节(2.23 MiB);-s -w 1573024 字节(1.50 MiB) windows/amd64:默认 2496512 字节(2.38 MiB);-s -w 1660416 字节(1.58 MiB)
交叉编译出的 hello world 大小相近,windows 稍大。数字都是 CGO_ENABLED=0、go build 默认选项的结果,没有试 -gcflags、UPX 等其它减小体积的手段。