最近一位朋友问我“为什么同样的hello world 入门程序”为什么golang编译出来的二进制文件,比 C 大,而且大很多。我做了个测试,来分析这个问题。C 语言的hello world程序:

#include

int main() {

printf("hello world!\n");

return 0;

}

golang 语言的hello world程序:

package main

import "fmt"

func main() {

fmt.Printf("hello, world!\n")

}

编译,查看生成文件大小

root@cnxct:/home/cfc4n/go_vs_c# gcc -o c.out main.c

root@cnxct:/home/cfc4n/go_vs_c# go build -o go_fmt.out main_fmt.go

root@cnxct:/home/cfc4n/go_vs_c# ll

total 1552

drwxr-xr-x 2 root root 4096 Sep 20 16:56 ./

drwxr-xr-x 8 cfc4n cfc4n 4096 Sep 20 16:54 ../

-rwxr-xr-x 1 root root 8600 Sep 20 16:56 c.out*

-rwxr-xr-x 1 root root 1560062 Sep 20 16:56 go_fmt.out*

-rw-r--r-- 1 root root 78 Sep 20 16:54 main.c

-rw-r--r-- 1 root root 78 Sep 20 16:55 main_fmt.go

root@cnxct:/home/cfc4n/go_vs_c# du -sh *

12Kc.out

1.5Mgo_fmt.out

4.0Kmain.c

4.0Kmain_fmt.go

正如这位朋友所说c.out是12K,而 go_fmt.out是1.5M,差距奇大无比….为什么呢?

这两个二进制可执行文件文件里,都包含了什么?

众所周知,linux 上的二进制可执行文件是 ELF Executable and Linkable Format 可执行和可链接格式

elf-layout-en.svg

ELF文件格式组成

如上图,ELF 文件分为如下:

ELF文件的组成:ELF header

程序头:描述段信息

Section头:链接与重定位需要的数据

程序头与Section头需要的数据.text .data

在 Linux 上, 查看elf格式构成可以使用readelf

ELF Header:头的信息

root@cnxct:/home/cfc4n/go_vs_c# readelf -h c.out

ELF Header:

Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

Class: ELF64

Data: 2's complement, little endian

Version: 1 (current)

OS/ABI: UNIX - System V

ABI Version: 0

Type: EXEC (Executable file)

Machine: Advanced Micro Devices X86-64

Version: 0x1

Entry point address: 0x400430

Start of program headers: 64 (bytes into file)

Start of section headers: 6616 (bytes into file)

Flags: 0x0

Size of this header: 64 (bytes)

Size of program headers: 56 (bytes)

Number of program headers: 9

Size of section headers: 64 (bytes)

Number of section headers: 31

Section header string table index: 28

root@cnxct:/home/cfc4n/go_vs_c# readelf -h go_fmt.out

ELF Header:

Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

Class: ELF64

Data: 2's complement, little endian

Version: 1 (current)

OS/ABI: UNIX - System V

ABI Version: 0

Type: EXEC (Executable file)

Machine: Advanced Micro Devices X86-64

Version: 0x1

Entry point address: 0x44e360

Start of program headers: 64 (bytes into file)

Start of section headers: 456 (bytes into file)

Flags: 0x0

Size of this header: 64 (bytes)

Size of program headers: 56 (bytes)

Number of program headers: 7

Size of section headers: 64 (bytes)

Number of section headers: 23

Section header string table index: 3

ELF 头的长度都是一样的,不会带来总体体积的变化。区别是个别字节的值不一样,比如Entry point address 程序入口点的值不一样等。

接下来是 程序头:,也就是 section部分(在linker连接器的角度是section部分或者装载器角度的segment)

root@cnxct:/home/cfc4n/go_vs_c# readelf -d c.out

Dynamic section at offset 0xe28 contains 24 entries:

Tag Type Name/Value

0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

0x000000000000000c (INIT) 0x4003c8

0x000000000000000d (FINI) 0x4005b4

0x0000000000000019 (INIT_ARRAY) 0x600e10

0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)

0x000000000000001a (FINI_ARRAY) 0x600e18

0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)

0x000000006ffffef5 (GNU_HASH) 0x400298

0x0000000000000005 (STRTAB) 0x400318

0x0000000000000006 (SYMTAB) 0x4002b8

