Go 程序是怎样跑起来的?你知道吗?

【Golang系列】Golang环境配置和第一个Go程序 对于Go语言的安装包,有两种类型,一种是msi文件,这个文件直接双击打开,然后设置安装目录,一直next即可,还有一个zip等压缩文件格式的安装包,直接解压到你想安装的目录即可。 安装好go语言包后,在命令行窗口运行go version,如果出现具体的版本就表示安装好了,为了配合VsCode等IDE环境,还需要配置环境变量。 阅读详情

一、引入

我们从一个 Hello World 的例子开始:

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

当我用我那价值 1800 元的 cherry 键盘潇洒地敲完上面的 hello world 代码时,保存在硬盘上的 hello.go 文件就是一个字节序列了,每个字节代表一个字符。

用 vim 打开 hello.go 文件,在命令行模式下,输入命令:

:%!xxd 

就能在 vim 里以十六进制查看文件内容:

 

最左边的一列代表地址值,中间一列代表文本对应的 ASCII 字符,最右边的列就是我们的代码。再在终端里执行 man ascii:

 

和 ASCII 字符表一对比,就能发现,中间的列和最右边的列是一一对应的。也就是说,刚刚写完的 hello.go 文件都是由 ASCII 字符表示的,它被称为文本文件,其他文件被称为二进制文件。

当然,更深入地看,计算机中的所有数据,像磁盘文件、网络中的数据其实都是一串比特位组成,取决于如何看待它。在不同的情景下,一个相同的字节序列可能表示成一个整数、浮点数、字符串或者是机器指令。

而像 hello.go 这个文件,8 个 bit,也就是一个字节看成一个单位(假定源程序的字符都是 ASCII 码),最终解释成人类能读懂的 Go 源码。

Go 程序并不能直接运行,每条 Go 语句必须转化为一系列的低级机器语言指令,将这些指令打包到一起,并以二进制磁盘文件的形式存储起来,也就是可执行目标文件。

从源文件到可执行目标文件的转化过程:

 

完成以上各个阶段的就是 Go 编译系统。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名为 GNU 编译器套装,它支持像 C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能够为很多不同的机器生成机器码。

可执行目标文件可以直接在机器上执行。一般而言,先执行一些初始化的工作;找到 main 函数的入口,执行用户写的代码;执行完成后,main 函数退出;再执行一些收尾的工作,整个过程完毕。

二、编译链接概述

Go 源码里的编译器源码位于 src/cmd/compile 路径下,链接器源码位于 src/cmd/link 路径下。

1、编译过程

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

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

之后,汇编器会将汇编代码转变成机器可以执行的指令。由于每一条汇编语句几乎都与一条机器指令相对应,所以只是一个简单的一一对应,比较简单,没有语法、语义分析,也没有优化这些步骤。

编译器是将高级语言翻译成机器语言的一个工具,编译过程一般分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。下图来自《程序员的自我修养》:

 

1.1 词法分析

通过前面的例子,我们知道,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 character %#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。

1.2 语法分析

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

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

 

整个语句被看作是一个赋值表达式,左子树是一个数组表达式,右子树是一个乘法表达式;数组表达式由 2 个符号表达式组成;乘号表达式则是由一个符号表达式和一个加号表达式组成;加号表达式则是由两个数字组成。符号和数字是最小的表达式,它们不能再被分解,通常作为树的叶子节点。

语法分析的过程可以检测一些形式上的错误,例如:括号是否缺少一半,+ 号表达式缺少一个操作数等。

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

1.3 语义分析

语法分析完成后,我们并不知道语句的具体意义是什么。像上面的 * 号的两棵子树如果是两个指针,这是不合法的,但语法分析检测不出来,语义分析就是干这个事。

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

语义分析阶段完成之后,会在每个节点上标注上类型:

 

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

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

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

例如比较常用的 make 关键字,用它可以创建各种类型,如 slice,map,channel 等等。到这一步的时候,对于 make 关键字,也就是 OMAKE 节点,会先检查它的参数类型,根据类型的不同,进入相应的分支。如果参数类型是 slice,就会进入 TSLICE case 分支,检查 len 和 cap 是否满足要求,如 len <= cap。最后节点类型会从 OMAKE 改成 OMAKESLICE。

1.4 中间代码生成

