Skip to content

Latest commit

 

History

History
216 lines (148 loc) · 14.9 KB

0.1.md

File metadata and controls

216 lines (148 loc) · 14.9 KB

go build

go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。

例如,如果我们在执行go build命令时不后跟任何代码包,那么命令将试图编译当前目录所对应的代码包。例如,我们想编译goc2p项目的代码包logging。其中一个方法是进入logging目录并直接执行该命令:

hc@ubt:~/golang/goc2p/src/logging$ go build

因为在代码包logging中只有库源码文件和测试源码文件,所以在执行go build命令之后不会在当前目录和goc2p项目的pkg目录中产生任何文件。

**插播:**Go语言的源码文件有三大类,即:命令源码文件、库源码文件和测试源码文件。他们的功用各不相同,而写法也各有各的特点。命令源码文件总是作为可执行的程序的入口。库源码文件一般用于集中放置各种待被使用的程序实体(全局常量、全局变量、接口、结构体、函数等等)。而测试源码文件主要用于对前两种源码文件中的程序实体的功能和性能进行测试。另外,后者也可以用于展现前两者中程序的使用方法。

另外一种编译logging包的方法是:

hc@ubt:~/golang/goc2p/src$ go build logging

在这里,我们把代码包logging的导入路径作为参数传递给go build命令。另一个例子:如果我们要编译代码包cnet/ctcp,只需要在任意目录下执行命令go build cnet/ctcp即可。

**插播:**之所以这样的编译方法可以正常执行,是因为我们已经在环境变量GOPATH中加入了goc2p项目的根目录(即~/golang/goc2p/)。这时,goc2p项目的根目录就成为了一个工作区目录。只有这样,Go语言才能正确识别我们提供的goc2p项目中某个代码包的导入路径。而代码包的导入路径是指,相对于Go语言自身的源码目录(即$GOROOT/src)或我们在环境变量GOPATH中指定的某个目录的src子目录下的子路径。例如,这里的代码包logging的绝对路径是~/golang/goc2p/src/logging。而不论goc2p项目的根文件夹被放在哪儿,logging包的导入路径都是logging。显而易见,我们在称呼一个代码包的时候总是以其导入路径作为其称谓。

言归正传,除了上面的简单用法,我们还可以同时编译多个Go源码文件:

hc@ubt:~/golang/goc2p/src$ go build logging/base.go logging/console_logger.go logging/log_manager.go logging/tag.go

但是,使用这种方法会有一个限制。作为参数的多个Go源码文件必须在同一个目录中。也就是说,如果我们想用这种方法既编译logging包又编译basic包是不可能的。不过别担心,在需要的时候,那些被编译目标依赖的代码包会被go build命令自动的编译。例如,如果有一个导入路径为app的代码包,同时依赖了logging包和basic包。那么在执行go build app的时候,该命令就会自动的在编译app包之前去检查logging包和basic包的编译状态。如果发现它们的编译结果文件不是最新的,那么该命令就会先去的编译这两个代码包,然后再编译app包。

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

另外,go build命令既不能编译包含多个命令源码文件的代码包,也不能同时编译多个命令源码文件。因为,如果把多个命令源码文件作为一个整体看待,那么每个文件中的main函数就属于重名函数,在编译时会抛出重复定义错误。假如,在goc2p项目的代码包cmd(此代码包仅用于示例目的,并不会永久存在于该项目中)中包含有两个命令源码文件showds.go和initpkg_demo.go,那么我们在使用go build命令同时编译它们时就会失败。示例如下:

hc@ubt:~/golang/goc2p/src/cmd$ go build showds.go initpkg_demo.go
# command-line-arguments
./initpkg_demo.go:19: main redeclared in this block
        previous declaration at ./showds.go:56

请注意上面示例中的command-line-arguments。在这个位置上应该显示的是作为编译目标的源码文件所属的代码包的导入路径。但是,这里显示的并不是它们所属的代码包的导入路径cmd。这是因为,命令程序在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包,则会在内部生成一个虚拟代码包。这个虚拟代码包的导入路径和名称都会是command-line-arguments。在其他基于编译流程的命令程序中也有与之一致的操作,比如go install命令和go run命令。

另一方面,如果我们编译的多个属于main包的源码文件中没有main函数的声明,那么就会使编译器立即报出“未定义main函数声明”的错误并中止编译。换句话说,在我们同时编译多个main包的源码文件时,要保证其中有且仅有一个main函数声明,否则编译是无法成功的。