0x000000000000000a (STRSZ) 61 (bytes)

0x000000000000000b (SYMENT) 24 (bytes)

0x0000000000000015 (DEBUG) 0x0

0x0000000000000003 (PLTGOT) 0x601000

0x0000000000000002 (PLTRELSZ) 48 (bytes)

0x0000000000000014 (PLTREL) RELA

0x0000000000000017 (JMPREL) 0x400398

0x0000000000000007 (RELA) 0x400380

0x0000000000000008 (RELASZ) 24 (bytes)

0x0000000000000009 (RELAENT) 24 (bytes)

0x000000006ffffffe (VERNEED) 0x400360

0x000000006fffffff (VERNEEDNUM) 1

0x000000006ffffff0 (VERSYM) 0x400356

0x0000000000000000 (NULL) 0x0

可以看到c.out里引用了一个动态链接库libc.so.6,再看下go_fmt.out的情况

root@cnxct:/home/cfc4n/go_vs_c# readelf -d go_fmt.out

There is no dynamic section in this file.

c.out的执行,依赖了libc.so.6, libc.so.6肯定需要ld.so的,看下依赖情况,

root@cnxct:/home/cfc4n/go_vs_c# ldd c.out

linux-vdso.so.1 => (0x00007fff3a195000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ac4d06000)

/lib64/ld-linux-x86-64.so.2 (0x0000558ece3fe000)

root@cnxct:/home/cfc4n/go_vs_c# ldd go_fmt.out

not a dynamic executable

5eb64008ef3c606c43d2061e7107b594.png

依赖了libc.so这个动态链接库

也就是说,C的程序默认使用了libc.so动态链接库,go 的程序,默认进行了静态编译,不依赖任何动态链接库。所以体积变大了。 那么,只是这一个原因吗?

我在 golang 的官方文档里找到如下的解释:

Why is my trivial program such a large binary?

The linker in the gc tool chain creates statically-linked binaries by default. All Go binaries therefore include the Go run-time, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces.

A simple C “hello, world” program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf is around 1.5 MB, but that includes more powerful run-time support and type information.

将c的程序也使用静态编译试试。。。

gcc -static -o c_static.out main.c

root@cnxct:/home/cfc4n/go_vs_c# du -sh *

12Kc.out

888Kc_static.out

1.5Mgo_fmt.out

4.0Kmain.c

4.0Kmain_fmt.go

可以看到,使用静态编译生成的二进制文件c_static.out为888K,仍然比 GO 写的小了一半,这到底是为什么呢?到底是哪里多了?

在ELF 可执行文件里,就需要以程序编译链接的角度来分析了,对于一个 ELF 文件的分析,上面部分分析过 ELF header部分,以及 dynamic section的情况了。再以看一下剩余的section信息。

c7b837f6a459d5b6029de3657f7c9aa3.png

链接器视图与加载器视图

ELF中的section主要提供给Linker使用, 而segment提供给Loader用,Linker需要关心.text, .rel.text, .data, .rodata等等,关键是Linker需要做relocation。而Loader只需要知道这个段的Read、Write、Execute的属性。

再去看go_fmt.out里都包含了什么,为了方便校对,写了一个程序来对比

package main

import (

"debug/elf"

"fmt"

"os"

)

func main() {

if len(os.Args) != 3 {

fmt.Println("参数不对")

os.Exit(0)

}

strFile1 := os.Args[1]

strFile2 := os.Args[2]

f1, e := elf.Open(strFile1)

if e != nil {

panic(e)

}

f2, e := elf.Open(strFile2)

if e != nil {

panic(e)

}

mapSection1 := make(map[string]string, 0)

mapSection2 := make(map[string]string, 0)

//[Nr] Name Type Address Offset Size EntSize Flags Link Info Align

var size1 uint64

var size2 uint64

for _, s := range f1.Sections {

mapSection1[s.Name] = fmt.Sprintf("%s\t%s\t%s\t%010x\t%010x\t%d\t%x\t%s\t%x\t%x\t%x\t", s.Name, strFile1, s.Type.String(), s.Addr, s.Offset, s.Size, s.Entsize, s.Flags.String(), s.Link, s.Info, s.Addralign)

size1 += s.Size

}

for _, s := range f2.Sections {

mapSection2[s.Name] = fmt.Sprintf("%s\t%s\t%s\t%010x\t%010x\t%d\t%x\t%s\t%x\t%x\t%x\t", s.Name, strFile2, s.Type.String(), s.Addr, s.Offset, s.Size, s.Entsize, s.Flags.String(), s.Link, s.Info, s.Addralign)

size2 += s.Size

}

fmt.Println(fmt.Sprintf("%s:%d\t%s:%d", strFile1, size1, strFile2, size2))

fmt.Println("Name\tFile\tType\tAddress\tOffset\tSize\tEntSize\tFlags\tLink\tInfo\tAlign")

for k, v := range mapSection1 {

fmt.Println(v)

if v1, found := mapSection2[k]; found {

fmt.Println(v1)

delete(mapSection2, k)

}

}

for _, v := range mapSection2 {

fmt.Println(v)

}

}