我们知道,编译过程一般可以分为前端和后端,前端生成和平台无关的中间代码,后端会针对不同的平台,生成不同的机器码。

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

编译过程有很多优化的环节,在这个环节是指源代码级别的优化。它将语法树转换成中间代码,它是语法树的顺序表示。

中间代码一般和目标机器以及运行时环境无关,它有几种常见的形式:三地址码、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 的架构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器的大小、可用寄存器列表等。中间代码生成和机器码生成这两部分会共享相同的设置。

在生成中间代码之前,会对抽象语法树中节点的一些元素进行替换。这里引用王晶大佬《面向信仰编程》编译原理相关博客里的一张图:

 

例如对于 map 的操作 m[i],在这里会被转换成 mapacess 或 mapassign。

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

中间代码的生成过程其实就是从 AST 抽象语法树到 SSA 中间代码的转换过程,在这期间会对语法树中的关键字在进行一次更新,更新后的语法树会经过多轮处理转变最后的 SSA 中间代码。

1.5 目标代码生成与优化

不同机器的机器字长、寄存器等等都不一样,意味着在不同机器上跑的机器码是不一样的。最后一步的目的就是要生成能在不同 CPU 架构上运行的代码。

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

这块实在没能力深入,幸好也不需要深入。对于应用层的软件开发工程师来说,了解一下就可以了。

2、 链接过程

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

链接过程就是要把编译器生成的一个个目标文件链接成可执行文件。最终得到的文件是分成各种段的,比如数据段、代码段、BSS段等等,运行时会被装载到内存中。各个段具有不同的读写、执行属性,保护了程序的安全运行。

这部分内容,推荐看《程序员的自我修养》和《深入理解计算机系统》。

三、Go 程序启动

仍然使用 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,得到可执行文件的文件头,列出了各种段:

 

同时,我们也得到了入口地址: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 $runtime·rt0_go(SB), AX JMP AX 

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

TEXT runtime·rt0_go(SB),NOSPLIT,$0 // 省略很多 CPU 相关的特性标志位检查的代码 // 主要是看不懂,^_^ // ……………………………… // 下面是最后调用的一些函数,比较重要 // 初始化执行文件的绝对路径 CALL runtime·args(SB) // 初始化 CPU 个数和内存页大小 CALL runtime·osinit(SB) // 初始化命令行参数、环境变量、gc、栈空间、内存管理、所有 P 实例、HASH算法等 CALL runtime·schedinit(SB) // 要在 main goroutine 上运行的函数 MOVQ $runtime·mainPC(SB), AX // entry PUSHQ AX PUSHQ $0 // arg size // 新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度 CALL runtime·newproc(SB) POPQ AX POPQ AX // 启动M,开始调度goroutine CALL runtime·mstart(SB) MOVL $0xf1, 0xf1 // crash RET DATA runtime·mainPC+0(SB)/8,$runtime·main(SB) GLOBL runtime·mainPC(SB),RODATA,$8 

文章【探索 golang 程序启动过程】研究得比较深入,总结下:

  • 检查运行平台的CPU,设置好程序运行需要相关标志。
  1. TLS的初始化。
  2. runtime.args、runtime.osinit、runtime.schedinit 三个方法做好程序运行需要的各种变量与调度器。
  3. runtime.newproc创建新的goroutine用于绑定用户写的main方法。
  4. runtime.mstart开始goroutine的调度。

最后用一张图来总结 go bootstrap 过程吧:

 

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

上面是启动过程,看一下退出过程:

当 main 函数执行结束之后,会执行 exit(0) 来退出进程。若执行 exit(0) 后,进程没有退出,main 函数最后的代码会一直访问非法地址:

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

正常情况下,一旦出现非法地址访问,系统会把进程杀死,用这样的方法确保进程退出。

关于程序退出这一段的阐述来自群聊《golang runtime 阅读》,又是一个高阶的读源码的组织,github 主页见参考资料。

当然 Go 程序启动这一部分其实还会涉及到 fork 一个新进程、装载可执行文件,控制权转移等问题。还是推荐看前面的两本书,我觉得我不会写得更好,就不叙述了。

四、GoRoot 和 GoPath

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

 

bin 目录下面:

 

pkg 目录下面:

 

Go 工具目录如下,其中比较重要的有编译器 compile,链接器 link:

 

