Skip to content

Commit 4bf0790

Browse files
committed
Wake up...
1 parent fba72f7 commit 4bf0790

20 files changed

+4995
-0
lines changed

0.0.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
0.0 标准命令详解
2+
3+
为了让讲解更具关联性,也为了让读者能够更容易的理解这些命令和工具,本教程并不会按照这些命令的字典顺序讲解它们,而会按照我们在实际开发过程中通常的使用顺序以及它们的重要程度的顺序推进说明。 我们先从```go build```命令开始。

0.1.md

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
5.1.1 go build
2+
3+
4+
5+
```go build```命令用来编译指定的代码或代码包及它们的依赖包。
6+
7+
例如,如果我们在执行```go build```命令时不后跟任何代码包,那么命令将试图编译当前目录所对应的代码包。例如,我们想编译goc2p项目的代码包```logging```。其中一个方法是进入```logging```目录并直接执行该命令:
8+
9+
hc@ubt:~/golang/goc2p/src/logging$ go build
10+
11+
因为在代码包```logging```中只有库源码文件和测试源码文件,所以在执行```go build```命令之后不会在当前目录和goc2p项目的pkg目录中产生任何文件。
12+
13+
还有另外一种编译```logging```包的方式:
14+
15+
hc@ubt:~/golang/goc2p/src$ go build logging
16+
17+
在这里,我们把代码包```logging```的导入路径作为参数传递给```go build```命令。又例如,如果我们要编译代码包```cnet/ctcp```,只需要在任意目录下执行命令```go build cnet/ctcp```即可。
18+
19+
当然,我们也可以通过指定多个Go源码文件来完成编译行为:
20+
21+
hc@ubt:~/golang/goc2p/src$ go build logging/base.go logging/console_logger.go logging/log_manager.go logging/tag.go
22+
23+
但是,使用这种方法会有一个限制。作为参数的多个Go源码文件必须在同一个目录中。也就是说,如果我们想用一个命令既编译```logging```包又编译```basic```包是不可能的。不过别担心,```go build```命令能够在需要的时候去编译它们。假设有一个导入路径为```app```的代码包,同时依赖了```logging```包和```basic```包。那么在执行命令```go build app```的时候,该工具就会自动的在编译```app```包之前去编译它的所有依赖包,包括```logging```包和```basic```包。
24+
25+
注意,```go build```命令既不能编译包含多个命令源码文件的代码包,也不能同时编译多个命令源码文件。因为,如果把多个命令源码文件作为一个整体看待,那么每个文件中的main函数就属于重名函数,在编译时会抛出重复定义错误。假如,在goc2p项目的代码包```cmd```(此代码包仅用于示例目的,并不会永久存在于该项目中)中包含有两个命令源码文件showds.go和initpkg_demo.go,那么我们在使用```go build```命令同时编译它们时就会失败。示例如下:
26+
27+
hc@ubt:~/golang/goc2p/src/cmd$ go build showds.go initpkg_demo.go
28+
# command-line-arguments
29+
./initpkg_demo.go:19: main redeclared in this block
30+
previous declaration at ./showds.go:56
31+
32+
请注意上面示例中的“command-line-arguments”。在这个位置上应该显示的是作为参数的源码文件所属代码包的导入路径。但是,这里显示的并不是它们所属的代码包的导入路径```cmd```。这是因为,命令程序在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包,则会在内部生成一个虚拟代码包。这个虚拟代码包的导入路径和名称都会是“command-line-arguments”。在其他基于编译流程的命令程序中也有与之一致的操作。比如```go install```命令和```go run```命令。
33+
34+
现在我们使用```go build```命令编译单一命令源码文件。我们在执行命令时加入一个标记```-v```。这个标记的意义在于可以使命令把执行过程中构建的包名打印出来。我们会在稍后对这个标记进行详细说明。现在我们先来看一个示例:
35+
36+
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
37+
initpkg_demo.go
38+
hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -v initpkg_demo.go
39+
command-line-arguments
40+
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
41+
initpkg_demo initpkg_demo.go
42+
43+
我们在执行命令```go build -v initpkg_demo.go ```之后被打印出的“command-line-arguments”就是命令程序为命令源码文件initpkg_demo.go生成的虚拟代码包的包名。
44+
45+
```go build```命令会把编译命令源码文件后生成的结果文件存放到执行该命令时所在的目录下。这个所说的结果文件就是与命令源码文件对应的可执行文件。它的名称会与命令源码文件的主文件名相同。
46+
47+
我们可以自定义生成的可执行文件的名字,示例如下:
48+
49+
hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -o initpkg initpkg_demo.go
50+
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
51+
initpkg initpkg_demo.go
52+
53+
使用```-o```标记可以指定输出文件(在这个示例中是可执行文件)的名称。它是最常用的一个```go build```命令标记。但需要注意的是,当使用标记```-o```的时候,不能同时对多个代码包进行编译。
54+
55+
除此之外,还有一些标记在我们日常开发过程中可能会被用到。如下表。
56+
57+
_表0-1 ```go build```命令的常用标记说明_
58+
59+
<table class="table table-bordered table-striped table-condensed">
60+
<tr>
61+
<th width=25%>
62+
标记名称
63+
</th>
64+
<th>
65+
标记描述
66+
</th>
67+
</tr>
68+
<tr>
69+
<td>
70+
-o
71+
</td>
72+
<td>
73+
指定输出文件。
74+
</td>
75+
</tr>
76+
<tr>
77+
<td>
78+
-a
79+
</td>
80+
<td>
81+
强行对所有涉及到的代码包(包括标准库中的代码包)进行重新构建,即使它们已经是最新的了。
82+
</td>
83+
</tr>
84+
<tr>
85+
<td>
86+
-n
87+
</td>
88+
<td>
89+
打印构建期间所用到的其它命令,但是并不真正执行它们。
90+
</td>
91+
</tr>
92+
<tr>
93+
<td>
94+
-p n
95+
</td>
96+
<td>
97+
构建的并行数量(n)。默认情况下并行数量与CPU数量相同。
98+
</td>
99+
</tr>
100+
<tr>
101+
<td>
102+
-race
103+
</td>
104+
<td>
105+
开启数据竞争检测。此标记目前仅在linux/amd64、darwin/amd64和windows/amd64平台下被支持。
106+
</td>
107+
</tr>
108+
<tr>
109+
<td>
110+
-v
111+
</td>
112+
<td>
113+
打印出被构建的代码包的名字。
114+
</td>
115+
</tr>
116+
<tr>
117+
<td>
118+
-wrok
119+
</td>
120+
<td>
121+
打印出临时工作目录的名字,并且取消在构建完成后对它的删除操作。
122+
</td>
123+
</tr>
124+
<tr>
125+
<td>
126+
-x
127+
</td>
128+
<td>
129+
打印出构建期间所用到的其它命令。
130+
</td>
131+
</tr>
132+
</table>
133+
134+
我们在这里忽略了一些并不常用的或作用于编译器或连接器的标记。在本小节的最后将会对这些标记进行简单的说明。如果读者有兴趣,也可以查看Go语言的官方文档以获取相关信息。
135+
136+
下面我们就用其中几个标记来查看一下在构建代码包```logging```时创建的临时工作目录的路径:
137+
138+
hc@ubt:~/golang/goc2p/src$ go build -v -work logging
139+
WORK=/tmp/go-build888760008
140+
logging
141+
142+
上面命令的结果输出的第一行是为了编译```logging```包,Go创建的一个临时工作目录,这个目录被创建到了Linux的临时目录下。输出的第二行是对标记```-v```的响应,意味着这个命令执行时仅编译了```logging```包。关于临时工作目录的用途和内容,我们会在讲解```go run```命令和```go test```命令的时候详细说明。
143+
144+
现在我们再来看看如果强制重新编译会涉及到哪些代码包:
145+
146+
hc@ubt:~/golang/goc2p/src$ go build -a -v -work logging
147+
WORK=/tmp/go-build929017331
148+
runtime
149+
errors
150+
sync/atomic
151+
math
152+
unicode/utf8
153+
unicode
154+
sync
155+
io
156+
syscall
157+
strings
158+
time
159+
strconv
160+
os
161+
reflect
162+
fmt
163+
log
164+
logging
165+
166+
怎么会多编译了这么多代码包呢?代码包```logging```中的代码直接依赖了标准库中的```runtime```包、```strings```包、```fmt```包和```log```包。那么其他的代码包为什么也会被重新编译呢?
167+
168+
从代码包编译的角度来说,如果代码包A依赖代码包B,则称代码包B是代码包A的依赖代码包(以下简称依赖包),代码包A是代码包B的触发代码包(以下简称触发包)。
169+
170+
```go build```命令在执行时,编译程序会先查找目标代码包的所有依赖包,以及这些依赖包的依赖包,直至找到最深层的依赖包为止。在此过程中,如果发现有循环依赖的情况,编译程序就会输出错误信息并立即退出。此过程完成之后,所有的依赖关系形成了一棵含有重复元素的依赖树。对于依赖树中的一个节点(代码包),其直接分支节点(依赖包),是按照代码包导入路径的字典序从左到右排列的。最左边的分支节点会最先被编译。编译程序会依此设定每个代码包的编译优先级。
171+
172+
执行```go build```命令的计算机如果是多CPU的,那么编译代码包的顺序可能会有一些不确定性。但一定会满足这样的约束条件:```依赖代码包 -> 当前代码包 -> 触发代码包```
173+
174+
标记```-p n```可以限制编译代码包时的并发数量,```n```默认为当前计算机的CPU数量。如果在执行```go build````命令时加入标记```-p 1```,就可以保证代码包编译顺序严格按照预先设定好的优先级进行。现在我们再来构建```logging```包:
175+
176+
hc@ubt:~/golang/goc2p/src$ go build -a -v -work -p 1 logging
177+
WORK=/tmp/go-build114039681
178+
runtime
179+
errors
180+
sync/atomic
181+
sync
182+
io
183+
math
184+
syscall
185+
time
186+
os
187+
unicode/utf8
188+
strconv
189+
reflect
190+
fmt
191+
log
192+
unicode
193+
strings
194+
logging
195+
196+
197+
我们可以认为,以上示例中所显示的代码包的顺序,就是```logging```包直接或间接依赖的代码包按照优先级从高到低的排序。
198+
199+
另外,如果在命令中加入标记```-n```,则编译程序只会输出所用到的命令而不会真正运行。在这种情况下,编译过程不会使用并发模式。
200+
201+
关于```go build```命令可接受但不常用的标记的说明如下:
202+
203+
+ -ccflags:需要传递给每一个5c、6c或者8c编译器的参数的列表。
204+
205+
+ -compiler:指定作为运行时编译器的编译器名称。其值可以为gccgo或gc。
206+
207+
+ -gccgoflags:需要传递给每一个gccgo编译器或链接器的参数的列表。
208+
209+
+ -gcflags:需要传递给每一个5g、6g或者8g编译器的参数的列表。
210+
211+
+ -installsuffix;为了使当前的输出的目录与默认的编译输出目录分离,可以使用这个标记。此标记的值会作为结果文件的父目录名称的后缀。实际上,如果使用了```-race```标记,这个值会被自动设置为```race```。如果同时使用了这两个标记,则会在```-installsuffix```标记的值的后面再加上```_race```,以此来作为实际使用的后缀。
212+
213+
+ -ldflags:需要传递给每一个5l、6l或者8l链接器的参数的列表。
214+
215+
+ -tags:在实际编译期间需要考虑满足的编译标签(也可被称为编译约束)的列表。可以查看代码包go/build的文档已获得更多的关于编译标签的信息。
216+
217+
其中,gccgo是GNU项目针对于Go语言出品的编译器。GNU是一个众所周知的自由软件工程项目。在开源软件界不应该有人不知道它。好吧,如果你确实不知道它,赶紧去google吧。
218+
219+
gc是Go语言的官方编译器。5g、6g和8g分别是gc编译器在x86(32bit)计算架构、x86-64(64bit)计算架构和ARM计算架构的计算机上的编译程序。
220+
221+
5c、6c和8c分别是Plan 9版本的gc编译器在x86(32bit)计算架构、x86-64(64bit)计算架构和ARM计算架构的计算机上的编译程序。而Plan 9是一个分布式操作系统,由贝尔实验室的计算科学研究中心在1980年代中期至2002年开发,以作为UNIX的后继者。Plan 9操作系统与Go语言的渊源很深。Go语言的三个设计者Robert Griesemer、Rob Pike和Ken Thompson不但是C语言和Unix操作系统的设计者,也同样是Plan 9操作系统的开发者。
222+
223+
5l、6l和8l分别是在x86(32bit)计算架构、x86-64(64bit)计算架构和ARM计算架构的计算机上的链接器。
224+
225+
顺便提一下,Go语言的专用环境变量GOARCH对应于x86(32bit)计算架构、x86-64(64bit)计算架构和ARM计算架构的值分别为386、amd64和arm。

0.10.md

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
5.1.10 go fix与go tool fix
2+
3+
4+
命令```go fix```会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。这里所说的版本即Go语言的版本。代码包的所有Go语言源码文件不包括其子代码包(如果有的话)中的文件。修正操作包括把对旧程序调用的代码更换为对新程序调用的代码、把旧的语法更换为新的语法,等等。
5+
6+
这个工具其实非常有用。在编程语言的升级和演进的过程中,难免会对过时的和不够优秀的语法及标准库进行改进。这样的改进对于编程语言的向后兼容性是个挑战。我们在前面提到过向后兼容这个词。简单来说,向后兼容性就是指新版本的编程语言程序能够正确识别和解析用该编程语言的旧版本编写的程序和软件,以及在新版本的编程语言的运行时环境中能够运行用该编程语言的旧版本编写的程序和软件。对于Go语言来说,语法的改变和标准库的变更都会使得用旧版本编写的程序无法在新版本环境中编译通过。这就等于破坏了Go语言的向后兼容性。对于一个编程语言、程序库或基础软件来说,向后兼容性是非常重要的。但有时候为了让软件更加优秀,软件的开发者或维护者不得不在向后兼容性上做出一些妥协。这是一个在多方利益之间进行权衡的结果。本小节所讲述的工具正是Go语言的创造者们为了不让这种妥协给语言使用者带来困扰和额外的工作量而编写的自动化修正工具。这也充分体现了Go语言的软件工程哲学。下面让我们来详细了解它们的使用方法和内部机理。
7+
8+
命令```go fix```其实是命令```go tool fix```的简单封装。这甚至比```go fmt```命令对```gofmt```命令的封装更简单。像其它的Go命令一样,```go fix```命令会先对作为参数的代码包导入路径进行验证,以确保它是正确有效的。像在本小节开始处描述的那样,```go fix```命令会把有效代码包中的所有Go语言源码文件作为多个参数传递给```go tool fix```命令。实际上,```go fix```命令本身不接受任何标记,它会把加入的所有标记都原样传递给```go tool fix```命令。```go tool fix```命令可接受的标记如下表。
9+
10+
_表0-15 ```go tool fix```命令的标记说明_
11+
<table class="table table-bordered table-striped table-condensed">
12+
<tr>
13+
<th width=25%>
14+
标记名称
15+
</th>
16+
<th>
17+
标记描述
18+
</th>
19+
</tr>
20+
<tr>
21+
<td>
22+
-diff
23+
</td>
24+
<td>
25+
不将修正后的内容写入文件,而只打印修正前后的内容的对比信息到标准输出。
26+
</td>
27+
</tr>
28+
<tr>
29+
<td>
30+
-r
31+
</td>
32+
<td>
33+
只对目标源码文件做有限的修正操作。该标记的值即为允许的修正操作的名称。多个名称之间用英文半角逗号分隔。
34+
</td>
35+
</tr>
36+
<tr>
37+
<td>
38+
-force
39+
</td>
40+
<td>
41+
使用此标记后,即使源码文件中的代码已经与Go语言的最新版本相匹配了,也会强行执行指定的修正操作。该标记的值就是需要强行执行的修正操作的名称,多个名称之间用英文半角逗号分隔。
42+
</td>
43+
</tr>
44+
<table>
45+
46+
在默认情况下,```go tool fix```命令程序会在目标源码文件上执行所有的修正操作。多个修正操作的执行会按照每个修正操作中标示的操作建立日期以从早到晚的顺序进行。我们可以通过执行```go tool fix -?```来查看```go tool fix```命令的使用说明以及当前支持的修正操作。
47+
48+
与本书对应的Go语言版本的```go tool fix```命令目前只支持两个修正操作。一个是与标准库代码包```go/printer```中的结构体类型```Config```的初始化代码相关的修正操作,另一个是与标准库代码包``net```中的结构体类型```IPAddr``````UDPAddr``````TCPAddr```的初始化代码相关的修正操作。从修正操作的数量来看,自第一个正式版发布以来,Go语言的向后兼容性还是很好的。从Go语言官网上的说明也可以获知,在Go语言的第二个大版本(Go 2.x)出现之前,它会一直良好的向后兼容性。
49+
50+
值得一提的是,上述的修正操作都是依靠Go语言的标准库代码包```go```及其子包中提供的功能来完成的。实际上,```go tool fix```命令程序在执行修正操作之前,需要先将目标源码文件中的内容解析为一个抽象语法树实例。这一功能其实就是由代码包```go/parser```提供的。而在这个抽象语法树实例中的各个元素的结构体类型的定义以及检测、访问和修改它们的方法则由代码包```go/ast```提供。有兴趣的读者可以阅读这些代码包中的代码。这对于深入理解Go语言对代码的静态处理过程是非常有好处的。
51+
52+
回到正题。与```gofmt```命令相同,```go tool fix```命令也有交互模式。我们同样可以通过执行不带任何参数的命令来进入到这个模式。但是与```gofmt```命令不同的是,我们在```go tool fix```命令的交互模式中输入的代码必须是完整的,即必须要符合Go语言源码文件的代码组织形式。当我们输入了不完整的代码片段时,命令程序将显示错误提示信息并退出。示例如下:
53+
54+
hc@ubt:~$ go tool fix -r='netipv6zone'
55+
a := &net.TCPAddr{ip4, 8080}
56+
standard input:1:1: expected 'package', found 'IDENT' a
57+
58+
相对于上面的示例,我们必须要这样输入源码才能获得正常的结果:
59+
60+
hc@ubt:~$ go tool fix -r='netipv6zone'
61+
package main
62+
63+
import (
64+
"fmt"
65+
"net"
66+
)
67+
68+
func main() {
69+
addr := net.TCPAddr{"127.0.0.1", 8080}
70+
fmt.Printf("TCP Addr: %s\n", addr)
71+
}
72+
standard input: fixed netipv6zone
73+
package main
74+
75+
import (
76+
"fmt"
77+
"net"
78+
)
79+
80+
func main() {
81+
addr := net.TCPAddr{IP: "127.0.0.1", Port: 8080}
82+
fmt.Printf("TCP Addr: %s\n", addr)
83+
}
84+
85+
上述示例的输出结果中有这样一行提示信息:“standard input: fixed netipv6zone”。其中,“standard input”表明源码是从标准输入而不是源码文件中获取的,而“fixed netipv6zone”则表示名为netipv6zone的修正操作发现输入的源码中有需要修正的地方,并且已经修正完毕。另外,我们还可以看到,输出结果中的代码已经经过了格式化。

0 commit comments

Comments
 (0)