对比一下两个文件的section段信息

root@cnxct:/home/cfc4n/go_vs_c# ./diffelf c_static.out go_fmt.out

c_static.out:910462go_fmt.out:1674012

NameFileTypeAddressOffsetSizeEntSizeFlagsLinkInfoAlign

.init_arrayc_static.outSHT_INIT_ARRAY00006c8ed800000c8ed8160SHF_WRITE+SHF_ALLOC008

.fini_arrayc_static.outSHT_FINI_ARRAY00006c8ee800000c8ee8160SHF_WRITE+SHF_ALLOC008

.datac_static.outSHT_PROGBITS00006c908000000c908068640SHF_WRITE+SHF_ALLOC0020

.datago_fmt.outSHT_PROGBITS00004fa4e000000fa4e074400SHF_WRITE+SHF_ALLOC0020

.strtabc_static.outSHT_STRTAB000000000000000d6b402670300x0001

.strtabgo_fmt.outSHT_STRTAB000000000000001704e05148600x0001

__libc_subfreeresc_static.outSHT_PROGBITS00004bd6a800000bd6a8800SHF_ALLOC008

__libc_thread_subfreeresc_static.outSHT_PROGBITS00004bd70800000bd70880SHF_ALLOC008

.gotc_static.outSHT_PROGBITS00006c8fe800000c8fe8168SHF_WRITE+SHF_ALLOC008

.commentc_static.outSHT_PROGBITS000000000000000cab50521SHF_MERGE+SHF_STRINGS001

.finic_static.outSHT_PROGBITS00004a047000000a047090SHF_ALLOC+SHF_EXECINSTR004

.eh_framec_static.outSHT_PROGBITS00004bd71000000bd710449480SHF_ALLOC008

.bssc_static.outSHT_NOBITS00006cab6000000cab5062640SHF_WRITE+SHF_ALLOC0020

.bssgo_fmt.outSHT_NOBITS00004fc20000000fc2001088080SHF_WRITE+SHF_ALLOC0020

.gcc_except_tablec_static.outSHT_PROGBITS00004c86a400000c86a41790SHF_ALLOC001

.tdatac_static.outSHT_PROGBITS00006c8eb800000c8eb8320SHF_WRITE+SHF_ALLOC+SHF_TLS008

.note.gnu.build-idc_static.outSHT_NOTE00004001b000000001b0360SHF_ALLOC004

__libc_freeres_fnc_static.outSHT_PROGBITS000049de60000009de6095130SHF_ALLOC+SHF_EXECINSTR0010

.pltc_static.outSHT_PROGBITS00004002f000000002f01600SHF_ALLOC+SHF_EXECINSTR0010

.note.stapsdtc_static.outSHT_NOTE000000000000000cab84386400x0004

.symtabc_static.outSHT_SYMTAB000000000000000cbaa045216180x0202c78

.symtabgo_fmt.outSHT_SYMTAB0000000000000016400050400180x0165f8

.note.ABI-tagc_static.outSHT_NOTE00004001900000000190320SHF_ALLOC004

.rela.pltc_static.outSHT_RELA00004001d800000001d824018SHF_ALLOC+SHF_INFO_LINK0188

.rodatac_static.outSHT_PROGBITS00004a048000000a04801193320SHF_ALLOC0020

.rodatago_fmt.outSHT_PROGBITS000047e000000007e0002123440SHF_ALLOC0020