GoPath 的作用在于提供一个可以寻找 .go 源码的路径,它是一个工作空间的概念,可以设置多个目录。Go 官方要求,GoPath 下面需要包含三个文件夹:

src pkg bin  

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

五、Go 命令详解

直接在终端执行:

go 

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

 

和编译相关的命令主要是:

1、go build 
2、go install 
3、go run 

1、go build

go build 用来编译指定 packages 里的源码文件以及它们的依赖包,编译的时候会到 $GoPath/src/package 路径下寻找源码文件。go build 还可以直接编译指定的源码文件,并且可以同时指定多个。

通过执行 go help build 命令得到 go build 的使用方法:

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() 函数,且第一行用 package main 声明属于 main 包。

库源码文件:主要是各种函数、接口等,例如工具类的函数。

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

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

我们通过一个很简单的例子来演示 go build 命令。我用 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 目录下,直接执行 go build 命令,在同级目录生成了一个可执行文件,文件名为 src,使用 ./src 命令直接执行,输出:

hello world! Local IP: 192.168.1.3 

我们也可以指定生成的可执行文件的名称:

go build -o bin/hello  

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

其实,util 包可以单独被编译。我们可以在项目根目录下执行:

go build util 

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

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

go build 命令在编译只包含库源码文件的代码包(或者同时编译多个代码包)时,只会做检查性的编译,而不会输出任何结果文件。

为了展示整个编译链接的运行过程,我们在项目根目录执行如下的命令:

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

-v 会打印所编译过的包名字,-x 打印编译期间所执行的命令,-work 打印编译期间生成的临时文件路径,并且编译完成之后不会被删除。

执行结果:

 

从结果来看,图中用箭头标注了本次编译过程涉及 2 个包:util,command-line-arguments。第二个包比较诡异,源码里根本就没有这个名字好吗?其实这是 go build 命令检测到 [packages] 处填的是一个 .go 文件,因此创建了一个虚拟的包:command-line-arguments。

同时,用红框圈出了 compile, link,也就是先编译了 util 包和 main.go 文件,分别得到 .a 文件,之后将两者进行链接,最终生成可执行文件,并且移动到 bin 目录下,改名为 hello。

另外,第一行显示了编译过程中的工作目录,此目录的文件结构是:

 

可以看到,和 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 核心,那么编译代码包的顺序可能会存在一些不确定性。但是,它一定会满足这样的约束条件:依赖代码包 -> 当前代码包 -> 触发代码包。

顺便推荐一个浏览器插件 Octotree,在看 github 项目的时候,此插件可以在浏览器里直接展示整个项目的文件结构,非常方便:

 

到这里,你一定会发现,对于 hello-wrold 文件夹下的 pkg 目录好像一直没有涉及到。

其实,pkg 目录下面应该存放的是涉及到的库文件编译后的包,也就是一些 .a 文件。但是 go build 执行过程中,这些 .a 文件放在临时文件夹中,编译完成后会被直接删掉,因此一般不会用到。

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

在项目根目录执行 go build -i src/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-Questions hello-world项目】。

2、go install

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

还是使用之前 hello-world 项目的例子,我们先将 pkg 目录删掉,在项目根目录执行:

go install src/main.go 或者 go install util 

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

并且,在执行前者的时候,会在 GOBIN 目录下生成名为 main 的可执行文件。

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

go install 在 GoPath 有多个目录的时候,会产生一些问题,具体可以去看郝林老师的 Go 命令教程,这里不展开了。

3、go run

go run 用于编译并运行命令源码文件。

在 hello-world 项目的根目录,执行 go run 命令:

go run -x -work src/main.go  

-x 可以打印整个过程涉及到的命令,-work 可以看到临时的工作目录:

 

从上图中可以看到,仍然是先编译,再连接,最后直接执行,并打印出了执行结果。

第一行打印的就是工作目录,最终生成的可执行文件就是放置于此:

 

main 就是最终生成的可执行文件。

 

使用go pprof进行golang程序内存分析 use pprof to fix oom 阅读详情

相关推荐

你不知道的 Golang 打包配置文件

众所周知,Golang 适合写 CLI 工具,但你可能还不知道 Golang 还可以打包配置文件。 背景 最近在写一个涉及到管理阿里云 ECS 的 CLI 工具,这里当然就要考虑阿里云资源使用的安全性了,要求阿里云账号的 AccessKeyId 和 AccessKeySecret 不能下发给 CLI 工具的使用者。 所以这里选择将一份包含 AccessKeyId 和 AccessKeySecret 的配置文件打包进了 CLI 工具中,CLI 工具的使用者默认将使用已经打包了的配置文件,当然也可以通过指定

