这篇文章给大家聊聊关于网站层级目录源码分享,以及网站目录的层次一般不得超过几层对应的知识点,希望对各位有所帮助,不要忘了收藏本站哦。
Go语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!
刚开始写这篇文章的时候,目标非常大,想要探索Go程序的一生:编码、编译、汇编、链接、运行、退出。它的每一步具体如何进行,力图弄清Go程序的这一生。
在这个过程中,我又复习了一遍《程序员的自我修养》。这是一本讲编译、链接的书,非常详细,值得一看!数年前,我第一次看到这本书的书名,就非常喜欢。因为它模仿了周星驰喜剧之王里出现的一本书——《演员的自我修养》。心向往之!
在开始本文之前,先推荐一下王晶大佬(以前在滴滴)的博客——《面向信仰编程》,他的Go编译系列文章,非常有深度,直接深入编译器源代码,我是看了很多遍了。博客链接可以从参考资料里获取。
理想很大,实现的难度也是非常大。为了避免砸了“深度解密”这个牌子,这次起了个更温和的名字,嘿嘿。
引入
我们从一个HelloWorld的例子开始:
packagemain\nimport”fmt”\nfuncmain(){\nfmt.Println(“helloworld”)\n}\n
当我用我那价值1800元的cherry键盘潇洒地敲完上面的helloworld代码时,保存在硬盘上的hello.go文件就是一个字节序列了,每个字节代表一个字符。
用vim打开hello.go文件,在命令行模式下,输入命令:
:%!xxd\n
就能在vim里以十六进制查看文件内容:
最左边的一列代表地址值,中间一列代表文本对应的ASCII字符,最右边的列就是我们的代码。再在终端里执行manascii:
和ASCII字符表一对比,就能发现,中间的列和最右边的列是一一对应的。也就是说,刚刚写完的hello.go文件都是由ASCII字符表示的,它被称为文本文件,其他文件被称为二进制文件。
当然,更深入地看,计算机中的所有数据,像磁盘文件、网络中的数据其实都是一串比特位组成,取决于如何看待它。在不同的情景下,一个相同的字节序列可能表示成一个整数、浮点数、字符串或者是机器指令。
而像hello.go这个文件,8个bit,也就是一个字节看成一个单位(假定源程序的字符都是ASCII码),最终解释成人类能读懂的Go源码。
Go程序并不能直接运行,每条Go语句必须转化为一系列的低级机器语言指令,将这些指令打包到一起,并以二进制磁盘文件的形式存储起来,也就是可执行目标文件。
从源文件到可执行目标文件的转化过程:
完成以上各个阶段的就是Go编译系统。你肯定知道大名鼎鼎的GCC(GNUCompileCollection),中文名为GNU编译器套装,它支持像C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能够为很多不同的机器生成机器码。
可执行目标文件可以直接在机器上执行。一般而言,先执行一些初始化的工作;找到main函数的入口,执行用户写的代码;执行完成后,main函数退出;再执行一些收尾的工作,整个过程完毕。
在接下来的文章里,我们将探索编译和运行的过程。
编译链接概述
Go源码里的编译器源码位于src/cmd/compile路径下,链接器源码位于src/cmd/link路径下。
编译过程
我比较喜欢用IDE(集成开发环境)来写代码,Go源码用的Goland,有时候直接点击IDE菜单栏里的“运行”按钮,程序就跑起来了。这实际上隐含了编译和链接的过程,我们通常将编译和链接合并到一起的过程称为构建(Build)。
编译过程就是对源文件进行词法分析、语法分析、语义分析、优化,最后生成汇编代码文件,以.s作为文件后缀。
之后,汇编器会将汇编代码转变成机器可以执行的指令。由于每一条汇编语句几乎都与一条机器指令相对应,所以只是一个简单的一一对应,比较简单,没有语法、语义分析,也没有优化这些步骤。
编译器是将高级语言翻译成机器语言的一个工具,编译过程一般分为6步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。下图来自《程序员的自我修养》:
词法分析
通过前面的例子,我们知道,Go程序文件在机器看来不过是一堆二进制位。我们能读懂,是因为Goland按照ASCII码(实际上是UTF-8)把这堆二进制位进行了编码。例如,把8个bit位分成一组,对应一个字符,通过对照ASCII码表就可以查出来。
当把所有的二进制位都对应成了ASCII码字符后,我们就能看到有意义的字符串。它可能是关键字,例如:package;可能是字符串,例如:“HelloWorld”。
词法分析其实干的就是这个。输入是原始的Go程序文件,在词法分析器看来,就是一堆二进制位,根本不知道是什么东西,经过它的分析后,变成有意义的记号。简单来说,词法分析是计算机科学中将字符序列转换为标记(token)序列的过程。
我们来看一下维基百科上给出的定义:
词法分析(lexicalanalysis)是计算机科学中将字符序列转换为标记(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexicalanalyzer,简称lexer),也叫扫描器(scanner)。词法分析器一般以函数的形式存在,供语法分析器调用。
.go文件被输入到扫描器(Scanner),它使用一种类似于有限状态机的算法,将源代码的字符系列分割成一系列的记号(Token)。
记号一般分为这几类:关键字、标识符、字面量(包含数字、字符串)、特殊符号(如加号、等号)。
例如,对于如下的代码:
slice[i]=i*(2+6)\n
总共包含16个非空字符,经过扫描后:
上面的例子源自《程序员的自我修养》,主要讲解编译、链接相关的内容,很精彩,推荐研读。
Go语言(本文的Go版本是1.9.2)扫描器支持的Token在源码中的路径:
src/cmd/compile/internal/syntax/token.go\n
感受一下:
还是比较熟悉的,包括名称和字面量、操作符、分隔符和关键字。
而扫描器的路径是:
src/cmd/compile/internal/syntax/scanner.go\n
其中最关键的函数就是next函数,它不断地读取下一个字符(不是下一个字节,因为Go语言支持Unicode编码,并不是像我们前面举得ASCII码的例子,一个字符只有一个字节),直到这些字符可以构成一个Token。
代码的主要逻辑就是通过c:=s.getr()获取下一个未被解析的字符,并且会跳过之后的空格、回车、换行、tab字符,然后进入一个大的switch-case语句,匹配各种不同的情形,最终可以解析出一个Token,并且把相关的行、列数字记录下来,这样就完成一次解析过程。
当前包中的词法分析器scanner也只是为上层提供了next方法,词法解析的过程都是惰性的,只有在上层的解析器需要时才会调用next获取最新的Token。
语法分析
上一步生成的Token序列,需要经过进一步处理,生成一棵以表达式为结点的语法树。
比如最开始的那个例子,slice[i]=i*(2+6),得到的一棵语法树如下:
整个语句被看作是一个赋值表达式,左子树是一个数组表达式,右子树是一个乘法表达式;数组表达式由2个符号表达式组成;乘号表达式则是由一个符号表达式和一个加号表达式组成;加号表达式则是由两个数字组成。符号和数字是最小的表达式,它们不能再被分解,通常作为树的叶子节点。
语法分析的过程可以检测一些形式上的错误,例如:括号是否缺少一半,+号表达式缺少一个操作数等。
语法分析是根据某种特定的形式文法(Grammar)对Token序列构成的输入文本进行分析并确定其语法结构的一种过程。
语义分析
语法分析完成后,我们并不知道语句的具体意义是什么。像上面的*号的两棵子树如果是两个指针,这是不合法的,但语法分析检测不出来,语义分析就是干这个事。
编译期所能检查的是静态语义,可以认为这是在“代码”阶段,包括变量类型的匹配、转换等。例如,将一个浮点值赋给一个指针变量的时候,明显的类型不匹配,就会报编译错误。而对于运行期间才会出现的错误:不小心除了一个0,语义分析是没办法检测的。
语义分析阶段完成之后,会在每个节点上标注上类型:
Go语言编译器在这一阶段检查常量、类型、函数声明以及变量赋值语句的类型,然后检查哈希中键的类型。实现类型检查的函数通常都是几千行的巨型switch/case语句。
类型检查是Go语言编译的第二个阶段,在词法和语法分析之后我们得到了每个文件对应的抽象语法树,随后的类型检查会遍历抽象语法树中的节点,对每个节点的类型进行检验,找出其中存在的语法错误。
在这个过程中也可能会对抽象语法树进行改写,这不仅能够去除一些不会被执行的代码对编译进行优化提高执行效率,而且也会修改make、new等关键字对应节点的操作类型。
例如比较常用的make关键字,用它可以创建各种类型,如slice,map,channel等等。到这一步的时候,对于make关键字,也就是OMAKE节点,会先检查它的参数类型,根据类型的不同,进入相应的分支。如果参数类型是slice,就会进入TSLICEcase分支,检查len和cap是否满足要求,如len<=cap。最后节点类型会从OMAKE改成OMAKESLICE。
中间代码生成
我们知道,编译过程一般可以分为前端和后端,前端生成和平台无关的中间代码,后端会针对不同的平台,生成不同的机器码。
前面词法分析、语法分析、语义分析等都属于编译器前端,之后的阶段属于编译器后端。
编译过程有很多优化的环节,在这个环节是指源代码级别的优化。它将语法树转换成中间代码,它是语法树的顺序表示。
中间代码一般和目标机器以及运行时环境无关,它有几种常见的形式:三地址码、P-代码。例如,最基本的三地址码是这样的:
x=yopz\n
表示变量y和变量z进行op操作后,赋值给x。op可以是数学运算,例如加减乘除。
前面我们举的例子可以写成如下的形式:
t1=2+6\nt2=i*t1\nslice[i]=t2\n
这里2+6是可以直接计算出来的,这样就把t1这个临时变量“优化”掉了,而且t1变量可以重复利用,因此t2也可以“优化”掉。优化之后:
t1=i*8\nslice[i]=t1\n
Go语言的中间代码表示形式为SSA(StaticSingle-Assignment,静态单赋值),之所以称之为单赋值,是因为每个名字在SSA中仅被赋值一次。。
这一阶段会根据CPU的架构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器的大小、可用寄存器列表等。中间代码生成和机器码生成这两部分会共享相同的设置。
在生成中间代码之前,会对抽象语法树中节点的一些元素进行替换。这里引用王晶大佬《面向信仰编程》编译原理相关博客里的一张图:
例如对于map的操作m[i],在这里会被转换成mapacess或mapassign。
Go语言的主程序在执行时会调用runtime中的函数,也就是说关键字和内置函数的功能其实是由语言的编译器和运行时共同完成的。
中间代码的生成过程其实就是从AST抽象语法树到SSA中间代码的转换过程,在这期间会对语法树中的关键字在进行一次更新,更新后的语法树会经过多轮处理转变最后的SSA中间代码。
目标代码生成与优化
不同机器的机器字长、寄存器等等都不一样,意味着在不同机器上跑的机器码是不一样的。最后一步的目的就是要生成能在不同CPU架构上运行的代码。
为了榨干机器的每一滴油水,目标代码优化器会对一些指令进行优化,例如使用移位指令代替乘法指令等。
这块实在没能力深入,幸好也不需要深入。对于应用层的软件开发工程师来说,了解一下就可以了。
链接过程
编译过程是针对单个文件进行的,文件与文件之间不可避免地要引用定义在其他模块的全局变量或者函数,这些变量或函数的地址只有在此阶段才能确定。
链接过程就是要把编译器生成的一个个目标文件链接成可执行文件。最终得到的文件是分成各种段的,比如数据段、代码段、BSS段等等,运行时会被装载到内存中。各个段具有不同的读写、执行属性,保护了程序的安全运行。
这部分内容,推荐看《程序员的自我修养》和《深入理解计算机系统》。
Go程序启动
仍然使用hello-world项目的例子。在项目根目录下执行:
gobuild-gcflags”-N-l”-ohellosrc/main.go\n
-gcflags”-N-l”是为了关闭编译器优化和函数内联,防止后面在设置断点的时候找不到相对应的代码位置。
得到了可执行文件hello,执行:
[qcrao@qcraohello-world]$gdbhello\n
进入gdb调试模式,执行infofiles,得到可执行文件的文件头,列出了各种段:
同时,我们也得到了入口地址:0x450e20。
(gdb)b*0x450e20\nBreakpoint1at0x450e20:file/usr/local/go/src/runtime/rt0_linux_amd64.s,line8.\n
这就是Go程序的入口地址,我是在linux上运行的,所以入口文件为src/runtime/rt0_linux_amd64.s,runtime目录下有各种不同名称的程序入口文件,支持各种操作系统和架构,代码为:
TEXT_rt0_amd64_linux(SB),NOSPLIT,$-8\nLEAQ8(SP),SI//argv\nMOVQ0(SP),DI//argc\nMOVQ$main(SB),AX\nJMPAX\n
主要是把argc,argv从内存拉到了寄存器。这里LEAQ是计算内存地址,然后把内存地址本身放进寄存器里,也就是把argv的地址放到了SI寄存器中。最后跳转到:
TEXTmain(SB),NOSPLIT,$-8\nMOVQ$runtime·rt0_go(SB),AX\nJMPAX\n
继续跳转到runtime·rt0_go(SB),位置:/usr/local/go/src/runtime/asm_amd64.s,代码:
参考文献里的一篇文章【探索golang程序启动过程】研究得比较深入,总结下:
检查运行平台的CPU,设置好程序运行需要相关标志。TLS的初始化。runtime.args、runtime.osinit、runtime.schedinit三个方法做好程序运行需要的各种变量与调度器。runtime.newproc创建新的goroutine用于绑定用户写的main方法。runtime.mstart开始goroutine的调度。
最后用一张图来总结gobootstrap过程吧:
main函数里执行的一些重要的操作包括:新建一个线程执行sysmon函数,定期垃圾回收和调度抢占;启动gc;执行所有的init函数等等。
上面是启动过程,看一下退出过程:
当main函数执行结束之后,会执行exit(0)来退出进程。若执行exit(0)后,进程没有退出,main函数最后的代码会一直访问非法地址:
exit(0)\nfor{\nvarx*int32\n*x=0\n}\n
正常情况下,一旦出现非法地址访问,系统会把进程杀死,用这样的方法确保进程退出。
关于程序退出这一段的阐述来自群聊《golangruntime阅读》,又是一个高阶的读源码的组织,github主页见参考资料。
当然Go程序启动这一部分其实还会涉及到fork一个新进程、装载可执行文件,控制权转移等问题。还是推荐看前面的两本书,我觉得我不会写得更好,就不叙述了。
GoRoot和GoPath
GoRoot是Go的安装路径。mac或unix是在/usr/local/go路径上,来看下这里都装了些什么:
bin目录下面:
pkg目录下面:
Go工具目录如下,其中比较重要的有编译器compile,链接器link:
GoPath的作用在于提供一个可以寻找.go源码的路径,它是一个工作空间的概念,可以设置多个目录。Go官方要求,GoPath下面需要包含三个文件夹:
src\npkg\nbin\n
src存放源文件,pkg存放源文件编译后的库文件,后缀为.a;bin则存放可执行文件。
Go命令详解
直接在终端执行:
go\n
就能得到和go相关的命令简介:
和编译相关的命令主要是:
gobuild\ngoinstall\ngorun\n
gobuild
gobuild用来编译指定packages里的源码文件以及它们的依赖包,编译的时候会到$GoPath/src/package路径下寻找源码文件。gobuild还可以直接编译指定的源码文件,并且可以同时指定多个。
通过执行gohelpbuild命令得到gobuild的使用方法:
usage:gobuild[-ooutput][-i][buildflags][packages]\n
-o只能在编译单个包的时候出现,它指定输出的可执行文件的名字。
-i会安装编译目标所依赖的包,安装是指生成与代码包相对应的.a文件,即静态库文件(后面要参与链接),并且放置到当前工作区的pkg目录下,且库文件的目录层级和源码层级一致。
至于buildflags参数,build,clean,get,install,list,run,test这些命令会共用一套:
我们知道,Go语言的源码文件分为三类:命令源码、库源码、测试源码。
命令源码文件:是Go程序的入口,包含funcmain()函数,且第一行用packagemain声明属于main包。
库源码文件:主要是各种函数、接口等,例如工具类的函数。
测试源码文件:以_test.go为后缀的文件,用于测试程序的功能和性能。
注意,gobuild会忽略*_test.go文件。
我们通过一个很简单的例子来演示gobuild命令。我用Goland新建了一个hello-world项目(为了展示引用自定义的包,和之前的hello-world程序不同),项目的结构如下:
最左边可以看到项目的结构,包含三个文件夹:bin,pkg,src。其中src目录下有一个main.go,里面定义了main函数,是整个项目的入口,也就是前面提过的所谓的命令源码文件;src目录下还有一个util目录,里面有util.go文件,定义了一个可以获取本机IP地址的函数,也就是所谓的库源码文件。
中间是main.go的源码,引用了两个包,一个是标准库的fmt;一个是util包,util的导入路径是util。所谓的导入路径是指相对于Go的源码目录$GoRoot/src或者$GoPath/src的下的子路径。例如main包里引用的fmt的源码路径是/usr/local/go/src/fmt,而util的源码路径是/Users/qcrao/hello-world/src/util,正好我们设置的GoPath=/Users/qcrao/hello-world。
最右边是库函数的源码,实现了获取本机IP的函数。
在src目录下,直接执行gobuild命令,在同级目录生成了一个可执行文件,文件名为src,使用./src命令直接执行,输出:
helloworld!\nLocalIP:192.168.1.3\n
我们也可以指定生成的可执行文件的名称:
gobuild-obin/hello\n
这样,在bin目录下会生成一个可执行文件,运行结果和上面的src一样。
其实,util包可以单独被编译。我们可以在项目根目录下执行:
gobuildutil\n
编译程序会去$GoPath/src路径找util包(其实是找文件夹)。还可以在./src/util目录下直接执行gobuild编译。
当然,直接编译库源码文件不会生成.a文件,因为:
gobuild命令在编译只包含库源码文件的代码包(或者同时编译多个代码包)时,只会做检查性的编译,而不会输出任何结果文件。
为了展示整个编译链接的运行过程,我们在项目根目录执行如下的命令:
gobuild-v-x-work-obin/hellosrc/main.go\n
-v会打印所编译过的包名字,-x打印编译期间所执行的命令,-work打印编译期间生成的临时文件路径,并且编译完成之后不会被删除。
执行结果:
从结果来看,图中用箭头标注了本次编译过程涉及2个包:util,command-line-arguments。第二个包比较诡异,源码里根本就没有这个名字好吗?其实这是gobuild命令检测到[packages]处填的是一个.go文件,因此创建了一个虚拟的包:command-line-arguments。
同时,用红框圈出了compile,link,也就是先编译了util包和main.go文件,分别得到.a文件,之后将两者进行链接,最终生成可执行文件,并且移动到bin目录下,改名为hello。
另外,第一行显示了编译过程中的工作目录,此目录的文件结构是:
可以看到,和hello-world目录的层级基本一致。command-line-arguments就是虚拟的main.go文件所处的包。exe目录下的可执行文件在最后一步被移动到了bin目录下,所以这里是空的。
整体来看,gobuild在执行时,会先递归寻找main.go所依赖的包,以及依赖的依赖,直至最底层的包。这里可以是深度优先遍历也可以是宽度优先遍历。如果发现有循环依赖,就会直接退出,这也是经常会发生的循环引用编译错误。
正常情况下,这些依赖关系会形成一棵倒着生长的树,树根在最上面,就是main.go文件,最下面是没有任何其他依赖的包。编译器会从最左的节点所代表的包开始挨个编译,完成之后,再去编译上一层的包。
这里,引用郝林老师几年前在github上发表的go命令教程,可以从参考资料找到原文地址。
从代码包编译的角度来说,如果代码包A依赖代码包B,则称代码包B是代码包A的依赖代码包(以下简称依赖包),代码包A是代码包B的触发代码包(以下简称触发包)。
执行gobuild命令的计算机如果拥有多个逻辑CPU核心,那么编译代码包的顺序可能会存在一些不确定性。但是,它一定会满足这样的约束条件:依赖代码包->当前代码包->触发代码包。
顺便推荐一个浏览器插件Octotree,在看github项目的时候,此插件可以在浏览器里直接展示整个项目的文件结构,非常方便:
到这里,你一定会发现,对于hello-wrold文件夹下的pkg目录好像一直没有涉及到。
其实,pkg目录下面应该存放的是涉及到的库文件编译后的包,也就是一些.a文件。但是gobuild执行过程中,这些.a文件放在临时文件夹中,编译完成后会被直接删掉,因此一般不会用到。
前面我们提到过,在gobuild命令里加上-i参数会安装这些库文件编译的包,也就是这些.a文件会放到pkg目录下。
在项目根目录执行gobuild-isrc/main.go后,pkg目录里增加了util.a文件:
darwin_amd64表示的是:
GOOS和GOARCH。这两个环境变量不用我们设置,系统默认的。
GOOS是Go所在的操作系统类型,GOARCH是Go所在的计算架构。
Mac平台上这个目录名就是darwin_amd64。
生成了util.a文件后,再次编译的时候,就不会再重新编译util.go文件,加快了编译速度。
同时,在根目录下生成了名称为main的可执行文件,这是以main.go的文件名命令的。
hello-world这个项目的代码已经上传到了github项目Go-Questions,这个项目由问题导入,企图串连Go的所有知识点,正在完善,期待你的star。地址见参考资料【Go-Questionshello-world项目】。
goinstall
goinstall用于编译并安装指定的代码包及它们的依赖包。相比gobuild,它只是多了一个“安装编译后的结果文件到指定目录”的步骤。
还是使用之前hello-world项目的例子,我们先将pkg目录删掉,在项目根目录执行:
goinstallsrc/main.go\n或者\ngoinstallutil\n
两者都会在根目录下新建一个pkg目录,并且生成一个util.a文件。
并且,在执行前者的时候,会在GOBIN目录下生成名为main的可执行文件。
所以,运行goinstall命令,库源码包对应的.a文件会被放置到pkg目录下,命令源码包生成的可执行文件会被放到GOBIN目录。
goinstall在GoPath有多个目录的时候,会产生一些问题,具体可以去看郝林老师的Go命令教程,这里不展开了。
gorun
gorun用于编译并运行命令源码文件。
在hello-world项目的根目录,执行gorun命令:
gorun-x-worksrc/main.go\n
-x可以打印整个过程涉及到的命令,-work可以看到临时的工作目录:
从上图中可以看到,仍然是先编译,再连接,最后直接执行,并打印出了执行结果。
第一行打印的就是工作目录,最终生成的可执行文件就是放置于此:
main就是最终生成的可执行文件。
总结
这次的话题太大了,困难重重。从编译原理到go启动时的流程,到go命令原理,每个话题单独抽出来都可以写很多。
幸好有一些很不错的书和博客文章可以去参考。这篇文章就作为一个引子,你可以跟随参考资料里推荐的一些内容去发散。
参考资料
【《程序员的自我修养》全书】https://book.douban.com/subject/3652388/
【面向信仰编程编译过程概述】https://draveness.me/golang-compile-intro
【golangruntime阅读】https://github.com/zboya/golang_runtime_reading
【Go-Questionshello-world项目】https://github.com/qcrao/Go-Questions/tree/master/examples/hello-world
【雨痕大佬的Go语言学习笔记】https://github.com/qyuhen/book
【vim以16进制文本】https://www.cnblogs.com/meibenjin/archive/2012/12/06/2806396.html
【Go编译命令执行过程】https://halfrost.com/go_command/
【Go命令执行过程】https://github.com/hyper0x/go_command_tutorial
【Go词法分析】https://ggaaooppeenngg.github.io/zh-CN/2016/04/01/go-lexer-%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/
【曹大博客golang与ast】http://xargin.com/ast/
【Golang词法解析器,scanner源码分析】https://blog.csdn.net/zhaoruixiang1111/article/details/89892435
【GopathExplained】https://flaviocopes.com/go-gopath/
【UnderstandingtheGOPATH】https://www.digitalocean.com/community/tutorials/understanding-the-gopath
【讨论】https://stackoverflow.com/questions/7970390/what-should-be-the-values-of-gopath-and-goroot
【Go官方Gopath】https://golang.org/cmd/go/#hdr-GOPATH_environment_variable
【Gopackage的探索】https://mp.weixin.qq.com/s/OizVLXfZ6EC1jI-NL7HqeA
【Go官方关于Go项目的组织结构】https://golang.org/doc/code.html
【Gomodules】https://www.melvinvivas.com/go-version-1-11-modules/
【GolangInstallation,Setup,GOPATH,andGoWorkspace】https://www.callicoder.com/golang-installation-setup-gopath-workspace/
【编译、链接过程链接】https://mikespook.com/2013/11/%E7%BF%BB%E8%AF%91-go-build-%E5%91%BD%E4%BB%A4%E6%98%AF%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%E7%9A%84%EF%BC%9F/
【1.5编译器由go语言完成】https://www.infoq.cn/article/2015/08/go-1-5
【Go编译过程系列文章】https://www.ct8olib.com/topics-3724.html
【曹大gobootstrap】https://github.com/cch123/golang-notes/blob/master/bootstrap.md
【golang启动流程】https://blog.iceinto.com/posts/go/start/
【探索golang程序启动过程】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2golang%E7%A8%8B%E5%BA%8F%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B/
【探索goroutine的创建】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
本文作者:饶全成,原创授权发布
OK,本文到此结束,希望对大家有所帮助。