.data.rel.roc_static.outSHT_PROGBITS00006c8f0000000c8f002280SHF_WRITE+SHF_ALLOC0020

.initc_static.outSHT_PROGBITS00004002c800000002c8260SHF_ALLOC+SHF_EXECINSTR004

.textc_static.outSHT_PROGBITS000040039000000003906458280SHF_ALLOC+SHF_EXECINSTR0010

.textgo_fmt.outSHT_PROGBITS000040100000000010005087790SHF_ALLOC+SHF_EXECINSTR0010

__libc_atexitc_static.outSHT_PROGBITS00004bd6f800000bd6f880SHF_ALLOC008

.stapsdt.basec_static.outSHT_PROGBITS00004bd70000000bd70010SHF_ALLOC001

.jcrc_static.outSHT_PROGBITS00006c8ef800000c8ef880SHF_WRITE+SHF_ALLOC008

c_static.outSHT_NULL00000000000000000000000x0000

go_fmt.outSHT_NULL00000000000000000000000x0000

__libc_thread_freeres_fnc_static.outSHT_PROGBITS00004a039000000a03902220SHF_ALLOC+SHF_EXECINSTR0010

__libc_freeres_ptrsc_static.outSHT_NOBITS00006cc3d800000cab50480SHF_WRITE+SHF_ALLOC008

.shstrtabc_static.outSHT_STRTAB000000000000000dd38f36100x0001

.shstrtabgo_fmt.outSHT_STRTAB000000000000000b1d8025700x0001

.tbssc_static.outSHT_NOBITS00006c8ed800000c8ed8480SHF_WRITE+SHF_ALLOC+SHF_TLS008

.got.pltc_static.outSHT_PROGBITS00006c900000000c90001048SHF_WRITE+SHF_ALLOC008

.itablinkgo_fmt.outSHT_PROGBITS00004b29d800000b29d8560SHF_ALLOC008

.gopclntabgo_fmt.outSHT_PROGBITS00004b2a2000000b2a202824140SHF_ALLOC0020

.debug_abbrevgo_fmt.outSHT_PROGBITS000051c00000000fd00025500x0001

.debug_framego_fmt.outSHT_PROGBITS000052b2d5000010c2d56956400x0001

.debug_arangesgo_fmt.outSHT_PROGBITS000054634c000012734c4800x0001

.debug_infogo_fmt.outSHT_PROGBITS00005463a600001273a624863800x0001

.note.go.buildidgo_fmt.outSHT_NOTE0000400fc80000000fc8560SHF_ALLOC004

.debug_pubtypesgo_fmt.outSHT_PROGBITS000053e9e5000011f9e53107900x0001

.debug_gdb_scriptsgo_fmt.outSHT_PROGBITS000054637c000012737c4200x0001

.debug_linego_fmt.outSHT_PROGBITS000051c0ff00000fd0ff6191000x0001

.typelinkgo_fmt.outSHT_PROGBITS00004b1ea000000b1ea028720SHF_ALLOC0020

.noptrdatago_fmt.outSHT_PROGBITS00004f800000000f800094160SHF_WRITE+SHF_ALLOC0020

.gosymtabgo_fmt.outSHT_PROGBITS00004b2a1000000b2a1000SHF_ALLOC001

.noptrbssgo_fmt.outSHT_NOBITS0000516b200000116b20180800SHF_WRITE+SHF_ALLOC0020

.debug_pubnamesgo_fmt.outSHT_PROGBITS000053c291000011d2911006800x0001

发现go_fmt.out多了好多.debug_*开头的 section,这是用于 debug 的段信息。再次编译,去除这些信息,同时也把 C 静态编译的二进制也去除符号表和重定位信息。

root@cnxct:/home/cfc4n/go_vs_c# gcc -static -o c_static_gs.out -g -s main.c

root@cnxct:/home/cfc4n/go_vs_c# go build -o go_fmt_sw.out -ldflags="-s -w" main_fmt.go

root@cnxct:/home/cfc4n/go_vs_c# du -sh *

12Kc.out

820Kc_static_gs.out

888Kc_static.out

1.5Mgo_fmt.out

1012Kgo_fmt_sw.out

如上结果,go_fmt_sw.out为1012K,c_static_gs.out为820K,还大了近200KB。到底是哪里大的呢?