K8sCat 4288

Go 程序是怎样跑起来的

这次的话题太大了,困难重重。从编译原理到 go 启动时的流程,到 go 命令原理,每个话题单独抽出来都可以写很多。幸好有一些很不错的书和博客文章可以去参考。这篇文章就作为一个引子,你可以跟随参考资料里推荐的一些内容去发散。

wang11876的博客 741

Go 程序是怎样跑起来的?

这次的话题太大了,困难重重。从编译原理到 go 启动时的流程,到 go 命令原理,每个话题单独抽出来都可以写很多。幸好有一些很不错的书和博客文章可以去参考。这篇文章就作为一个引子,你可以跟随参考资料里推荐的一些内容去发散。

yonggeit的博客 3355

Go语言的运行机制&程序是怎么跑起来的

学习Golang有一段时间了,自己看着各种教程也码了些demo。其实接触了这么多语言,当因为工作、项目、兴趣所驱在短时间切换一门编程语言时,并不会太难上手,甚至会对了解一些很雷同的基础语法感到枯燥,但这是必经之路。对于一个技术爱好者而言,技术广度、技术深度、技术新特性等往往是最好的兴奋剂。今天这篇文章主要结合最近的资料学习,对Go语言的运行机制及Go程序的运作进行一些稍微深入的分析及总结,对Go的启动和执行流程建立简单的宏观认知~ 为什么Go语言适合现代的后端编程环境? 服务类应用以API居多,IO密.

AlexGeek 2944

华三R4900 G5服务器实战:用PMC P460-B4阵列卡配置RAID1+RAID6,并搞定CentOS 7.9安装(含热备盘设置)

本文详细解析了在华三R4900 G5服务器上使用PMC P460-B4阵列卡配置RAID1+RAID6混合阵列的全过程,包括热备盘设置和CentOS 7.9系统安装。通过实战步骤和性能优化建议,帮助企业IT人员高效部署高可用存储方案,特别适合需要数据冗余和系统稳定性的企业级应用场景。

weixin_29248653的博客 358

使用基于python的neurora进行表征相似性分析(RSA)

发现了一款用于表征分析的Python API,还是挺方便的,这里简单介绍一下进行表征相似性分析的使用方法。

zjh12312311的博客 2767

【Golang入门教程】如何使用Goland创建并运行项目

通过本文的指南,您应该已经成功地创建了一个简单的Go语言项目并运行了它。Goland作为一款强大的IDE,不仅仅提供了方便的项目管理工具,还支持调试、代码导航、自动补全等功能,这些都能够显著提高开发效率。随着您深入了解Goland的各种功能和选项,将能够更好地利用这个工具进行高效的Go语言开发。😊🙏如果大家对相关文章感兴趣,可以关注公众号"架构殿堂",会持续更新AIGC,java基础面试题, netty, spring boot, spring cloud等系列文章,一系列干货随时送达!

Venus's Blog 2万+

PCIe5.0均衡简介(整理版)

pcie5.0 均衡 equalization

m0_53559277的博客 9110

go 二进制程序守护_图解 Go 程序是怎样跑起来的

Go语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!刚开始写这篇文章的时候,目标非常大,想要探索 Go 程序的一生:编码、编译、汇编、链接、运行、退出。它的每一步具体如何进行,力图弄清 Go 程序的这一生。在这个过程中,我又复习了一遍《程序员的自我修养》。这是一本讲编译、链接的书,非常详细,值得一看!数年前,我第一次看到这本书的书名,就非常喜欢。因为它模仿了周星驰喜剧之...

weixin_39702559的博客 227

WKWebView 白屏问题,WKWebView request body丢失问题

1,WKWebView 白屏问题 WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会...

weixin_37543822的博客 739

Linux 之 supervisor安装和使用

运行Supervisor的后台服务,它用来启动和管理那些你需要Supervisor管理的子进程,响应客户端发来的请求,重启意外退出的子进程,将子进程的stdout和stderr写入日志,响应事件等。它是Supervisor最核心的部分。

chang995196962的博客 5548