现在我们使用go build命令编译单一命令源码文件。我们在执行命令时加入一个标记-v。这个标记的意义在于可以使命令把执行过程中构建的包名打印出来。我们会在稍后对这个标记进行详细说明。现在我们先来看一个示例:

hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
initpkg_demo.go
hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -v initpkg_demo.go 
command-line-arguments
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
initpkg_demo  initpkg_demo.go

我们在执行命令go build -v initpkg_demo.go之后被打印出的command-line-arguments”`就是命令程序为命令源码文件initpkg_demo.go生成的虚拟代码包的包名。顺带说一句,

命令go build会把编译命令源码文件后生成的结果文件存放到执行该命令时所在的目录下。这个所说的结果文件就是与命令源码文件对应的可执行文件。它的名称会与命令源码文件的主文件名相同。

顺便说一下,如果我们有多个声明为属于main包的源码文件,且其中只有一个文件声明了main函数的话,那么是可以使用go build命令同时编译它们的。在这种情况下,不包含main函数声明的那几个源码文件会被视为库源码文件(理所当然)。如此编译之后的结果文件的名称将会与我们指定的编译目标中最左边的那个源码文件的主文件名相同。

其实,除了让Go语言编译器自行决定可执行文件的名称,我们还可以自定义它。示例如下:

hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -o initpkg initpkg_demo.go 
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
initpkg    initpkg_demo.go

使用-o标记可以指定输出文件(在这个示例中指的是可执行文件)的名称。它是最常用的一个go build命令标记。但需要注意的是,当使用标记-o的时候,不能同时对多个代码包进行编译。

标记-i会使go build命令安装那些编译目标依赖的且还未被安装的代码包。这里的安装意味着产生与代码包对应的归档文件,并将其放置到当前工作区目录的pkg子目录的相应子目录中。在默认情况下,这些代码包是不会被安装的。

除此之外,还有一些标记不但受到go build命令的支持,而且对于后面会提到的go installgo rungo test等命令同样是有效的。下表列出了其中比较常用的标记。

表0-1 go build命令的常用标记说明

标记名称 标记描述
-a 强行对所有涉及到的代码包(包含标准库中的代码包)进行重新构建,即使它们已经是最新的了。
-n 打印编译期间所用到的其它命令,但是并不真正执行它们。
-p n 指定编译过程中执行各任务的并行数量(确切地说应该是并发数量)。在默认情况下,该数量等于CPU的逻辑核数。但是在darwin/arm平台(即iPhone和iPad所用的平台)下,该数量默认是1
-race 开启竞态条件的检测。不过此标记目前仅在linux/amd64freebsd/amd64darwin/amd64windows/amd64平台下受到支持。
-v 打印出那些被编译的代码包的名字。
-work 打印出编译时生成的临时工作目录的路径,并在编译结束时保留它。在默认情况下,编译结束时会删除该目录。
-x 打印编译期间所用到的其它命令。注意它与-n标记的区别。

我们在这里忽略了一些并不常用的或作用于编译器或连接器的标记。在本小节的最后将会对这些标记进行简单的说明。如果读者有兴趣,也可以查看Go语言的官方文档以获取相关信息。

下面我们就用其中几个标记来查看一下在构建代码包logging时创建的临时工作目录的路径:

hc@ubt:~/golang/goc2p/src$ go build -v -work logging
WORK=/tmp/go-build888760008
logging

上面命令的结果输出的第一行是为了编译logging包,Go创建的一个临时工作目录,这个目录被创建到了Linux的临时目录下。输出的第二行是对标记-v的响应。这意味着此次命令执行时仅编译了logging包。关于临时工作目录的用途和内容,我们会在讲解go run命令和go test命令的时候详细说明。

现在我们再来看看如果强制重新编译会涉及到哪些代码包:

hc@ubt:~/golang/goc2p/src$ go build -a -v -work logging
WORK=/tmp/go-build929017331
runtime
errors
sync/atomic
math
unicode/utf8
unicode
sync
io
syscall
strings
time
strconv
reflect
os
fmt
log
logging

怎么会多编译了这么多代码包呢?可以确定的是,代码包logging中的代码直接依赖了标准库中的runtime包、strings包、fmt包和log包。那么其他的代码包为什么也会被重新编译呢?

从代码包编译的角度来说,如果代码包A依赖代码包B,则称代码包B是代码包A的依赖代码包(以下简称依赖包),代码包A是代码包B的触发代码包(以下简称触发包)。

go build命令在执行时,编译程序会先查找目标代码包的所有依赖包,以及这些依赖包的依赖包,直至找到最深层的依赖包为止。在此过程中,如果发现有循环依赖的情况,编译程序就会输出错误信息并立即退出。此过程完成之后,所有的依赖关系也就形成了一棵含有重复元素的依赖树。对于依赖树中的一个节点(代码包)来说,它的直接分支节点(前者的依赖包),是按照代码包导入路径的字典序从左到右排列的。最左边的分支节点会最先被编译。编译程序会依此设定每个代码包的编译优先级。

执行go build命令的计算机如果拥有多个逻辑CPU核心,那么编译代码包的顺序可能会存在一些不确定性。但是,它一定会满足这样的约束条件:依赖代码包 -> 当前代码包 -> 触发代码包

标记-p n可以限制编译过程中任务执行的并发数量,n默认为当前计算机的CPU逻辑核数。如果在执行go build命令时加入标记-p 1,那么就可以保证代码包编译顺序严格按照预先设定好的优先级进行。现在我们再来编译logging包:

hc@ubt:~/golang/goc2p/src$ go build -a -v -work -p 1 logging
WORK=/tmp/go-build114039681
runtime
errors
sync/atomic
sync
io
math
syscall
time
os
unicode/utf8
strconv
reflect
fmt
log
unicode
strings
logging

我们可以认为,以上示例中所显示的代码包的顺序,就是logging包直接或间接依赖的代码包按照优先级从高到低排列后的排序。

另外,如果在命令中加入标记-n,那么编译程序只会输出所用到的命令而不会真正运行。在这种情况下,编译过程不会使用并发模式。

在本节的最后,我们对一些并不太常用的标记进行简要的说明:

  • -asmflags

此标记可以后跟另外一些标记,如-D-I-S等。这些后跟的标记用于控制Go语言编译器编译汇编语言文件时的行为。

  • -buildmode

此标记用于指定编译模式,使用方式如-buildmode=default(这等同于默认情况下的设置)。此标记支持的编译模式目前有6种。借此,我们可以控制编译器在编译完成后生成静态链接库(即.a文件,也就是我们之前说的归档文件)、动态链接库(即.so文件)或/和可执行文件(在Windows下是.exe文件)。

  • -compiler

此标记用于指定当前使用的编译器的名称。其值可以为gcgccgo。其中,gc编译器即为Go语言自带的编辑器,而gccgo编译器则为GCC提供的Go语言编译器。而GCC则是GNU项目出品的编译器套件。GNU是一个众所周知的自由软件项目。在开源软件界不应该有人不知道它。好吧,如果你确实不知道它,赶紧去google吧。

  • -gccgoflags

此标记用于指定需要传递给gccgo编译器或链接器的标记的列表。

  • -gcflags

此标记用于指定需要传递给go tool compile命令的标记的列表。

  • -installsuffix

为了使当前的输出目录与默认的编译输出目录分离,可以使用这个标记。此标记的值会作为结果文件的父目录名称的后缀。其实,如果使用了-race标记,这个标记会被自动追加且其值会为race。如果我们同时使用了-race标记和-installsuffix,那么在-installsuffix标记的值的后面会再被追加_race,并以此来作为实际使用的后缀。

  • -ldflags

此标记用于指定需要传递给go tool link命令的标记的列表。

  • -linkshared

此标记用于与-buildmode=shared一同使用。后者会使作为编译目标的非main代码包都被合并到一个动态链接库文件中,而前者则会在此之上进行链接操作。

  • -pkgdir

使用此标记可以指定一个目录。编译器会只从该目录中加载代码包的归档文件,并会把编译可能会生成的代码包归档文件放置在该目录下。

  • -tags

此标记用于指定在实际编译期间需要受理的编译标签(也可被称为编译约束)的列表。这些编译标签一般会作为源码文件开始处的注释的一部分,例如,在$GOROOT/src/os/file_posix.go开始处的注释为:

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windows

最后一行注释即包含了与编译标签有关的内容。大家可以查看代码包go/build的文档已获得更多的关于编译标签的信息。

  • -toolexec

此标记可以让我们去自定义在编译期间使用一些Go语言自带工具(如vetasm等)的方式。