刚刚的两个elf 文件的section对比中,还有一个比较特殊的go_fmt.out中 有一个名字叫.gopclntab的段,类型是SHT_PROGBITS程序段,大小为 282414字节,也就是275K,在c_static.out里并没有这个段的,也没有.gosymtab这个段。二者不一样,section段名字有规范标准吗?

其实,对于linker链接器来说,会关心段(section)的名字,但对loader加载器来说,并不关心名字,只关心这个段(segment)的权限,是否可执行,所在的偏移地址,用于函数的执行。

那.gopclntab段包含了什么内容呢?我写了一个程序分析了这个段的内容,程序代码如下:

package main

import (

"debug/elf"

"debug/gosym"

"fmt"

"os"

)

func main() {

if len(os.Args) != 2 {

fmt.Println("参数不对")

os.Exit(0)

}

strFile1 := os.Args[1]

f1, err := elf.Open(strFile1)

if err != nil {

panic(err)

}

symtab, err := f1.Section(".gosymtab").Data()

if err != nil {

f1.Close()

panic(".gosymtab 异常")

}

gopclntab, err := f1.Section(".gopclntab").Data()

if err != nil {

f1.Close()

panic(".gopclntab 异常")

}

pcln := gosym.NewLineTable(gopclntab, f1.Section(".text").Addr)

var tab *gosym.Table

tab, err = gosym.NewTable(symtab, pcln)

if err != nil {

f1.Close()

panic(err)

}

for _, x := range tab.Funcs {

fmt.Println(fmt.Sprintf("addr:0x%x\t\tname:%s,\t",x.Entry,x.Name))

}

}

编译后执行

root@cnxct:/home/cfc4n/go_vs_c# ./expelf go_fmt.out

addr:0x401000name:sync/atomic.StoreUint32,

addr:0x401010name:sync/atomic.StoreUint64,

addr:0x401020name:sync/atomic.StoreUintptr,

addr:0x401030name:runtime.memhash0,

addr:0x401040name:runtime.memhash8,

......

addr:0x427040name:runtime.printnl,

......

addr:0x44dc80name:runtime.memmove,

addr:0x44e360name:_rt0_amd64_linux,

addr:0x44e380name:main,

addr:0x44e390name:runtime.exit,

addr:0x44ea70name:runtime.epollwait,

addr:0x44ea90name:runtime.(*cpuProfile).(runtime.flushlog)-fm,

addr:0x44eae0name:type..hash.runtime.uncommontype,

......

addr:0x452d60name:math.init.1,

addr:0x452e00name:math.init,

addr:0x452e70name:math.hasSSE4,

addr:0x452e90name:type..hash.[70]float64,

addr:0x452f10name:type..eq.[70]float64,

addr:0x452f50name:errors.New,

addr:0x452ff0name:errors.(*errorString).Error,

......

addr:0x4534c0name:unicode/utf8.RuneCountInString,

addr:0x453600name:strconv.(*decimal).String,

addr:0x453a00name:strconv.digitZero,

addr:0x453a30name:strconv.trim,

addr:0x453aa0name:strconv.(*decimal).Assign,

......

addr:0x4599c0name:strconv.init,

addr:0x459ad0name:type..hash.strconv.decimal,

addr:0x459ec0name:type..eq.[61]strconv.leftCheat,

addr:0x459f80name:sync.(*Mutex).Lock,

......

addr:0x45c6d0name:syscall.Syscall6,

addr:0x45c740name:type..hash.[133]string,

addr:0x45c7c0name:type..eq.[133]string,

addr:0x45c880name:time.init,

addr:0x45df30name:type..hash.os.PathError,

addr:0x45dfc0name:type..eq.os.PathError,

addr:0x45e0e0name:reflect.makeMethodValue,

addr:0x45eaf0name:reflect.resolveReflectName,

addr:0x45eb40name:reflect.(*rtype).nameOff,

......

addr:0x4710d0name:reflect.(*funcTypeFixed64).Comparable,

addr:0x4710f0name:type..hash.reflect.funcTypeFixed128,

addr:0x471170name:type..eq.reflect.funcTypeFixed128,

addr:0x471230name:reflect.(*funcTypeFixed128).uncommon,

......