go 根据输入类型执行对应的方法_Go 程序是怎样跑起来的?你知道吗?

一、引入我们从一个 Hello World 的例子开始:package mainimport "fmt"func main() { fmt.Println("hello world") } 当我用我那价值 1800 元的 cherry 键盘潇洒地敲完上面的 hello world 代码时,保存在硬盘上的 hello.go 文件就是一个字节序列了,每个字节代表一个字符。用 vim 打开 hello...

weixin_39895977的博客 152

CVPR 2026 即插即用 | 注意力篇 | PFG:多尺度大核+像素级频率门控,特征显著增强,精准适配边缘纹理场景

本文提出了一种基于频率引导的轻量级外围门控模块PFG,用于高效时空预测学习。该模块通过多尺度大核分解卷积(1×K + K×1)捕捉外围空间依赖,同时利用固定频率滤波器提取高频线索,动态融合不同尺度的外围特征。核心创新包括:外围聚焦的多尺度空间增强、频率引导的动态融合机制、双阶段归一化协同等。实验表明,PFGNet在多个基准数据集上达到SOTA性能,同时保持较低计算复杂度。该模块可直接嵌入各类视觉模型,适用于需要强化空间-通道协同表达的轻量级任务。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 484

Go程序的一生——Go如何跑起来的?

Go 源码里的编译器源码位于路径下,链接器源码位于路径下。这次的话题太大了,困难重重。从编译原理到 go 启动时的流程,到 go 命令原理,每个话题单独抽出来都可以写很多。幸好有一些很不错的书和博客文章可以去参考。这篇文章就作为一个引子,你可以跟随参考资料里推荐的一些内容去发散。

Soul丶君的博客 1280

Claude Code 完全指南:从入门到精通

《AI编程助手ClaudeCode深度指南》摘要 ClaudeCode作为新一代AI编程助手,凭借200K tokens的超长上下文窗口和深度代码理解能力,正在重塑开发工作流程。本文系统介绍了从安装配置到高级应用的完整使用指南: 核心优势包括: 项目级代码理解能力 Agent自主规划复杂任务 原生终端集成支持 多文件协同编辑功能 实战应用场景: 快速理解陌生项目架构 诊断性能瓶颈并提供优化方案 自动生成符合规范的API代码 开发可复用技术组件 高级技巧: 通过CLAUDE.md定义项目规范 合理使用/cle

分享AI、区块链技术及应用,未来已来 605

计算机网络技术研究方向有哪些创新点

计算机网络技术方向的创新点众多,这些创新不仅推动了网络技术的不断发展和完善,还为用户提供了更高效、更安全、更便捷的网络服务。- 边缘计算是一种将计算和数据存储推向网络边缘的技术,通过将数据处理和分析的任务分散到网络的各个节点,降低了数据传输的延迟,提高了实时性。- 通过网络自动化工具和技术,可以实现网络配置的自动化、网络故障的自动诊断与修复等功能,降低了网络运维的复杂性和成本。- 通过引入人工智能技术,网络可以实现更智能的流量管理、故障预测和自动修复等功能,提高了网络的性能和稳定性。

Loving_enjoy的博客 1498

[GO语言基础] 一.为什么我要学习Golang以及GO语言入门普及

作为网络安全初学者,会遇到采用Go语言开发的恶意样本。因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识;另一方面是分享与读者,希望大家一起进步。这系列文章入门部分将参考“尚硅谷”韩顺平老师的视频和书籍《GO高级编程》,详见参考文献,并结合作者多年的编程经验进行学习和丰富,且看且珍惜吧!后续会结合网络安全进行GO语言实战深入,驾驭~ 这些年我学过各种编程语言,从最早的C语言到C++,再到C#、PHP、JAVA,再到IOS开发、Python,到最新的GO语言,学得是真的杂。有时

杨秀璋的专栏 12万+

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

GO开篇:手握Java走进Golang的世界

Go简介、Go和Java的对比

健身变秃,coding变强 1万+

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

golang学习之go程序docker部署

windows 安装wsl2 docker 及 go 程序 docker 部署

weixin_56349119的博客 2843

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

上一篇: 工程师必备,扒一扒Java并发!!!
下一篇: 这几道Redis面试题都不懂,怎么拿offer?
加班不秃顶
博客等级 码龄7年 1822粉丝 599原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值