o语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!
刚开始写这篇文章的时候,目标非常大,想要探索 Go 程序的一生:编码、编译、汇编、链接、运行、退出。它的每一步具体如何进行,力图弄清 Go 程序的这一生。
在这个过程中,我又复习了一遍《程序员的自我修养》。这是一本讲编译、链接的书,非常详细,值得一看!数年前,我第一次看到这本书的书名,就非常喜欢。因为它模仿了周星驰喜剧之王里出现的一本书 ——《演员的自我修养》。心向往之!
在开始本文之前,先推荐一下王晶大佬(以前在滴滴)的博客——《面向信仰编程》,他的 Go 编译系列文章,非常有深度,直接深入编译器源代码,我是看了很多遍了。博客链接可以从参考资料里获取。
理想很大,实现的难度也是非常大。为了避免砸了“深度解密”这个牌子,这次起了个更温和的名字,嘿嘿。
我们从一个 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 路径下。
我比较喜欢用 IDE(集成开发环境)来写代码, Go 源码用的 Goland,有时候直接点击 IDE 菜单栏里的“运行”按钮,程序就跑起来了。这实际上隐含了编译和链接的过程,我们通常将编译和链接合并到一起的过程称为构建(Build)。
编译过程就是对源文件进行词法分析、语法分析、语义分析、优化,最后生成汇编代码文件,以 .s 作为文件后缀。
之后,汇编器会将汇编代码转变成机器可以执行的指令。由于每一条汇编语句几乎都与一条机器指令相对应,所以只是一个简单的一一对应,比较简单,没有语法、语义分析,也没有优化这些步骤。
编译器是将高级语言翻译成机器语言的一个工具,编译过程一般分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。下图来自《程序员的自我修养》:
通过前面的例子,我们知道,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 个非空字符,经过扫描后:
上面的例子源自《程序员的自我修养》,主要讲解编译、链接相关的内容,很精彩,推荐研读。
Go 语言(本文的 Go 版本是 1.9.2)扫描器支持的 Token 在源码中的路径:
src/cmd/compile/internal/syntax/token.go
感受一下:
还是比较熟悉的,包括名称和字面量、操作符、分隔符和关键字。
而扫描器的路径是:
src/cmd/compile/internal/syntax/scanner.go
其中最关键的函数就是 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,就会进入 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 的架构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器的大小、可用寄存器列表等。中间代码生成和机器码生成这两部分会共享相同的设置。
在生成中间代码之前,会对抽象语法树中节点的一些元素进行替换。这里引用王晶大佬《面向信仰编程》编译原理相关博客里的一张图:
例如对于 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,得到可执行文件的文件头,列出了各种段:
同时,我们也得到了入口地址: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,代码:
参考文献里的一篇文章【探索 golang 程序启动过程】研究得比较深入,总结下:
最后用一张图来总结 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 是 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 build go install go run
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 这些命令会共用一套:
我们知道,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项目】。
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 命令教程,这里不展开了。
go run 用于编译并运行命令源码文件。
在 hello-world 项目的根目录,执行 go run 命令:
go run -x -work src/main.go
-x 可以打印整个过程涉及到的命令,-work 可以看到临时的工作目录:
从上图中可以看到,仍然是先编译,再连接,最后直接执行,并打印出了执行结果。
第一行打印的就是工作目录,最终生成的可执行文件就是放置于此:
main 就是最终生成的可执行文件。
这次的话题太大了,困难重重。从编译原理到 go 启动时的流程,到 go 命令原理,每个话题单独抽出来都可以写很多。
幸好有一些很不错的书和博客文章可以去参考。这篇文章就作为一个引子,你可以跟随参考资料里推荐的一些内容去发散。
【《程序员的自我修养》全书】https://book.douban.com/subject/3652388/
【面向信仰编程 编译过程概述】https://draveness.me/golang-compile-intro
【golang runtime 阅读】https://github.com/zboya/golang_runtime_reading
【Go-Questions hello-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
【Gopath Explained】https://flaviocopes.com/go-gopath/
【Understanding the GOPATH】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
【Go package 的探索】https://mp.weixin.qq.com/s/OizVLXfZ6EC1jI-NL7HqeA
【Go 官方 关于 Go 项目的组织结构】https://golang.org/doc/code.html
【Go modules】https://www.melvinvivas.com/go-version-1-11-modules/
【Golang Installation, Setup, GOPATH, and Go Workspace】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
【曹大 go bootstrap】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/
本文作者:饶全成,原创授权发布
o语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!
以常规方式编写并发程序,需要对共享变量作正确的访问控制,处理起来很困难。而golang提出一种不同的方式,即共享变量通过channel传递,共享变量从不被各个独立运行的线程(goroutine)同时享有,在任一时刻,共享变量仅可被一个goroutine访问。所以,不会产生数据竞争。并发编程,golang鼓励以此种方式进行思考,精简为一句口号——“勿通过共享内存来进行通信,而应通过通信来进行内存共享”。
Unbuffered channels的接收者阻塞直至收到消息,发送者阻塞直至接收者接收到消息,该机制可用于两个goroutine的状态同步。Buffered channels在缓冲区未满时,发送者仅在值拷贝到缓冲区之前是阻塞的,而在缓冲区已满时,发送者会阻塞,直至接收者取走了消息,缓冲区有了空余。
如下代码使用Unbuffered channel作同步控制。给定一个整型数组,在主routine启动另一个goroutine将该数组排序,当其完成时,给done channel发送完成消息,主routine会一直等待直至排序完成,打印结果。
如下代码中,messages chan的缓冲区大小为2,因其为Buffered channel,所以消息发送与接收无须分开到两个并发的goroutine中。
函数封装时,对仅作消息接收或仅作消息发送的chan标识direction可以借用编译器检查增强类型使用安全。如下代码中,ping函数中pings chan仅用来接收消息,所以参数列表中将其标识为接收者。pong函数中,pings chan仅用来发送消息,pongs chan仅用来接收消息,所以参数列表中二者分别标识为发送者与接收者。
使用select可以用来等待多个channel的消息,如下代码,创建两个chan,启动两个goroutine耗费不等时间计算结果,主routine监听消息,使用两次select,第一次接收到了ch2的消息,第二次接收到了ch1的消息,用时2.000521146s。
select with default可以用来处理非阻塞式消息发送、接收及多路选择。如下代码中,第一个select为非阻塞式消息接收,若收到消息,则落入<-messages case,否则落入default。第二个select为非阻塞式消息发送,与非阻塞式消息接收类似,因messages chan为Unbuffered channel且无异步消息接收者,因此落入default case。第三个select为多路非阻塞式消息接收。
当无需再给channel发送消息时,可将其close。如下代码中,创建一个Buffered channel,首先启动一个异步goroutine循环消费消息,然后主routine完成消息发送后关闭chan,消费goroutine检测到chan关闭后,退出循环。
2.5 for range
for range语法不仅可对基础数据结构(slice、map等)作迭代,还可对channel作消息接收迭代。如下代码中,给messages chan发送两条消息后将其关闭,然后迭代messages chan打印消息。
资源访问、网络请求等场景作超时控制是非常必要的,可以使用channel结合select来实现。如下代码,对常规sum函数增加超时限制,sumWithTimeout函数中,select的v :=<-rlt在等待计算结果,若在时限范围内计算完成,则正常返回计算结果,若超过时限则落入<-time.After(timeout) case,抛出timeout error。
本文代码托管地址:https://github.com/olzhy/go-excercises/tree/master/channels
参考资料
[1] https://golang.org/doc/effective_go.html#channels
[2] https://gobyexample.com/channel-synchronization
[3] https://gobyexample.com/channel-buffering
[4] https://gobyexample.com/channel-directions
[5] https://gobyexample.com/select
[6] https://gobyexample.com/non-blocking-channel-operations
[7] https://gobyexample.com/closing-channels
[8] https://gobyexample.com/range-over-channels
[9] https://gobyexample.com/timeouts
原文:https://leileiluoluo.com/posts/golang-channels.html
本文作者:磊磊落落的博客,原创授权发布
为一款网红编程语言,Go语言还十分年轻,很多程序员无法及时了解到Go语言的框架、库和软件应用。近日,Github用户avelino分享了一张非常完整且庞大的表单,包括命令行、数据库、Web框架、机器学习、自然语言处理......以下是部分内容截取,感谢avelino的分享。
标准CLI
用于构建标准或基本命令行应用程序的库。
argv - 使用bash语法将库命令行字符串拆分为参数数组。
cli - 基于golang的功能丰富且易于使用的命令行程序包。
cli-init - 开始构建Golang命令行应用程序的简单方法。
climax - 具有“human face”的替代CLI。
cobra - CLI交互指挥官。
complete - 在Go + Go命令bash完成中写入bash完成。
docopt.go - 命令行参数解析器。
drive - Google Drive客户端命令行。
env - 基于标签的结构环境配置。·
flag - 简单而强大的命令行选项解析库支持Go子命令。
go-arg - 在Go中基于结构的参数解析。
go-flags - go命令行选项解析器。
kingpin - 支持子命令的命令行和标志解析器。
liner - 用于命令行接口的类似于readline的库。
mitchellh/ cli - 用于实现命令行界面的库。
mow.cli - 用于构建具有复杂标志和参数解析验证的CLI应用程序库。
pflag - 替换Go的flag包,实现POSIX/GNU-style --flags。
readline - 纯Golang实现,在MIT许可下提供GNU-Readline中的大部分功能。
sflags - 基于结构的标志生成器,用于flag, urfave/cli, pflag, cobra, kingpin和其他库。
ukautz/ clif - 小型命令行界面框架。
urfave/ cli - 在Go(以前的codegangsta / cli)中构建命令行应用程序的简单,快速和有趣的包。
wlog - 支持跨平台颜色和并发性的简单日志记录界面。
wmenu - 易于使用的菜单结构,用于提示用户进行选择的cli应用程序。
高级控制台UI
用于构建控制台应用程序和控制台用户界面的库。
aurora - 支持fmt.Printf / Sprintf的ANSI终端颜色。
chalk - 直观的包装,用于优化终端/控制台输出。
color - 用于彩色终端输出的多功能包装。
colourize - 终端中ANSI文本颜色的Go库。
go-ataman - Go库,用于在终端中呈现ANSI彩色文本模板。
go-colorable - Windows的可着色画笔。
go-colortext - 用于在终端中输出颜色的库。
gocui - Minimalist —Go库旨在创建控制台用户界面。
gommon / color - Style终端文本。
mpb - 终端应用程序的多进度条。
termbox-go - Termbox是一个用于创建跨平台的、基于文本的界面的库。
termtables - 将Ruby库终端表的端口用于简单的ASCII表生成以及提供HTML输出。
termui - 终端仪表板,基于termbox-go,并受到blessed-contrib的启发。
uilive - 用于实时更新终端输出的库。
uiprogress - 灵活的库用于在终端应用程序中呈现进度条。
uitable - 使用表格数据提高终端应用程序的可读性。
数据结构
Go中的通用数据结构和算法。
binpacker - 二进制打包程序和解包程序可帮助用户构建自定义二进制流。
bit - Golang设置数据结构,带有加密的bit-twiddling功能。
bitset - Go包执行位组。
bloom - 在Go中实现的Bloom过滤器。
bloom - Golang Bloom过滤器实现。
boomfilters - 用于处理连续,无界流的概率数据结构。
concurrent-writer - bufio.Writer的高度并发插件替换。
count-min-log - 执行计数最小日志草图:使用近似计数器近似计数。
encoding - 整数压缩库。
go-adaptive-radix-tree - 执行自适应基数树。
go-datastructures - 收集有用的,执行的和线程安全的数据结构。
go-ef - 执行Elias-Fano编码。
go-geoindex - 内存geo索引。
go-rquad - 具有高效点位置和邻居查找的区域四叉树。
gods-数据结构。容器,集合,列表,堆栈,地图,BidiMaps,树,HashSet等
gangang-set - 线程安全和非线程安全的高性能Go集合。
goset - Go的一个有用的集合实现。
goskiplist - Go中的 Skip list实现。
goota - 数据框架和数据争用方法实现。
hilbert - Go包,用于将值映射到空格填充曲线(如Hilbert和Peano曲线)。
hyperloglog - HyperLogLog实现与稀疏,LogLog-Beta偏差校正和TailCut空间缩减。
levenshtein - Levenshtein距离和相似性度量。
levenshtein - 在Go中计算levenshtein距离的实现。
mafsa - MA-FSA实现与最小完美哈希。
merkletree - 实现一个merkle树,提供数据结构内容的高效安全验证。
ttlcache - 内存中的LRU string-interface {}映射
willf/ bloom - 执行Bloom过滤器的包。
数据库
Go中实现的数据库。
badger - 快捷键值对存储。
BigCache - 高效的键/值缓存,用于千兆字节数据。
bolt - Go的低级键/值数据库。
buntdb - 具有自定义索引和空间支持的快速可嵌入内存中的键/值数据库。
cache2go - 内存中的Key:value缓存,支持基于超时的自动无效。
cockroach - 可扩展,地理复制,事务性数据存储。
couchcache - 由Couchbase服务器支持的RESTful缓存微服务。
dgraph - 可扩展,分布式,低延迟,高吞吐量图形数据库。
diskv - 支持键值存储。
eliasdb - 具有REST API,短语搜索和类似SQL的查询语言的无依赖关系的事务图数据库。
forestdb - ForestDB绑定。
GCache - 缓存库,支持可预见的Cache,LFU,LRU和ARC。
geocache - 内存缓存,适用于基于位置的应用程序。
go-cache - 内存key:value存储/缓存(类似于Memcached)库,适用于单机应用程序。
goleveldb - 在Go中实现LevelDB键/值数据库。
groupcache - Groupcache是一个缓存和缓存填充库,用于在许多情况下替代memcached。
influxdb - 可扩展的数据存储区,用于度量,事件和实时分析。
ledisdb - Ledisdb是一个基于LevelDB的高性能NoSQL,如Redis。
levigo - Levigo是LevelDB的Go包装器。
Moss - Moss是一个简单的LSM键值存储引擎,用100%的Go语言编写。
piladb - 基于堆栈数据结构的轻量级RESTful数据库引擎。
prometheus - 监控系统和时间序列数据库。
rqlite - 构建在SQLite上的轻量级,分布式,关系型数据库。
Scribble - 微小的平面文件JSON存储。
tempdb - 临时项目的键值存储。
tidb - TiDB是一个分布式SQL数据库。灵感来自于Google F1的设计。
tiedot - 由Golang提供支持的NoSQL数据库。
Tile38 - 具有空间索引和实时地理位置的地理数据库。
数据库模式迁移
darwin - Go的数据库模式演化库。
go-fixtures - 用于Golang内置数据库/ sql库的Django样式装置。
goose - 数据库迁移工具,可以通过创建增量SQL或Go脚本来管理数据库的演进。
gormigrate - Gorm ORM的数据库模式迁移帮助器。
migrate - 数据库迁移。CLI和Golang库。
pravasan - 简单的迁移工具 - 目前用于MySQL,但计划即将支持Postgres,SQLite,MongoDB等。
soda - MySQL,PostgreSQL和SQLite的数据库迁移,创建,ORM等。
sql-migrate - 数据库迁移工具。允许使用go-bindata将迁移嵌入到应用程序中。
数据库工具
go-mysql - Go工具集来处理MySQL协议和复制。
go-mysql-elasticsearch - 自动将MySQL数据同步到弹性搜索。
kingshard - kingshard是由Golang提供的MySQL高性能代理。
myreplication - MySql二进制日志复制侦听器,支持语句和基于行的复制。
orchestrator - MySQL复制拓扑管理器和可视化器。
pgweb - 基于Web的PostgreSQL数据库浏览器。
pREST - 从任何PostgreSQL数据库提供RESTful API。
vitess - 提供服务器和工具,便于MySQL数据库扩展大型Web服务。
SQL查询构建器,用于构建和使用SQL库
dat - Postgres数据访问工具包。
Dotsql - 将sql文件保存在一个地方并轻松使用。
goqu - 惯用SQL构建器和查询库。
igor - PostgreSQL抽象层,支持高级功能,并使用类似gorm的语法。
ozzo-dbx - 强大的数据检索方法以及与数据库无关的查询构建能力。
scaneo - 生成Go代码将数据库行转换为任意结构。
sqrl - SQL查询生成器,性能提升。
Squirrel - 构建SQL查询的库。
xo - 根据现有架构定义或支持PostgreSQL,MySQL,SQLite,Oracle和Microsoft SQL Server的自定义查询,为数据库生成惯用Go代码。
机器学习
机器学习库
bayesian - Go语言的朴素贝叶斯分类。
CloudForest - 以纯Go为机器学习的快速,灵活,多线程的决策树组合。
gago - 灵活并行的遗传算法。
go-fann - 快速人工神经网络(FANN)库的绑定。
gogo galib - Go / golang编写的遗传算法库。
go-pr - Golang中的图像识别包。
gobrain - Go语言写的神经网络。
godist - 各种概率分布和相关方法。
goga - Go的遗传算法库。
GoLearn - Go的通用机器学习库。
golinear - Go的liblinear绑定。
goml - 在线机器学习。
goRecommend - 使用Go编写的推荐算法库。
gorgonia - 基于图形的计算库,如Theano for Go,为构建各种机器学习和神经网络算法提供原始数据。
goscore - 获取PMML的API。
libsvm - libsvm golang版本派生工作基于LIBSVM 3.14。
mlgo - 该项目旨在提供Go中的简约机器学习算法。
neat -NeuroEvolution增强拓扑(NEAT)的即插即用并行Go框架。
neural-go —Go中实施的多层感知网络,通过反向传播进行培训。
probab - 概率分布函数。
regommend - 推荐和协同过滤引擎。
shield - 贝叶斯文本分类器,具有灵活的标记器和Go存储后端。
自然语言处理
dpar - 基于过渡的统计依赖解析器。
go-eco - 相似性,不相似性和距离矩阵;多样性,公平和不平等的措施;物种丰富度估计; coenocline模型。
go-i18n - 使用本地化文本的软件包和随附工具。
go-mystem - CGo绑定到Yandex.Mystem - 俄语形态分析器。
go-nlp - 使用离散概率分布和其他可用于执行NLP工具的实用程序。
go-stem - porter stemming算法实现。
go-unidecode - Unicode文本的ASCII音译。
go2vec - word2vec嵌入式阅读器和效用函数。
gojieba - 这是一个Go执行的jieba中文分词算法。
gounidecode - 用于Go的Unicode音译(也称为unidecode)。
icu - Cgo绑定icu4c C库检测和转换功能。保证与版本50.1兼容。
libtextcat - 用于libtextcat C库的Cgo绑定。保证与版本2.2的兼容性。
MMSEGO - 这是一个中文分词算法MMSEG的GO实现。
nlp - 从字符串中提取值并使用nlp填充结构体。
nlp - 自然语言处理库支持LSA(潜在语义分析)。
paicehusk - Golang实施Paice / Husk Stemming算法。
porter - 这是一个非常简单的 Martin Porter实现Porter干扰算法的端口。
prose - 支持标记化,词性标注,命名实体提取等的文本处理库。
RAKE.go - 快速自动关键词提取算法(RAKE)的端口。
stemmer - 用于Go的Stemmer包。包括英语和德语词干。
textcat - 基于n-gram的文本分类Go包,支持utf-8和原始文本。
whatlanggo - Go的自然语言检测包。支持84种语言和24种脚本(写作系统,如拉丁语,西里尔字体等)。
when - 具有可插拔规则的自然EN和RU语言日期/时间解析器。
如果你觉得这些还不过瘾,可以去Github页面(项目源地址:https://github.com/avelino/awesome-go#web-frameworks)与众多Go语言程序员互动。
*请认真填写需求信息,我们会在24小时内与您取得联系。