addr:0x471c50name:reflect.(*sliceType).Comparable,

addr:0x471c70name:type..hash.struct { reflect.b bool; reflect.x interface {} },

addr:0x471cf0name:type..eq.struct { reflect.b bool; reflect.x interface {} },

addr:0x471d70name:type..hash.[27]string,

addr:0x471df0name:type..eq.[27]string,

addr:0x471ea0name:fmt.(*fmt).writePadding,

addr:0x472020name:fmt.(*fmt).pad,

......

addr:0x47b730name:fmt.(*pp).badArgNum,

addr:0x47b940name:fmt.(*pp).missingArg,

addr:0x47bb50name:fmt.(*pp).doPrintf,

addr:0x47cf70name:fmt.glob..func1,

addr:0x47cfd0name:fmt.init,

addr:0x47d170name:type..hash.fmt.fmt,

addr:0x47d1f0name:type..eq.fmt.fmt,

addr:0x47d2a0name:main.main,

addr:0x47d310name:main.init,

如上可以看到,有很多函数是以fmt.(*pp)、strconv.*、sync.*、reflect.*、unicode.*等开头的,后面对应的函数名,也与 golang 的包里对应的包中函数名一致。。。用 IDA来确认一遍

a0b6efc709ecad7b072b424b16e179fe.png,果然在 .gopclntab 段里有很多 reflect.*开头的函数。

这就很奇怪了,golang 编译时,默认把 runtime 包编译进来就好了,应该不会把strconv\sync\reflect\unicode等包包含进来啊。程序中,只写了一句fmt.Println(),莫非是fmt包import了其他几个包导致的?回去搜了下代码,果然…

e76b0f33bb3f78f61217378cba984b6b.png

ffa82afe525ff19bcc5b8297fbf82e5e.png

嗯,应该是这里问题,改用 go 的内置函数print试试。

package main

func main() {

print("hello, world!\n")

}

编译后,对比大小

root@cnxct:/home/cfc4n/go_vs_c# go build -o go_print.out main_print.go

root@cnxct:/home/cfc4n/go_vs_c# go build -o go_print_sw.out -ldflags="-s -w" main_print.go

root@cnxct:/home/cfc4n/go_vs_c# du -sh *

12Kc.out

820Kc_static_gs.out

888Kc_static.out

1.5Mgo_fmt.out

1012Kgo_fmt_sw.out

940Kgo_print.out

624Kgo_print_sw.out

4.0Kmain.c

4.0Kmain_fmt.go

4.0Kmain_print.go

看如上结果,go_print_sw.out 变成了 624K, c_static_gs.out为820K,不光没比C的静态编译的大,还比它小呢。。。 不过呢,这也不能说明什么问题,只是因为其包含的函数内容不一样。

好了,至此已经知道为什么 golang 编译的文件比 C 的大了,因为 go 语言是静态编译的,而 C 的编译(比如 gcc编译器)都是动态链接库形式编译的。所以,导致了 go 编译的文件稍微大的问题。其次,跟其他语言比较字符串输出的话,用print内置函数就好了,就不要使用fmt包下的函数来比较了,因为 fmt 包引入了好多其他的包。。。这也增加编译后的二进制文件的体积。

其实呢,golang 的编译(不涉及 cgo 编译的前提下)默认使用了静态编译,不依赖任何动态链接库,这样可以任意部署到各种运行环境,不用担心依赖库的版本问题。只是体积大一点而已,存储时占用了一点磁盘,运行时,多占用了一点内存。早期动态链接库的产生,是因为早期的系统的内存资源十分宝贵,由于内存紧张的问题在早期的系统中显得更加突出,因此人们首先想到的是要解决内存使用效率不高这一问题,于是便提出了动态装入的思想。也就产生了动态链接库。在现在的计算机里,操作系统的硬盘内存更大了,尤其是服务器,32G、64G 的内存都是最基本的。可以不用为了节省几百 KB 或者1M,几 M 的内存而大大费周折了。而 golang 就采用这种做法,可以避免各种 so 动态链接库依赖的问题,这点是非常值得称赞的。

另外,.gopclntab 跟 .gosymtab段的讨论,以及其他建议

e42f3812330f272021b64aa998232a7d.png

程序员的自我修养

参考:

No related posts.