Go 程序是怎样跑起来的

引入

咱们从一个 helloworld 的比如开端

package main
import "fmt"
func main() {
    fmt.Println("hello world")
}

用 vim 要翻开,输入指令:

:%!xxd

下面是输出

00000000:7061 636b 6167 6520 6d61 696e 0a0a 696d package main.. im
00000010:706f 7274 2022 666d 7422 0a0a 6675 6e63 port "fmt".. func
00000020:206d 6169 6e28 2920 7b0a 2020 2020 666d main(){. fm
00000030:742e 5072 696e 746c 6e28 2268 656c 6c6f t. Println("hello
00000040:2077 6f72 6c64 2229 0a7d 0a world").}

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

和 ascii 一对比,就能发现,中心的列和最右边的一一对应。也便是说,刚写完的 hello.go 是由ascii字符表明的。被称为文本文件。

go程序并不能直接运转,每条 go 句子必须转化为一系列的低级机器言语指令,将这些指令

打包到一起,并以二进制格式存储,也便是可履行文件。

从源文件到可履行方针文件的转化进程:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

完结以上各个阶段的便是 go 编译体系。你必定听过 GCC,它支撑 C/C++/JAVA/PYTHON/OBJECTIV-CAADA/FORTRAN/PASCAL,能够为许多不同的机器生成机器码。

可履行方针文件能够直接在机器上履行。一般而言,

  • 先履行一些初始化的作业;

  • 找到main函数的进口,履行用户写的代码;

  • 履行完结后,main 函数退出;

  • 再履行一些收尾的作业,整个进程完毕。

接下来,咱们将探索 编译运转 的进程

编译链接

Go 源码里的编译器源码坐落 src/cmd/compile 途径下,链接器源码坐落 src/cmd/link 途径下。

编译进程

我比较喜爱用 IDE(集成开发环境)来写代码, Go 源码用的 Goland,有时分直接点击 IDE 菜单栏里的“运转”按钮,程序就跑起来了。这实际上隐含了编译和链接的进程,咱们通常将编译和链接合并到一起的进程称为构建(Build)。

编译进程便是对源文件进行词法剖析、语法剖析、语义剖析、优化,终究生成汇编代码文件,以 .s 作为文件后缀。

之后,汇编器会将汇编代码改变成机器能够履行的指令。由于每一条汇编句子几乎都与一条机器指令相对应,所以只是一个简略的一一对应,比较简略,没有语法、语义剖析,也没有优化这些步骤。 编译器是将高档言语翻译成机器言语的一个东西,编译进程一般分为 6 步:扫描、语法剖析、语义剖析、源代码优化、代码生成、方针代码优化。下图来自《程序员的自我涵养》:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

词法剖析

经过前面的比如,咱们知道,Go 程序文件在机器看来不过是一堆二进制位。咱们能读懂,是因为 Goland 按照 ASCII 码(实际上是 UTF-8)把这堆二进制位进行了编码。例如,把 8个 bit 位分红一组,对应一个字符,经过对照 ASCII 码表就能够查出来。 当把一切的二进制位都对应成了 ASCII 码字符后,咱们就能看到有含义的字符串。它或许是关键字,例如:package;或许是字符串,例如:“Hello World”。

词法剖析其实干的便是这个。输入是原始的 Go 程序文件,在词法剖析器看来,便是一堆二进制位,底子不知道是什么东西,经过它的剖析后,变成有含义的记号。简略来说,词法剖析是核算机科学中将字符序列转化为符号(token)序列的进程。

咱们来看一下维基百科上给出的界说:

词法剖析(lexical analysis)是核算机科学中将字符序列转化为符号(token)序列的进程。进行词法剖析的程序或许函数叫作词法剖析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。词法剖析器一般以函数的方式存在,供语法剖析器调用。

.go 文件被输入到扫描器(Scanner),它运用一种类似于 有限状态机算法,将源代码的字符系列分割成一系列的记号(Token)。 记号一般分为这几类:关键字、标识符、字面量(包括数字、字符串)、特殊符号(如加号、等号)。 例如,关于如下的代码:

slice[i] = i * (2 + 6)

一共包括 16 个非空字符,经过扫描后

记号 类型
slice 标识符
[ 左方括号
i 标识符
] 右方括号
= 赋值
i 标识符
* 乘号
( 左圆括号
2 数字
+ 加号
6 数字
) 右圆括号

Go 言语(本文的 Go 版本是 1.9.2)扫描器支撑的 Token 在源码中的途径:

src/cmd/compile/internal/syntax/token.go

感受一下:

var tokstrings = [...]string{
	// source control
	_EOF: "EOF",  
	// names and literals
	_Name:    "name",    
	_Literal:"literal", 
	// operators and operations
	_Operator:"op",    
	_AssignOp:"op=",    
	_IncOp:    "opop",    
	_Assign:   "=",    
	_Define:   ":=",    
	_Arrow:    "<-",    
	_Star:     "*",  
	// delimitors
	_Lparen:    "(",    
	_Lbrack:    "[",    
	_Lbrace:    "{",    
	_Rparen:    ")",    
	_Rbrack:    "]",    
	_Rbrace:    "}",    
	_Comma:     ",",    
	_Semi:      ";",    
	_Colon:     ":",    
	_Dot:       ".",    
	_DotDotDot:"...",  
	// keywords 
	_Break:       "break",    
	_Case:        "case",    
	_Chan:        "chan",    
	_Const:       "const",    
	_Continue:    "continue",    
	_Default:     "default",    
	_Defer:       "defer",    
	_Else:        "else",    
	_Fallthrough:"fallthrough",    
	_For:         "for",    
	_Func:        "func",    
	_Go:          "go",    
	_Goto:        "goto",    
	_If:          "if",    
	_Import:      "import",    
	_Interface:   "interface",    
	_Map:         "map",    
	_Package:     "package",    
	_Range:       "range",    
	_Return:      "return",    
	_Select:      "select",    
	_Struct:      "struct",    
	_Switch:      "switch",    
	_Type:        "type",    
	_Var:         "var",}

仍是比较了解的,包括称号和字面量、操作符、分隔符和关键字。 而扫描器的途径是:

src/cmd/compile/internal/syntax/scanner.go

其间最关键的函数便是 next 函数,它不断地读取下一个字符(不是下一个字节,因为 Go 言语支撑 Unicode 编码,并不是像咱们前面举得 ASCII 码的比如,一个字符只有一个字节),直到这些字符能够构成一个 Token。

func (s *scanner) next() {
// ...
    redo:
    // skip white space
    c := s.getr()
    for c == ' ' || c == '\t' || c=='\n' && !nlsemi || c == '\r' {
        c = s.getr()
    }
    //token start
    s.line, s.col == s.source.line0, s.source.col0
    if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) {
        s.ident()
        return 
    }
    switch c {
        //...
        case '\n':
        s.lit = "newline"
        s.tok= _Semi
        case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
        s.number(c)
        //..
    default:
        s.tok = 0
        s.error(fmt.Sprintf("invalid charscter %#U", c))
        goto redo
     return 
        assignop:
        if c == '=' {
            s.tok = _AssignOp
            return
        }
        s.ungetr()
        s.tok = _Operator
}

代码的首要逻辑便是经过 c:=s.getr() 获取下一个未被解析的字符,而且会跳过之后的空格、回车、换行、tab 字符,然后进入一个大的 switch-case 句子,匹配各种不同的情形,终究能够解析出一个 Token,而且把相关的行、列数字记录下来,这样就完结一次解析进程。 当前包中的词法剖析器 scanner 也只是为上层提供了 next 办法,词法解析的进程都是慵懒的,只有在上层的解析器需求时才会调用 next 获取最新的 Token。

语法剖析

上一步生成的 Token 序列,需求经过进一步处理,生成一棵以 表达式为结点的 语法树

比如最开端的那个比如, slice[i]=i*(2+6),得到的一棵语法树如下:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

整个句子被看作是一个赋值表达式,左子树是一个数组表达式,右子树是一个乘法表达式;数组表达式由 2 个符号表达式组成;乘号表达式则是由一个符号表达式和一个加号表达式组成;加号表达式则是由两个数字组成。符号和数字是最小的表达式,它们不能再被分解,通常作为树的叶子节点。 语法剖析的进程能够检测一些方式上的过错,例如:括号是否缺少一半, + 号表达式缺少一个操作数等。

语法剖析是依据某种特定的方式文法(Grammar)对 Token 序列构成的输入文本进行剖析并确定其语法结构的一种进程。

语义剖析

语法剖析完结后,咱们并不知道句子的详细含义是什么。像上面的 * 号的两棵子树假如是两个指针,这是不合法的,但语法剖析检测不出来,语义剖析便是干这个事。

编译期所能查看的是静态语义,能够以为这是在“代码”阶段,包括变量类型的匹配、转化等。例如,将一个浮点值赋给一个指针变量的时分,显着的类型不匹配,就会报编译过错。而关于运转期间才会呈现的过错:不小心除了一个 0 ,语义剖析是没办法检测的。

语义剖析阶段完结之后,会在每个节点上标示上类型:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

Go 言语编译器在这一阶段查看常量、类型、函数声明以及变量赋值句子的类型,然后查看哈希中键的类型。实现类型查看的函数通常都是几千行的巨型 switch/case 句子。

类型查看是 Go 言语编译的第二个阶段,在词法和语法剖析之后咱们得到了每个文件对应的笼统语法树,随后的类型查看会遍历笼统语法树中的节点,对每个节点的类型进行检验,找出其间存在的语法过错。

在这个进程中也或许会对笼统语法树进行改写,这不仅能够去除一些不会被履行的代码对编译进行优化进步履行效率,而且也会修改 make、new 等关键字对应节点的操作类型。

例如比较常用的 make 关键字,用它能够创建各种类型,如 slice,map,channel 等等。到这一步的时分,关于 make 关键字,也便是 OMAKE 节点,会先查看它的参数类型,依据类型的不同,进入相应的分支。假如参数类型是 slice,就会进入 TSLICE case 分支,查看 len 和 cap 是否满意要求,如 len <= cap。终究节点类型会从 OMAKE 改成 OMAKESLICE。

中心代码生成

咱们知道,编译进程一般能够分为前端和后端,前端生成和渠道无关的中心代码,后端会针对不同的渠道,生成不同的机器码。

前面词法剖析、语法剖析、语义剖析等都归于编译器前端,之后的阶段归于编译器后端。

编译进程有许多优化的环节,在这个环节是指源代码级别的优化。它将语法树转化成中心代码,它是语法树的次序表明。

中心代码一般和方针机器以及运转时环境无关,它有几种常见的方式:三地址码、P-代码。例如,最基本的 三地址码是这样的:

x = y op z

表明变量 y 和 变量 z 进行 op 操作后,赋值给 x。op 能够是数学运算,例如加减乘除。

前面咱们举的比如能够写成如下的方式:

t1 = 2 + 6
t2 = i * t1
slice[i] = t2

这儿 2 + 6 是能够直接核算出来的,这样就把 t1 这个暂时变量“优化”掉了,而且 t1 变量能够重复运用,因而 t2 也能够“优化”掉。优化之后:

t1 = i * 8
slice[i] = t1

Go 言语的中心代码表明方式为 SSA(Static Single-Assignment,静态单赋值),之所以称之为单赋值,是因为每个姓名在 SSA 中仅被赋值一次。。

这一阶段会依据 CPU 的架构设置相应的用于生成中心代码的变量,例如编译器运用的指针和寄存器的巨细、可用寄存器列表等。中心代码生成和机器码生成这两部分会同享相同的设置。

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

例如关于 map 的操作 m[i],在这儿会被转化成 mapacess 或 mapassign。

Go 言语的主程序在履行时会调用 runtime 中的函数,也便是说关键字和内置函数的功用其实是由言语的编译器和运转时共同完结的。

中心代码的生成进程其实便是从 AST 笼统语法树到 SSA 中心代码的转化进程,在这期间会对语法树中的关键字在进行一次更新,更新后的语法树会经过多轮处理改变终究的 SSA 中心代码。

方针代码生成与优化

不同机器的机器字长、寄存器等等都不相同,意味着在不同机器上跑的机器码是不相同的。终究一步的目的便是要生成能在不同 CPU 架构上运转的代码。

为了榨干机器的每一滴油水,方针代码优化器会对一些指令进行优化,例如运用移位指令代替乘法指令等。

这块实在没才能深化,幸好也不需求深化。关于应用层的软件开发工程师来说,了解一下就能够了。

链接进程

编译进程是针对单个文件进行的,文件与文件之间不可避免地要引证界说在其他模块的全局变量或许函数,这些变量或函数的地址只有在此阶段才能确定。

链接进程便是要把编译器生成的一个个方针文件链接成可履行文件。终究得到的文件是分红各种段的,比如数据段、代码段、BSS段等等,运转时会被装载到内存中。各个段具有不同的读写、履行属性,维护了程序的安全运转。

程序发动

仍然运用 hello-world 项目的比如。在项目根目录下履行:

go build -gcflags "-N -l"  -o hello src/main.go

-gcflags"-N -l" 是为了关闭编译器优化和函数内联,避免后面在设置断点的时分找不到相对应的代码位置。

得到了可履行文件 hello,履行:

[qcrao@qcrao hello-world]$ gdb hello

进入 gdb 调试形式,履行 info files,得到可履行文件的文件头,列出了各种段:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

一起,咱们也得到了进口地址:0x450e20。

(gdb) b *0x450e20
Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.

这便是 Go 程序的进口地址,我是在 linux 上运转的,所以进口文件为 src/runtime/rt0_linux_amd64.s,runtime 目录下有各种不同称号的程序进口文件,支撑各种操作体系和架构,代码为:

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    LEAQ    8(SP), SI // argv
    MOVQ   0(SP), DI // argc
    MOVQ    $main(SB), AX
    JMP AX

首要是把 argc,argv 从内存拉到了寄存器。这儿 LEAQ 是核算内存地址,然后把内存地址自身放进寄存器里,也便是把 argv 的地址放到了 SI 寄存器中。终究跳转到:

TEXT main(SB),NOSPLIT,$-8
    MOVQ    $runtimert0_go(SB), AX
    JMP AX

继续跳转到 runtimert0_go(SB),位置:/usr/local/go/src/runtime/asm_amd64.s,代码:

TEXT runtimert0_go(SB),NOSPLIT,$0
// 省略许多 CPU 相关的特性标志位查看的代码
// 首要是看不懂,^_^
// ………………………………
// 下面是终究调用的一些函数,比较重要
// 初始化履行文件的绝对途径
    CALL    runtimeargs(SB)
// 初始化 CPU 个数和内存页巨细
    CALL    runtimeosinit(SB)
// 初始化指令行参数、环境变量、gc、栈空间、内存办理、一切 P 实例、HASH算法等
    CALL    runtimeschedinit(SB)
// 要在 main goroutine 上运转的函数
    MOVQ    $runtimemainPC(SB), AX        
// entry
    PUSHQ   AX
    PUSHQ   $0         
// arg size
// 新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度
    CALL    runtimenewproc(SB)
    POPQ    AX
    POPQ    AX
// 发动M,开端调度goroutine
    CALL    runtimemstart(SB)
    MOVL    $0xf1, 0xf1
// crash
    RET
DATA    runtimemainPC+0(SB)/8,$runtimemain(SB)
GLOBL    runtimemainPC(SB),RODATA,$8

总结下:

  1. 查看运转渠道的CPU,设置好程序运转需求相关标志。

  2. TLS的初始化。

  3. runtime.args、runtime.osinit、runtime.schedinit 三个办法做好程序运转需求的各种变量与调度器。

  4. runtime.newproc创建新的goroutine用于绑定用户写的main办法。

  5. runtime.mstart开端goroutine的调度。

终究用一张图来总结 go bootstrap 进程吧:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

main 函数里履行的一些重要的操作包括:新建一个线程履行 sysmon 函数,定期垃圾收回和调度抢占;发动 gc;履行一切的 init 函数等等。

上面是发动进程,看一下退出进程:

当 main 函数履行结束之后,会履行 exit(0) 来退出进程。若履行 exit(0) 后,进程没有退出,main 函数终究的代码会一向拜访不合法地址:

exit(0)
for { 
	var x *int32
    *x = 0
}

正常情况下,一旦呈现不合法地址拜访,体系会把进程杀死,用这样的办法保证进程退出。

当然 Go 程序发动这一部分其实还会触及到 fork 一个新进程、装载可履行文件,控制权搬运等问题。仍是引荐看前面的两本书,我觉得我不会写得更好,就不叙说了。

GoRoot 和 GoPath

GoRoot 是 Go 的装置途径。mac 或 unix 是在 /usr/local/go 途径上,来看下这儿都装了些什么:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

bin 目录下面:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

pkg 目录下面:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

Go 东西目录如下,其间比较重要的有编译器 compile,链接器 link

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

GoPath 的效果在于提供一个能够寻找 .go 源码的途径,它是一个作业空间的概念,能够设置多个目录。Go 官方要求,GoPath 下面需求包括三个文件夹:

  1. src

  2. pkg

  3. bin

src 存放源文件,pkg 存放源文件编译后的库文件,后缀为 .a;bin 则存放可履行文件。

Go 指令详解

直接在终端履行:

  1. go

就能得到和 go 相关的指令简介:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

和编译相关的指令首要是:

  1. go build

  2. go install

  3. go run

go build

go build 用来编译指定 packages 里的源码文件以及它们的依靠包,编译的时分会到 $GoPath/src/package 途径下寻找源码文件。go build 还能够直接编译指定的源码文件,而且能够一起指定多个。

经过履行 go help build 指令得到 go build 的运用办法:

  1. usage: go build [-o output] [-i] [build flags] [packages]

-o 只能在编译单个包的时分呈现,它指定输出的可履行文件的姓名。

-i 会装置编译方针所依靠的包,装置是指生成与代码包相对应的 .a 文件,即静态库文件(后面要参与链接),而且放置到当前作业区的 pkg 目录下,且库文件的目录层级和源码层级共同。

至于 build flags 参数, build,clean,get,install,list,run,test 这些指令会共用一套:

参数 效果
-a 强制从头编译一切触及到的包,包括标准库中的代码包,这会重写 /usr/local/go 目录下的 .a 文件
-n 打印指令履行进程,不真正履行
-p n 指定编译进程中指令履行的并行数,n 默以为 CPU 核数
-race 检测并陈述程序中的数据竞争问题
-v 打印指令履行进程中所触及到的代码包称号
-x 打印指令履行进程中所触及到的指令,并履行
-work 打印编译进程中的暂时文件夹。通常情况下,编译完结后会被删除

咱们知道,Go 言语的源码文件分为三类:指令源码、库源码、测验源码。

指令源码文件:是 Go 程序的进口,包括 func main() 函数,且榜首行用 packagemain 声明归于 main 包。

库源码文件:首要是各种函数、接口等,例如东西类的函数。

测验源码文件:以 _test.go 为后缀的文件,用于测验程序的功用和功能。

注意, go build 会疏忽 *_test.go 文件。

咱们经过一个很简略的比如来演示 go build 指令。我用 Goland 新建了一个 hello-world项目(为了展现引证自界说的包,和之前的 hello-world 程序不同),项目的结构如下:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

最左边能够看到项目的结构,包括三个文件夹: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 目录下,直接履行 go build 指令,在同级目录生成了一个可履行文件,文件名为 src,运用 ./src 指令直接履行,输出:

  1. hello world!
  2. Local IP: 192.168.1.3

咱们也能够指定生成的可履行文件的称号:

  1. go build -o bin/hello

这样,在 bin 目录下会生成一个可履行文件,运转成果和上面的 src 相同。

其实,util 包能够独自被编译。咱们能够在项目根目录下履行:

  1. go build util

编译程序会去 $GoPath/src 途径找 util 包(其实是找文件夹)。还能够在 ./src/util 目录下直接履行 go build 编译。

当然,直接编译库源码文件不会生成 .a 文件,因为:

go build 指令在编译只包括库源码文件的代码包(或许一起编译多个代码包)时,只会做查看性的编译,而不会输出任何成果文件。

为了展现整个编译链接的运转进程,咱们在项目根目录履行如下的指令:

  1. go build -v -x -work -o bin/hello src/main.go

-v 会打印所编译过的包姓名, -x 打印编译期间所履行的指令, -work 打印编译期间生成的暂时文件途径,而且编译完结之后不会被删除。

履行成果:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

从成果来看,图中用箭头标示了本次编译进程触及 2 个包:util,command-line-arguments。第二个包比较怪异,源码里底子就没有这个姓名好吗?其实这是 go build 指令检测到 [packages] 处填的是一个 .go 文件,因而创建了一个虚拟的包:command-line-arguments。

一起,用红框圈出了 compile, link,也便是先编译了 util 包和 main.go 文件,别离得到 .a文件,之后将两者进行链接,终究生成可履行文件,而且移动到 bin 目录下,改名为 hello。

另外,榜首行显示了编译进程中的作业目录,此目录的文件结构是:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

能够看到,和 hello-world 目录的层级基本共同。command-line-arguments 便是虚拟的 main.go 文件所处的包。exe 目录下的可履行文件在终究一步被移动到了 bin 目录下,所以这儿是空的。

整体来看, go build 在履行时,会先递归寻找 main.go 所依靠的包,以及依靠的依靠,直至最底层的包。这儿能够是深度优先遍历也能够是宽度优先遍历。假如发现有循环依靠,就会直接退出,这也是经常会产生的循环引证编译过错。

正常情况下,这些依靠关系会构成一棵倒着生长的树,树根在最上面,便是 main.go 文件,最下面是没有任何其他依靠的包。编译器会从最左的节点所代表的包开端挨个编译,完结之后,再去编译上一层的包。

这儿,引证郝林教师几年前在 github 上宣布的 go 指令教程,能够从参考资料找到原文地址。

从代码包编译的视点来说,假如代码包 A 依靠代码包 B,则称代码包 B 是代码包 A 的依靠代码包(以下简称依靠包),代码包 A 是代码包 B 的触发代码包(以下简称触发包)。

履行 go build 指令的核算机假如拥有多个逻辑 CPU 中心,那么编译代码包的次序或许会存在一些不确定性。可是,它一定会满意这样的约束条件:依靠代码包 -> 当前代码包 -> 触发代码包。

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

到这儿,你一定会发现,关于 hello-wrold 文件夹下的 pkg 目录好像一向没有触及到。

其实,pkg 目录下面应该存放的是触及到的库文件编译后的包,也便是一些 .a 文件。可是 go build 履行进程中,这些 .a 文件放在暂时文件夹中,编译完结后会被直接删掉,因而一般不会用到。

前面咱们提到过,在 go build 指令里加上 -i 参数会装置这些库文件编译的包,也便是这些 .a 文件会放到 pkg 目录下。

在项目根目录履行 go build-i src/main.go 后,pkg 目录里增加了 util.a 文件:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

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-Questions hello-world项目】。

go install

go install 用于编译并装置指定的代码包及它们的依靠包。相比 go build,它只是多了一个“装置编译后的成果文件到指定目录”的步骤。

仍是运用之前 hello-world 项目的比如,咱们先将 pkg 目录删掉,在项目根目录履行:

  1. go install src/main.go

  2. 或许

  3. go install util

两者都会在根目录下新建一个 pkg 目录,而且生成一个 util.a 文件。

而且,在履行前者的时分,会在 GOBIN 目录下生成名为 main 的可履行文件。

所以,运转 go install 指令,库源码包对应的 .a 文件会被放置到 pkg 目录下,指令源码包生成的可履行文件会被放到 GOBIN 目录。

go install 在 GoPath 有多个目录的时分,会产生一些问题,详细能够去看郝林教师的 Go指令教程,这儿不展开了。

go run

go run 用于编译并运转指令源码文件。

在 hello-world 项目的根目录,履行 go run 指令:

  1. go run -x -work src/main.go

-x 能够打印整个进程触及到的指令,-work 能够看到暂时的作业目录:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

从上图中能够看到,仍然是先编译,再衔接,终究直接履行,并打印出了履行成果。

榜首行打印的便是作业目录,终究生成的可履行文件便是放置于此:

从编译原理到 Go 启动时的流程,Go程序是怎样跑起来的

main 便是终究生成的可履行文件。

总结

这次的论题太大了,困难重重。从编译原理到 go 发动时的流程,到 go 指令原理,每个论题独自抽出来都能够写许多。

幸好有一些很不错的书和博客文章能够去参考。这篇文章就作为一个引子,你能够跟随参考资料里引荐的一些内容去发散。