郝林,10年软件开发从业经验。搞过银行、电信软件和互联网社交等产品。对Go语言和Docker都情有独钟。现在正在从事互联网软件基础组件的构建以及Go语言的技术推广和社区运营等工作。他也是图灵原创技术图书《Go并发编程实战》和在线免费教程《Go语言第一课》的作者。
本教程的由来
关于协议
版本信息
书中演示代码基于以下版本:
系统
Go
1.3+
标准命令详解
1.4版本的Go工具目录的内容如下:
5a5l6g8caddr2linedistobjdumptour5c6a6l8gcgofixpackvet5g6c8a8lcovernmpprofyacc
下面是Go1.5版本的:
addr2lineasmcompiledistfixnmpacktourvetapicgocoverdoclinkobjdumppproftraceyacc
可以看到,1.5版本的目录内容精简了不少。这是因为Go1.5的编译器、链接器都已经完全用Go语言重写了。而在这之前,它们都是用C语言写的,因此不得不为每类平台编写不同的程序并生成不同的文件。例如,8g、6g和5g分别是gc编译器在x86(32bit)、x86-64(64bit)和ARM计算架构的计算机上的实现程序。相比之下,用Go语言实现的好处就是,编译器和链接器都将是跨平台的了。简要来说,Go1.5版本的目录中的文件compile即是统一后的编译器,而文件link则是统一后的链接器。
为了让讲解更具关联性,也为了让读者能够更容易的理解这些命令和工具,本教程并不会按照这些命令的字典顺序描述它们,而会按照我们在实际开发过程中通常的使用顺序以及它们的重要程度来逐一进行说明。现在,我们就先从gobuild命令开始。
gobuild命令用于编译我们指定的源码文件或代码包以及它们的依赖包。
例如,如果我们在执行gobuild命令时不后跟任何代码包,那么命令将试图编译当前目录所对应的代码包。例如,我们想编译goc2p项目的代码包logging。其中一个方法是进入logging目录并直接执行该命令:
hc@ubt:~/golang/goc2p/src/logging$gobuild
因为在代码包logging中只有库源码文件和测试源码文件,所以在执行gobuild命令之后不会在当前目录和goc2p项目的pkg目录中产生任何文件。
插播:Go语言的源码文件有三大类,即:命令源码文件、库源码文件和测试源码文件。他们的功用各不相同,而写法也各有各的特点。命令源码文件总是作为可执行的程序的入口。库源码文件一般用于集中放置各种待被使用的程序实体(全局常量、全局变量、接口、结构体、函数等等)。而测试源码文件主要用于对前两种源码文件中的程序实体的功能和性能进行测试。另外,后者也可以用于展现前两者中程序的使用方法。
另外一种编译logging包的方法是:
hc@ubt:~/golang/goc2p/src$gobuildlogging
在这里,我们把代码包logging的导入路径作为参数传递给gobuild命令。另一个例子:如果我们要编译代码包cnet/ctcp,只需要在任意目录下执行命令gobuildcnet/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$gobuildlogging/base.gologging/console_logger.gologging/log_manager.gologging/tag.go
但是,使用这种方法会有一个限制。作为参数的多个Go源码文件必须在同一个目录中。也就是说,如果我们想用这种方法既编译logging包又编译basic包是不可能的。不过别担心,在需要的时候,那些被编译目标依赖的代码包会被gobuild命令自动的编译。例如,如果有一个导入路径为app的代码包,同时依赖了logging包和basic包。那么在执行gobuildapp的时候,该命令就会自动的在编译app包之前去检查logging包和basic包的编译状态。如果发现它们的编译结果文件不是最新的,那么该命令就会先去的编译这两个代码包,然后再编译app包。
注意,gobuild命令在编译只包含库源码文件的代码包(或者同时编译多个代码包)时,只会做检查性的编译,而不会输出任何结果文件。
另外,gobuild命令既不能编译包含多个命令源码文件的代码包,也不能同时编译多个命令源码文件。因为,如果把多个命令源码文件作为一个整体看待,那么每个文件中的main函数就属于重名函数,在编译时会抛出重复定义错误。假如,在goc2p项目的代码包cmd(此代码包仅用于示例目的,并不会永久存在于该项目中)中包含有两个命令源码文件showds.go和initpkg_demo.go,那么我们在使用gobuild命令同时编译它们时就会失败。示例如下:
hc@ubt:~/golang/goc2p/src/cmd$gobuildshowds.goinitpkg_demo.go#command-line-arguments./initpkg_demo.go:19:mainredeclaredinthisblockpreviousdeclarationat./showds.go:56
请注意上面示例中的command-line-arguments。在这个位置上应该显示的是作为编译目标的源码文件所属的代码包的导入路径。但是,这里显示的并不是它们所属的代码包的导入路径cmd。这是因为,命令程序在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包,则会在内部生成一个虚拟代码包。这个虚拟代码包的导入路径和名称都会是command-line-arguments。在其他基于编译流程的命令程序中也有与之一致的操作,比如goinstall命令和gorun命令。
现在我们使用gobuild命令编译单一命令源码文件。我们在执行命令时加入一个标记-v。这个标记的意义在于可以使命令把执行过程中构建的包名打印出来。我们会在稍后对这个标记进行详细说明。现在我们先来看一个示例:
hc@ubt:~/golang/goc2p/src/basic/pkginit$lsinitpkg_demo.gohc@ubt:~/golang/goc2p/src/basic/pkginit$gobuild-vinitpkg_demo.gocommand-line-argumentshc@ubt:~/golang/goc2p/src/basic/pkginit$lsinitpkg_demoinitpkg_demo.go
我们在执行命令gobuild-vinitpkg_demo.go之后被打印出的command-line-arguments”`就是命令程序为命令源码文件initpkg_demo.go生成的虚拟代码包的包名。顺带说一句,
命令gobuild会把编译命令源码文件后生成的结果文件存放到执行该命令时所在的目录下。这个所说的结果文件就是与命令源码文件对应的可执行文件。它的名称会与命令源码文件的主文件名相同。
其实,除了让Go语言编译器自行决定可执行文件的名称,我们还可以自定义它。示例如下:
hc@ubt:~/golang/goc2p/src/basic/pkginit$gobuild-oinitpkginitpkg_demo.gohc@ubt:~/golang/goc2p/src/basic/pkginit$lsinitpkginitpkg_demo.go
使用-o标记可以指定输出文件(在这个示例中指的是可执行文件)的名称。它是最常用的一个gobuild命令标记。但需要注意的是,当使用标记-o的时候,不能同时对多个代码包进行编译。
标记-i会使gobuild命令安装那些编译目标依赖的且还未被安装的代码包。这里的安装意味着产生与代码包对应的归档文件,并将其放置到当前工作区目录的pkg子目录的相应子目录中。在默认情况下,这些代码包是不会被安装的。
除此之外,还有一些标记不但受到gobuild命令的支持,而且对于后面会提到的goinstall、gorun、gotest等命令同样是有效的。下表列出了其中比较常用的标记。
表0-1gobuild命令的常用标记说明
标记名称
标记描述
-a
强行对所有涉及到的代码包(包含标准库中的代码包)进行重新构建,即使它们已经是最新的了。
-n
打印编译期间所用到的其它命令,但是并不真正执行它们。
-pn
指定编译过程中执行各任务的并行数量(确切地说应该是并发数量)。在默认情况下,该数量等于CPU的逻辑核数。但是在darwin/arm平台(即iPhone和iPad所用的平台)下,该数量默认是1。
-race
开启竞态条件的检测。不过此标记目前仅在linux/amd64、freebsd/amd64、darwin/amd64和windows/amd64平台下受到支持。
-v
打印出那些被编译的代码包的名字。
-work
打印出编译时生成的临时工作目录的路径,并在编译结束时保留它。在默认情况下,编译结束时会删除该目录。
-x
打印编译期间所用到的其它命令。注意它与-n标记的区别。
下面我们就用其中几个标记来查看一下在构建代码包logging时创建的临时工作目录的路径:
hc@ubt:~/golang/goc2p/src$gobuild-v-workloggingWORK=/tmp/go-build888760008logging
上面命令的结果输出的第一行是为了编译logging包,Go创建的一个临时工作目录,这个目录被创建到了Linux的临时目录下。输出的第二行是对标记-v的响应。这意味着此次命令执行时仅编译了logging包。关于临时工作目录的用途和内容,我们会在讲解gorun命令和gotest命令的时候详细说明。
现在我们再来看看如果强制重新编译会涉及到哪些代码包:
hc@ubt:~/golang/goc2p/src$gobuild-a-v-workloggingWORK=/tmp/go-build929017331runtimeerrorssync/atomicmathunicode/utf8unicodesynciosyscallstringstimestrconvreflectosfmtloglogging
怎么会多编译了这么多代码包呢?可以确定的是,代码包logging中的代码直接依赖了标准库中的runtime包、strings包、fmt包和log包。那么其他的代码包为什么也会被重新编译呢?
从代码包编译的角度来说,如果代码包A依赖代码包B,则称代码包B是代码包A的依赖代码包(以下简称依赖包),代码包A是代码包B的触发代码包(以下简称触发包)。
gobuild命令在执行时,编译程序会先查找目标代码包的所有依赖包,以及这些依赖包的依赖包,直至找到最深层的依赖包为止。在此过程中,如果发现有循环依赖的情况,编译程序就会输出错误信息并立即退出。此过程完成之后,所有的依赖关系也就形成了一棵含有重复元素的依赖树。对于依赖树中的一个节点(代码包)来说,它的直接分支节点(前者的依赖包),是按照代码包导入路径的字典序从左到右排列的。最左边的分支节点会最先被编译。编译程序会依此设定每个代码包的编译优先级。
执行gobuild命令的计算机如果拥有多个逻辑CPU核心,那么编译代码包的顺序可能会存在一些不确定性。但是,它一定会满足这样的约束条件:依赖代码包->当前代码包->触发代码包。
标记-pn可以限制编译过程中任务执行的并发数量,n默认为当前计算机的CPU逻辑核数。如果在执行gobuild命令时加入标记-p1,那么就可以保证代码包编译顺序严格按照预先设定好的优先级进行。现在我们再来编译logging包:
hc@ubt:~/golang/goc2p/src$gobuild-a-v-work-p1loggingWORK=/tmp/go-build114039681runtimeerrorssync/atomicsynciomathsyscalltimeosunicode/utf8strconvreflectfmtlogunicodestringslogging
我们可以认为,以上示例中所显示的代码包的顺序,就是logging包直接或间接依赖的代码包按照优先级从高到低排列后的排序。
另外,如果在命令中加入标记-n,那么编译程序只会输出所用到的命令而不会真正运行。在这种情况下,编译过程不会使用并发模式。
在本节的最后,我们对一些并不太常用的标记进行简要的说明:
此标记可以后跟另外一些标记,如-D、-I、-S等。这些后跟的标记用于控制Go语言编译器编译汇编语言文件时的行为。
此标记用于指定编译模式,使用方式如-buildmode=default(这等同于默认情况下的设置)。此标记支持的编译模式目前有6种。借此,我们可以控制编译器在编译完成后生成静态链接库(即.a文件,也就是我们之前说的归档文件)、动态链接库(即.so文件)或/和可执行文件(在Windows下是.exe文件)。
此标记用于指定当前使用的编译器的名称。其值可以为gc或gccgo。其中,gc编译器即为Go语言自带的编辑器,而gccgo编译器则为GCC提供的Go语言编译器。而GCC则是GNU项目出品的编译器套件。GNU是一个众所周知的自由软件项目。在开源软件界不应该有人不知道它。好吧,如果你确实不知道它,赶紧去google吧。
此标记用于指定需要传递给gccgo编译器或链接器的标记的列表。
此标记用于指定需要传递给gotoolcompile命令的标记的列表。
为了使当前的输出目录与默认的编译输出目录分离,可以使用这个标记。此标记的值会作为结果文件的父目录名称的后缀。其实,如果使用了-race标记,这个标记会被自动追加且其值会为race。如果我们同时使用了-race标记和-installsuffix,那么在-installsuffix标记的值的后面会再被追加_race,并以此来作为实际使用的后缀。
此标记用于指定需要传递给gotoollink命令的标记的列表。
此标记用于与-buildmode=shared一同使用。后者会使作为编译目标的非main代码包都被合并到一个动态链接库文件中,而前者则会在此之上进行链接操作。
使用此标记可以指定一个目录。编译器会只从该目录中加载代码包的归档文件,并会把编译可能会生成的代码包归档文件放置在该目录下。
此标记用于指定在实际编译期间需要受理的编译标签(也可被称为编译约束)的列表。这些编译标签一般会作为源码文件开始处的注释的一部分,例如,在$GOROOT/src/os/file_posix.go开始处的注释为:
最后一行注释即包含了与编译标签有关的内容。大家可以查看代码包go/build的文档已获得更多的关于编译标签的信息。
此标记可以让我们去自定义在编译期间使用一些Go语言自带工具(如vet、asm等)的方式。
命令goinstall用于编译并安装指定的代码包及它们的依赖包。当指定的代码包的依赖包还没有被编译和安装时,该命令会先去处理依赖包。与gobuild命令一样,传给goinstall命令的代码包参数,应该以导入路径的形式提供。并且,gobuild命令的绝大多数标记也都可以用于goinstall命令。实际上,goinstall命令只比gobuild命令多做了一件事,即:安装编译后的结果文件到指定目录。
在对goinstall命令进行详细说明之前,让我们先回顾一下goc2p的目录结构。为了节省篇幅,我们在这里隐藏了代码包中的源码文件。如下:
$HOME/golang/goc2p:bin/pkg/src/cnet/logging/helper/ds/pkgtool/
我们看到,goc2p项目中有三个子目录,分别是bin目录、pkg目录和src目录。现在只有src目录中包含了一些目录,而其他两个目录都是空的。
现在,我们来看看安装代码包的规则。
安装代码包
如果goinstall命令后跟的代码包中仅包含库源码文件,那么goinstall命令会把编译后的结果文件保存在源码文件所在工作区的pkg目录下。对于仅包含库源码文件的代码包来说,这个结果文件就是对应的代码包归档文件。相比之下,我们在使用gobuild命令对仅包含库源码文件的代码包进行编译时,是不会在当前工作区的src目录和pkg目录下产生任何结果文件的。结果文件会出于编译的目的被生成在临时目录中,但并不会对当前工作区目录产生任何影响。
如果我们在执行goinstall命令时不后跟任何代码包参数,那么命令将试图编译当前目录所对应的代码包。比如,我们现在要安装代码包pkgtool:
hc@ubt:~/golang/goc2p/src/pkgtool$goinstall-v-workWORK=D:\cygwin\tmp\go-build758586887pkgtool
我们刚刚说过,执行goinstall命令后会对指定代码包先编译再安装。其中,编译代码包使用了与gobuild命令相同的程序。所以,执行goinstall命令后也会首先建立一个名称以go-build为前缀的临时目录。如果我们想强行重新安装指定代码包及其依赖包,那么就需要加入标记-a:
hc@ubt:~/golang/goc2p/src/pkgtool$goinstall-a-v-workWORK=/tmp/go-build014992994runtimeerrorssync/atomicunicodeunicode/utf8sortsynciosyscallbytesstringstimebufioospath/filepathpkgtool
可以看到,代码包pkgtool仅仅依赖了标准库中的代码包。
现在我们再来查看一下goc2p项目目录:
$HOME/golang/goc2p:bin/pkg/linux_386/pkgtool.asrc/
hc@ubt:~/golang/goc2p/src/pkgtool$goinstall-a-v-work../cnet/ctcpWORK=/tmp/go-build083178213runtimeerrorssync/atomicunicodeunicode/utf8mathsortsynciosyscallbytesstringsbufiotimestrconvmath/randosreflectfmtlogruntime/cgologgingnetcnet/ctcp
请注意,我们是在代码包pkgtool对应的目录下安装cnet/ctcp包的。我们使用了一个目录相对路径。
实际上,这种提供代码包位置的方式被叫做本地代码包路径方式,也是被所有Go命令接受的一种方式,这包括之前已经介绍过的gobuild命令。但是需要注意的是,本地代码包路径只能以目录相对路径的形式呈现,而不能使用目录绝对路径。请看下面的示例:
hc@ubt:~/golang/goc2p/src/cnet/ctcp$goinstall-v-work~/golang/goc2p/src/cnet/ctcpcan'tloadpackage:package/home/hc/golang/goc2p/src/cnet/ctcp:import"/home/hc/golang/goc2p/src/cnet/ctcp":cannotimportabsolutepath
从上述示例中的命令提示信息我们可以看到,以目录绝对路径的形式提供代码包位置是不会被Go命令认可的。
这是由于Go认为本地代码包路径的表示只能以“./”或“../”开始,再或者直接为“.”或“..”。而代码包的代码导入路径又不允许以“/”开始。所以,这种用绝对路径表示代码包位置的方式也就不被支持了。
上述规则适用于所有Go命令。读者可以自己尝试一下,比如在执行gobuild命令时分别以代码包导入路径、目录相对路径和目录绝对路径的形式提供代码包位置,并查看执行结果。
我们已经通过上面的示例强行的重新安装了cnet/ctcp包及其依赖包。现在我们再来看一下goc2p的项目目录:
$HOME/golang/goc2p:bin/pkg/linux_386//cnetctcp.alogging.apkgtool.asrc/
还有一个问题:上述的安装过程涉及到了那么多代码包,那为什么goc2p项目的pkg目录中只包含该项目中代码包的归档文件呢?实际上,goinstall命令会把标准库中的代码包的归档文件存放到Go根目录的pkg目录中,而把指定代码包依赖的第三方项目的代码包的归档文件存放到那个项目的pkg目录下。这样就实现了Go语言标准库代码包的归档文件与用户代码包的归档文件,以及处在不同工作区的用户代码包的归档文件之间的彻底分离。
安装命令源码文件
除了安装代码包之外,goinstall命令还可以安装命令源码文件。为了看到安装命令源码文件是goc2p项目目录的变化,我们先把该目录还原到原始状态,即清除bin子目录和pkg子目录下的所有目录和文件。然后,我们来安装代码包helper/ds下的命令源码文件showds.go,如下:
hc@ubt:~/golang/goc2p/src$goinstallhelper/ds/showds.gogoinstall:noinstalllocationfordirectory/home/hc/golang/goc2p/src/helper/dsoutsideGOPATH
这次我们没能成功安装。该Go命令认为目录/home/hc/golang/goc2p/src/helper/ds不在环境GOPATH中。我们可以通过Linux的echo命令来查看一下环境变量GOPATH的值:
hc@ubt:~/golang/goc2p/src$echo$GOPATH/home/hc/golang/lib:/home/hc/golang/goc2p
环境变量GOPATH的值中确实包含了goc2p项目的根目录。这到底是怎么回事呢?
作者通过查看Go命令的源码文件($GOROOT/src/go/*.go)找到了其根本原因。在上一小节我们提到过,在环境变量GOPATH中包含多个工作区目录路径时,我们需要在编译命令源码文件前先对环境变量GOBIN进行设置。实际上,这个环境变量所指的目录路径就是命令程序生成的结果文件的存放目录。goinstall命令会把相应的可执行文件放置到这个目录中。
由于命令gobuild在编译库源码文件后不会产生任何结果文件,所以自然也不用会在意结果文件的存放目录。在该命令编译单一的命令源码文件时,在结果文件存放目录无效的情况下会将结果文件(也就是可执行文件)存放到执行该命令时所在的目录下。因此,即使环境变量GOBIN的值无效,我们在执行gobuild命令时也不会见到这个错误提示信息。
然而,goinstall命令中一个很重要的步骤就是将结果文件(归档文件或者可执行文件)存放到相应的目录中。所以,命令goinstall在安装命令源码文件时,如果环境变量GOBIN的值无效,则它会在最后检查结果文件存放目录的时候发现这一问题,并打印与上述示例所示内容类似的错误提示信息,最后直接退出。
这个错误提示信息在我们安装多个库源码文件时也有可能遇到。示例如下:
hc@ubt:~/golang/goc2p/src/pkgtool$goinstallenvir.gofpath.goipath.gopnode.goutil.gogoinstall:noinstalllocationfordirectory/home/hc/golang/goc2p/src/pkgtooloutsideGOPATH
而且,在我们为环境变量GOBIN设置了正确的值之后,这个错误提示信息仍然会出现。这是因为,只有在安装命令源码文件的时候,命令程序才会将环境变量GOBIN的值作为结果文件的存放目录。而在安装库源码文件时,在命令程序内部的代表结果文件存放目录路径的变量不会被赋值。最后,命令程序会发现它依然是个无效的空值。所以,命令程序会同样返回一个关于“无安装位置”的错误。这就引出一个结论,我们只能使用安装代码包的方式来安装库源码文件,而不能在goinstall命令罗列并安装它们。另外,goinstall命令目前无法接受标记-o以自定义结果文件的存放位置。这也从侧面说明了goinstall命令当前还不支持针对库源码文件的安装操作。
单从上述问题来讲,Go工具在执行错误识别及其提示信息的细分方面还没有做到最好。
hc@ubt:~$gogetgithub.com/hyper-carrot/go_lib
命令goget可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。在上面这个示例中,我们从著名的代码托管站点Github上下载了一个项目(或称代码包),并安装到了环境变量GOPATH中包含的第一个工作区中。在本机中,这个代码包的导入路径就是github.com/hyper-carrot/go_lib。
一般情况下,为了分离自己与第三方的代码,我们会设置两个及以上的工作区。我们现在新建一个目录路径为~/golang/lib的工作区,并把这个工作区路径作为环境变量GOPATH值中的第一个目录路径。注意,环境变量GOPATH中包含的路径不能与环境变量GOROOT的值重复。如此一来,如果我们再使用goget命令下载和安装代码包,那么这些代码包就都会被安装在这个新的工作区中了。我们暂且把这个工作区叫做Lib工作区。假如我们在Lib工作区建立和设置完毕之后运行了上面示例中的命令,那么这个代码包就应该会被保存在Lib工作的src目录下,并且已经被安装妥当,如下所示:
$HOME/golang/lib:bin/pkg/linux_386/github.com/hyper-carrot/go_lib.asrc/github.com/hyper-carrot/go_lib/...
实际上,像goc2p项目这样直接以项目根目录的路径作为工作区路径的做法是不被推荐的。之所以这样做主要是想让读者更容易的理解Go语言的工程结构和工作区概念,也可以让读者看到另一种项目结构。
**远程导入路径分析**
实际上,goget命令所做的动作也被叫做代码包远程导入,而传递给该命令的作为代码包导入路径的参数又被叫做代码包远程导入路径。
实际上,goget命令不仅可以从像Github这样著名的代码托管站点上下载代码包,还可以从任何命令支持的代码版本控制系统(英文为VersionControlSystem,简称为VCS)检出代码包。任何代码托管站点都是通过某个或某些代码版本控制系统来提供代码上传下载服务的。所以,更严格的讲,goget命令所做的是从代码版本控制系统的远程仓库中检出/更新代码包并对其进行编译和安装。
该命令所支持的VCS的信息如下表:
表0-2goget命令支持的VCS
名称
主命令
说明
Mercurial
hg
Mercurial是一种轻量级分布式版本控制系统,采用Python语言实现,易于学习和使用,扩展性强。
Git
git
Git最开始是LinuxTorvalds为了帮助管理Linux内核开发而开发的一个开源的分布式版本控制软件。但现在已被广泛使用。它是被用来进行有效、高速的各种规模项目的版本管理。
Subversion
svn
Subversion是一个版本控制系统,也是第一个将分支概念和功能纳入到版本控制模型的系统。但相对于Git和Mercurial而言,它只算是传统版本控制系统的一员。
Bazaar
bzr
Bazaar是一个开源的分布式版本控制系统。但相比而言,用它来作为VCS的项目并不多。
goget命令在检出代码包之前必须要知道代码包远程导入路径所对应的版本控制系统和远程仓库的URL。
如果该代码包在本地工作区中已经存在,则会直接通过分析其路径来确定这几项信息。goget命令支持的几个版本控制系统都有一个共同点,那就是会在检出的项目目录中存放一个元数据目录,名称为“.”前缀加其主命令名。例如,Git会在检出的项目目录中加入名为“.git”的子目录。所以,这样就很容易判定代码包所用的版本控制系统。另外,又由于代码包已经存在,我们只需通过代码版本控制系统的更新命令来更新代码包,因此也就不需要知道其远程仓库的URL了。对于已存在于本地工作区的代码包,除非要求更新代码包,否则goget命令不会进行重复下载。如果想要求更新代码包,可以在执行goget命令时加入-u标记。这一标记会稍后介绍。
表0-3预置的代码托管站点的信息
主域名
支持的VCS
代码包远程导入路径示例
Bitbucket
bitbucket.org
Git,Mercurial
bitbucket.org/user/projectbitbucket.org/user/project/sub/directory
GitHub
github.com
github.com/user/projectgithub.com/user/project/sub/directory
GoogleCode
code.google.com
Git,Mercurial,Subversion
code.google.com/p/projectcode.google.com/p/project/sub/directorycode.google.com/p/project.subrepositorycode.google.com/p/project.subrepository/sub/directory
Launchpad
launchpad.net
launchpad.net/projectlaunchpad.net/project/serieslaunchpad.net/project/series/sub/directorylaunchpad.net/~user/project/branchlaunchpad.net/~user/project/branch/sub/directory
一般情况下,代码包远程导入路径中的第一个元素就是代码托管站点的主域名。在静态分析的时候,goget命令会将代码包远程导入路径与预置的代码托管站点的主域名进行匹配。如果匹配成功,则在对代码包远程导入路径的初步检查后返回正常的返回值或错误信息。如果匹配不成功,则会再对代码包远程导入路径进行动态分析。至于动态分析的过程,我们就不在这里赘述了。
如果对代码包远程导入路径的静态分析或/和动态分析成功并获取到对应的版本控制系统和远程仓库URL,那么goget命令就会进行代码包检出或更新的操作。随后,goget命令会在必要时以同样的方式检出或更新这个代码包的所有依赖包。
命令特有标记
命令goget可以接受所有可用于gobuild命令和goinstall命令的标记。这是因为goget命令的内部步骤中完全包含了编译和安装这两个动作。另外,goget命令还有一些特有的标记,如下表所示:
表0-4goget命令的特有标记说明
-d
让命令只执行下载动作,而不执行安装动作。
-fix
让命令在下载代码包后先执行修正动作,而后再进行编译和安装。
-u
让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。
为了更好的理解这几个特有标记,我们先清除Lib工作区的src目录和pkg目录中的所有子目录和文件。现在我们使用带有-d标记的goget命令来下载同样的代码包:
hc@ubt:~$goget-dgithub.com/hyper-carrot/go_lib
现在,让我们再来看一下Lib工作区的目录结构:
$HOME/golang/lib:bin/pkg/src/github.com/hyper-carrot/go_lib/...
我们可以看到,goget命令只将代码包下载到了Lib工作区(环境变量GOPATH中的第一个目录)的src目录,而没有进行后续的编译和安装动作。
我们知道,绝大多数计算机编程语言在进行升级和演进过程中,不可能保证100%的向后兼容(BackwardCompatibility)。在计算机世界中,向后兼容是指在一个程序或者代码库在更新到较新的版本后,用旧的版本程序创建的软件和系统仍能被正常操作或使用,或在旧版本的代码库的基础上编写的程序仍能正常编译运行的能力。Go语言的开发者们已想到了这点,并提供了官方的代码升级工具——fix。fix工具可以修复因Go语言规范变更而造成的语法级别的错误。关于fix工具,我们将放在本节的稍后位置予以说明。
假设我们本机安装的Go语言版本是1.3,但我们的程序需要用到一个很早之前用Go语言的0.9版本开发的代码包。那么我们在使用goget命令的时候可以加入-fix标记。这个标记的作用是在检出代码包之后,先对该代码包中不符合Go语言1.3版本的语言规范的语法进行修正,然后再下载它的依赖包,最后再对它们进行编译和安装。
hc@ubt:~$goget-vgithub.com/hyper-carrot/go_lib
因为我们在之前已经检出并安装了代码包go_lib,所以我们执行上面这条命令后什么也没发生。还记得加入标记-v标记意味着会打印出被构建的代码包的名字吗?现在我们使用标记-u来强行更新代码包:
hc@ubt:~$goget-v-ugithub.com/hyper-carrot/go_libgithub.com/hyper-carrot/go_lib(download)github.com/hyper-carrot/go_lib/logginggithub.com/hyper-carrot/go_lib
其中,带“(download)”后缀意味着命令从远程仓库检出或更新了代码包。从打印出的信息可以看到,goget命令先更新了参数指定的已存在于本地工作区的代码包,而后编译了它的唯一依赖包,最后编译了该代码包。我们还可以加上一个-x标记,以打印出用到的命令。读者可以自己试用一下它。
智能的下载
命令goget还有一个很值得称道的功能。在使用它检出或更新代码包之后,它会寻找与本地已安装Go语言的版本号相对应的标签(tag)或分支(branch)。比如,本机安装Go语言的版本是1.x,那么goget命令会在该代码包的远程仓库中寻找名为“go1”的标签或者分支。如果找到指定的标签或者分支,则将本地代码包的版本切换到此标签或者分支。如果没有找到指定的标签或者分支,则将本地代码包的版本切换到主干的最新版本。
前面我们说在执行goget命令时也可以加入-x标记,这样可以看到goget命令执行过程中所使用的所有命令。不知道读者是否已经自己尝试了。下面我们还是以代码包github.com/hyper-carrot/go_lib为例,并且通过之前示例中的命令的执行此代码包已经被检出到本地。这时我们再次更新这个代码包:
hc@ubt:~$goget-v-u-xgithub.com/hyper-carrot/go_libgithub.com/hyper-carrot/go_lib(download)cd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitfetchcd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitshow-refcd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitcheckoutorigin/masterWORK=/tmp/go-build034263530
在上述示例中,goget命令通过gitfetch命令将所有远程分支更新到本地,而后有用gitshow-ref命令列出本地和远程仓库中记录的代码包的所有分支和标签。最后,当确定没有名为“go1”的标签或者分支后,goget命令使用gitcheckoutorigin/master命令将代码包的版本切换到主干的最新版本。下面,我们在本地增加一个名为“go1”的标签,看看goget命令的执行过程又会发生什么改变:
hc@ubt:~$cd~/golang/lib/src/github.com/hyper-carrot/go_libhc@ubt:~/golang/lib/src/github.com/hyper-carrot/go_lib$gittaggo1hc@ubt:~$goget-v-u-xgithub.com/hyper-carrot/go_libgithub.com/hyper-carrot/go_lib(download)cd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitfetchcd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitshow-refcd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitshow-reftags/go1origin/go1cd/home/hc/golang/lib/src/github.com/hyper-carrot/go_libgitcheckouttags/go1WORK=/tmp/go-build636338114
将这两个示例进行对比,我们会很容易发现它们之间的区别。第二个示例的命令执行过程中使用gitshow-ref查看所有分支和标签,当发现有匹配的信息又通过gitshow-reftags/go1origin/go1命令进行精确查找,在确认无误后将本地代码包的版本切换到标签“go1”之上。
命令goget的这一功能是非常有用的。我们的代码在直接或间接依赖某些同时针对多个Go语言版本开发的代码包时,可以自动的检出其正确的版本。也可以说,goget命令内置了一定的代码包多版本依赖管理的功能。
执行goclean命令会删除掉执行其它命令时产生的一些文件和目录,包括:
我们再以goc2p项目的logging为例。为了能够反复体现每个标记的作用,我们会使用标记-n。使用标记-n会让命令在执行过程中打印用到的系统命令,但不会真正执行它们。如果想既打印命令又执行命令则需使用标记-x。现在我们来试用一下goclean命令:
hc@ubt:~/golang/goc2p/src$goclean-xloggingcd/home/hc/golang/goc2p/src/loggingrm-flogginglogging.exelogging.testlogging.test.exe
现在,我们加上标记-i:
hc@ubt:~/golang/goc2p/src$goclean-x-iloggingcd/home/hc/golang/goc2p/src/loggingrm-flogginglogging.exelogging.testlogging.test.exerm-f/home/hc/golang/goc2p/pkg/linux_386/logging.a
如果再加上标记-r又会打印出哪些命令呢?请读者自己试一试吧。
命令godoc是一个很强大的工具,用于展示指定代码包的文档。我们可以通过运行gogetcode.google.com/p/go.tools/cmd/godoc安装它。
hc@ubt:~$godocfmt
有时候我们只是想查看某一个函数或者结构体类型的文档,那么我们可以将这个函数或者结构体的名称加入命令的最后面,像这样:
hc@ubt:~$godocfmtPrintf
或者:
hc@ubt:~$godocosFile
如果我们想同时查看一个代码包中的几个函数的文档,则仅需将函数或者结构体名称追加到命令后面。比如我们要查看代码包fmt中函数Printf和函数Println的文档:
hc@ubt:~$godocfmtPrintfPrintln
hc@ubt:~$godoc-srcfmtPrintf
Go语言为程序使用示例代码设立了专有的规则。我们在这里暂不讨论这个规则的细节。只需要知道正因为有了这个专有规则,使得godoc命令可以根据这些规则提取相应的示例代码并把它们加入到对应的文档中。如果我们想在查看代码包net中的结构体Listener的文档的同时查看关于它的示例代码,那么我们只需要在执行命令时加入标记-ex。使用方法如下:
hc@ubt:~$godoc-exnetListener
在实际的Go语言环境中,我们可能会遇到一个命令源码文件所产生的可执行文件与代码包重名的情况。比如本节介绍的命令go和官方代码包go。现在我们要明确的告诉godoc命令要查看可执行文件go的文档,我们需要在名称前加入“cmd/”前缀:
hc@ubt:~$godoccmd/go
另外,如果我们想查看HTML格式的文档,就需要加入标记-html。当然,这样在命令行模式下的查看效果是很差的。但是,如果仔细查看的话,可以在其中找到一些相应源码的链接地址。
一般情况下,godoc命令会去Go语言根目录和环境变量GOPATH的值(一个或多个工作区)指向的工作区目录中查找代码包。不过,我们还可以通过加入标记-goroot来制定一个Go语言根目录。这个被指定的Go语言根目录仅被用于当次命令的执行。示例如下:
hc@ubt:~$godoc-goroot="/usr/local/go"fmt
我们使用如下命令启动这个文档Web服务器:
图片1.1本机的Go文档Web服务首页
图0-1本机的Go文档Web服务首页
图片1.2goc2p项目中的pkgtool包的Go文档页面
图0-2goc2p项目中的pkgtool包的Go文档页面
现在,我们在本机开启Go文档Web服务器,端口为9090。命令如下:
注意,要使用-index标记开启搜索索引,这个索引会在服务器启动时创建并维护。否则无论在Web页面还是命令行终端中提交查询都会返回错误“Searchindexdisabled:noresultsavailable”。
索引中提供了标示符和全文本搜索信息(通过正则表达式为可搜索性提供支持)。全文本搜索结果显示条目的最大数量可以通过标记-maxresults提供。标记-maxresults默认值是10000。如果不想提供如此多的结果条目,可以设置小一些的值。甚至,如果不想提供全文本搜索结果,可以将标记-maxresults的值设置为0,这样服务器就只会创建标识符索引,而根本不会创建全文本搜索索引了。标识符索引即为对程序实体(变量、常量、函数、结构体和接口)名称的索引。
正因为在使用了-index标记的情况下文档服务器会在启动时创建索引,所以在文档服务器启动之后还不能立即提供搜索服务,需要稍等片刻。在索引为被创建完毕之前,我们的搜索操作都会得到提示信息“Indexinginprogress:resultmaybeinaccurate”。
如果我们在本机用godoc命令启动了Go文档Web服务器,且IP地址为192.168.1.4、端口为9090,那么我们就可以在另一个命令行终端甚至另一台能够与本机联通的计算机中通过如下命令进行查询了。查询命令如下:
hc@ubt:~$godoc-q-server="192.168.1.4:9090"Listener
命令的最后为要查询的内容,可以是任何你想搜索的字符串,而不仅限于代码包、函数或者结构体的名称。
标记-q开启了远程查询的功能。而标记-server="192.168.1.4:9090"则指明了远程文档服务器的IP地址和端口号。实际上,如果不指明远程查询服务器的地址,那么该命令会自行将地址“:6060”和“golang.org”作为远程查询服务器的地址。这两个地址即是默认的本机文档Web站点地址和官方的文档Web站点地址。所以执行如下命令我们也可以查询到标准库的信息:
hc@ubt:~$godoc-q=truefmt
命令godoc还有很多可用的标记,但在通常情况下并不常用。读者如果有兴趣,可以在命令行环境下执行godoc进行查看。
至于怎样才能写出优秀的代码包文档,我在《Go并发编程实战》的5.2节中做了详细说明。
如果命令源码文件可以接受参数,那么在使用gorun命令运行它的时候就可以把它的参数放在它的文件名后面,像这样:
hc@ubt:~/golang/goc2p/src/helper/ds$gorunshowds.go-p~/golang/goc2p
在上面的示例中,我们使用gorun命令运行命令源码文件showds.go。这个命令源码文件可以接受一个名称为“p”的参数。我们用“-p”这种形式表示“p”是一个参数名而不是参数值。它与源码文件名之间需要用空格隔开。参数值会放在参数名的后面,两者成对出现。它们之间也要用空格隔开。如果有第二个参数,那么第二个参数的参数名与第一个参数的参数值之间也要有一个空格。以此类推。
gorun命令只能接受一个命令源码文件以及若干个库源码文件(需同属于main包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么gorun命令就只会打印错误提示信息并退出,而不会继续执行。
在通过参数检查后,gorun命令会将编译参数中的命令源码文件,并把编译后的可执行文件存放到临时工作目录中。
编译和运行过程
hc@ubt:~/golang/goc2p/src/helper/ds$gorun-nshowds.go##command-line-arguments#mkdir-p$WORK/command-line-arguments/_obj/mkdir-p$WORK/command-line-arguments/_obj/exe/cd/home/hc/golang/goc2p/src/helper/ds/usr/local/go/pkg/tool/linux_386/8g-o$WORK/command-line-arguments/_obj/_go_.8-pcommand-line-arguments-complete-D_/home/freej/mybook/goc2p/src/helper/ds-I$WORK./showds.go/usr/local/go/pkg/tool/linux_386/packgrcP$WORK$WORK/command-line-arguments.a$WORK/command-line-arguments/_obj/_go_.8cd./usr/local/go/pkg/tool/linux_386/8l-o$WORK/command-line-arguments/_obj/exe/showds-L$WORK$WORK/command-line-arguments.a$WORK/command-line-arguments/_obj/exe/showds
现在,我们来逐行解释这些被打印出来的信息。
以前缀“#”开始的是注释信息。我们看到信息中有三行注释信息,并在中间行出现了内容“command-line-arguments”。我们在讲gobuild命令的时候说过,编译命令在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包时,会在内部生成一个名为“command-line-arguments”的虚拟代码包。所以这里的注释信息就是要告诉我们下面的几行信息是关于虚拟代码包“command-line-arguments”的。
打印信息中的“$WORK”表示临时工作目录的绝对路径。为了存放对虚拟代码包“command-line-arguments”的编译结果,命令在临时工作目录中创建了名为command-line-arguments的子目录,并在其下又创建了_obj子目录和_obj/exe子目录。
然后,命令程序使用Go语言工具目录8g命令对命令源码文件showds.go进行了编译,并把结果文件存放到了$WORK/command-line-arguments/obj目录下,名为_go.8。我们在讲gobuild命令时提到过,8g命令是Go语言的官方编译器在x86(32bit)计算架构的计算机上所使用的编译程序。我们看到,编译结果文件的扩展名与底层编译命令名中的数字相对应。
编译成功后,命令程序使用pack命令将编译文件打包并直接存放到临时工作目录中。而后,它再用连接命令8l生成最终的可执行文件,并存于$WORK/command-line-arguments/_obj/exe/目录中。打印信息中的最后一行表示,命令运行了生成的可执行文件。
通过对这些打印出来的命令的解读,我们了解了临时工作目录的用途以和内容。
在上面的示例中,我们只是让gorun命令打印出运行命令源码文件showds.go过程中需要执行的命令,而没有真正运行它。如果我们想真正运行命令源码文件showds.go并且想知道临时工作目录的位置,就需要去掉标记-n并且加上标记-work。当然,如果依然想看到过程中执行的命令,可以加上标记-x。如果读者已经看过之前我们对gobuild命令的介绍,就应该知道标记-x与标记-n一样会打印出过程执行的命令,但不同的这些命令会被真正的执行。调整这些标记之后的命令就像这样:
hc@ubt:~/golang/goc2p/src/helper/ds$gorun-x-workshowds.go
当命令真正执行后,临时工作目录中就会出现实实在在的内容了,像这样:
/tmp/go-build204903183:path/_obj/_go_.8path.acommand-line-arguments/_obj/exe/showds_go_.8command-line-arguments.a
由于上述命令中包含了-work标记,所以我们可以从其输出中找到实际的工作目录(这里是/tmp/go-build204903183)。有意思的是,我们恰恰可以通过运行命令源码文件showds.go来查看这个临时工作目录的目录树:
hc@ubt:~/golang/goc2p/src/helper/ds$gorunshowds.go-p/tmp/go-build204903183
读者可以自己试一试。
我们在前面介绍过,命令源码文件如果可以接受参数,则可以在执行gorun命令运行这个命令源码文件时把参数名和参数值成对的追加在后面。实际上,如果在命令后追加参数,那么在最后执行生成的可执行文件的时候也会追加一致的参数。例如,如果这样执行命令:
hc@ubt:~/golang/goc2p/src/helper/ds$gorun-nshowds.go-p~/golang/goc2p
那么打印的最后一个命令就是:
$WORK/command-line-arguments/_obj/exe/showds-p/home/freej/golang/goc2p
可见,gorun命令会把追加到命令源码文件后面的参数原封不动的传给对应的可执行文件。
这就是一个命令源码文件从编译到运行的全过程。请记住,gorun命令包含了两个动作:编译命令源码文件和运行对应的可执行文件。
gotest命令用于对Go语言编写的程序进行测试。这种测试是以代码包为单位的。当然,这还需要测试源码文件的帮助。关于怎样编写并写好Go程序测试代码,我们会在本章的第二节加以详述。在这里,我们只讨论怎样使用命令启动测试。
现在,我们来测试goc2p项目中的几个代码包。在使用gotest命令时指定代码包的方式与其他命令无异——使用代码包导入路径。如果需要测试多个代码包,则需要在它们的导入路径之间加入空格以示分隔。示例如下:
hc@ubt:~$gotestbasiccnet/ctcppkgtoolokbasic0.010sokcnet/ctcp2.018sokpkgtool0.009s
另外,我们还可以指定测试源码文件来进行测试。这样的话,gotest命令只会执行指定文件中的测试,像这样:
hc@ubt:~/golang/goc2p/src/pkgtool$gotestenvir_test.go#command-line-arguments./envir_test.go:20:undefined:GetGoroot./envir_test.go:34:undefined:GetAllGopath./envir_test.go:74:undefined:GetSrcDirs./envir_test.go:76:undefined:GetAllGopath./envir_test.go:83:undefined:GetGorootFAILcommand-line-arguments[buildfailed]
我们看到,与指定源码文件进行编译或运行一样,命令程序会为指定的源码文件生成一个虚拟代码包——“command-line-arguments”。但是,测试并没有通过。但其原因并不是测试失败,而是编译失败。对于运行这次测试的命令程序来说,测试源码文件envir_test.go是属于代码包“command-line-arguments”的。并且,这个测试源码文件中使用了库源码文件envir.go中的函数。可以,它却没有显示导入这个库源码文件所属的代码包,这当然会引起编译错误。如果想解决这个问题,我们还需要在执行命令时加入这个测试源码文件所测试的那个源码文件。示例如下:
hc@ubt:~/golang/goc2p/src/pkgtool$gotestenvir_test.goenvir.gookcommand-line-arguments0.008s
现在,我们故意使代码包pkgtool中的某个测试失败。现在我们再来运行测试:
hc@ubt:~$gotestbasiccnet/ctcppkgtoolokbasic0.010sokcnet/ctcp2.015s---FAIL:TestGetSrcDirs(0.00seconds)envir_test.go:85:Error:Thesrcdir'/usr/local/go/src/pkg'isincorrect.FAILFAILpkgtool0.009s
我们通过以上示例中的概要信息获知,测试源码文件中envir_test.go的测试函数TestGetSrcDirs中的测试失败了。在包含测试失败的测试源码文件名的那一行信息中,紧跟测试源码文件名的用冒号分隔的数字是错误信息所处的行号,在行号后面用冒号分隔的是错误信息。这个错误信息的内容是用户自行编写的。另外,概要信息的最后一行以“FAIL”为前缀。这表明针对代码包pkgtool的测试未通过。未通过的原因在前面的信息中已有描述。
关于标记
gotest命令的标记处理部分是庞大且繁杂的,以至于使Go语言的开发者们不得不把这一部分的逻辑从gotest命令程序主体中分离出来并建立单独的源码文件。因为gotest命令中包含了编译动作,所以它可以接受可用于gobuild命令的所有标记。另外,它还有很多特有的标记。这些标记的用于控制命令本身的动作,有的用于控制和设置测试的过程和环境,还有的用于生成更详细的测试结果和统计信息。
可用于gotest命令的两个比较常用的标记是-i和标记-c。这两个就是用于控制gotest命令本身的动作的标记。详见下表。
表0-6gotest命令的标记说明
-c
生成用于运行测试的可执行文件,但不执行它。
-i
安装/重新安装运行测试所需的依赖包但不编译和运行测试代码。
上述这两个标记可以搭配使用。搭配使用的目的就是让gotest命令既安装依赖包又编译测试代码,但不运行测试。也就是说,让命令程序跑一遍运行测试之前的所有流程。这可以测试一下测试过程。需要注意的是,在加入-c标记后,命令程序在编译测试代码并生成用于运行测试的一系列文件之后会把临时工作目录及其下的所有内容一并删除。如果想在命令执行结束后再去查看这些内容的话,我们还需要加入-work标记。
除此之外,gotest命令还有很多功效各异的标记。但是由于这些标记的复杂性,我们需要结合测试源码文件进行详细的讲解。所以我们把这些内容放在了本章的第二节中。
golist命令的作用是列出指定的代码包的信息。与其他命令相同,我们需要以代码包导入路径的方式给定代码包。被给定的代码包可以有多个。这些代码包对应的目录中必须直接保存有Go语言源码文件,其子目录中的文件不算在内。否则,代码包将被看做是不完整的。现在我们来试用一下:
hc@ubt:~$golistcnet/ctcppkgtoolcnet/ctcppkgtool
我们看到,在不加任何标记的情况下,命令的结果信息中只包含了我们指定的代码包的导入路径。我们刚刚提到,作为参数的代码包必须是完整的代码包。例如:
hc@ubt:~$golistcnetpkgtoolcan'tloadpackage:packagecnet:noGosourcefilesin/home/hc/golang/goc2p/src/cnetpkgtool
这时,golist命令报告了一个错误——代码包cnet对应的目录下没有Go源码文件。但是命令还是把代码包pkgtool的导入路径打印出来了。然而,当我们在执行golist命令并加入标记-e时,即使参数中包含有不完整的代码包,命令也不会提示错误。示例如下:
hc@ubt:~$golist-ecnetpkgtoolcnetpkgtool
标记-e的作用是以容错模式加载和分析指定的代码包。在这种情况下,命令程序如果在加载或分析的过程中遇到错误只会在内部记录一下,而不会直接把错误信息打印出来。我们为了看到错误信息可以使用-json标记。这个标记的作用是把代码包的结构体实例用JSON的样式打印出来。
在了解了这些基本概念之后,我们来试用一下-json标记。示例如下:
hc@ubt:~$golist-e-jsoncnet{"Dir":"/home/hc/golang/goc2p/src/cnet","ImportPath":"cnet","Stale":true,"Root":"/home/hc/golang/goc2p","Incomplete":true,"Error":{"ImportStack":["cnet"],"Pos":"","Err":"noGosourcefilesin/home/hc/golang/goc2p/src/cnet"}}
在上述JSON格式的代码包信息中,对于结构体中的字段的显示是不完整的。因为命令程序认为我们指定cnet就是不完整的。在名为Error的字段中,我们可以看到具体说明。Error字段的内容其实也是一个结构体。在JSON格式下,这种嵌套的结构体被完美的展现了出来。Error字段所指代的结构体实例的Err字段说明了cnet不完整的原因。这与我们在没有使用-e标记的情况下所打印出来的错误提示信息是一致的。我们再来看Incomplete字段。它的值为true。这同样说明cnet是一个不完整的代码包。
实际上,在从这个代码包结构体实例到JSON格式文本的转换过程中,所有的值为其类型的空值的字段都已经被忽略了。
现在我们使用带-json标记的golist命令列出代码包cnet/ctcp的信息:
hc@ubt:~$golist-jsoncnet/ctcp{"Dir":"/home/freej/mybook/goc2p/src/cnet/ctcp","ImportPath":"cnet/ctcp","Name":"ctcp","Target":"/home/freej/mybook/goc2p/pkg/linux_386/cnet/ctcp.a","Stale":true,"Root":"/home/freej/mybook/goc2p","GoFiles":["base.go","tcp.go"],"Imports":["bufio","errors","logging","net","sync","time"],"Deps":["bufio","bytes","errors","fmt","io","log","logging","math","math/rand","net","os","reflect","runtime","runtime/cgo","sort","strconv","strings","sync","sync/atomic","syscall","time","unicode","unicode/utf8","unsafe"],"TestGoFiles":["tcp_test.go"],"TestImports":["bytes","fmt","net","strings","sync","testing","time"]}
由于cnet/ctcp包是一个完整有效的代码包,所以我们不使用-e标记也是没有问题的。在上面打印的cnet/ctcp包的信息中没有Incomplete字段。这是因为完整的代码包中的Incomplete字段的其类型的空值false。它已经在转换过程中被忽略掉了。另外,在cnet/ctcp包的信息中我们看到了很多其它的字段。现在我就来看看在Go命令程序中的代码包结构体都有哪些公开的字段。如下表。
表0-7代码包结构体中的基本字段
字段名称
字段类型
字段描述
Dir
字符串(string)
代码包对应的目录。
ImportPath
代码包的导入路径。
Name
代码包的名称。
Doc
代码包的文档字符串。
Target
代码包的安装路径。
Goroot
布尔(bool)
代码包是否在Go安装目录下。
Standard
代码包是否属于标准库的一部分。
Stale
代码包能否被```goinstall```命令安装。
Root
代码包所属的工作区或Go安装目录的路径。
表0-8代码包结构体中与源码文件有关的字段
GoFiles
字符串切片([]string)
Go源码文件的数组。不包含导入了代码包“C”的源码文件和测试源码文件。
CgoFiles
导入了代码包“C”的源码文件的数组。
IgnoredGoFiles
需要被编译器忽略的源码文件的数组。
CFiles
名称中有“.c”后缀的文件的数组。
HFiles
名称中有“.h”后缀的文件的数组。
SFiles
名称中有“.s”后缀的文件的数组。
SysoFiles
名称中有“.syso”后缀的文件的数组。这些文件需要被加入到归档文件中。
SwigFiles
名称中有“.swig”后缀的文件的数组。
SwigCXXFiles
名称中有“.swigcxx”后缀的文件的数组。
表0-9代码包结构体中与Cgo指令有关的字段
CgoCFLAGS
需要传递给C编译器的标记的数组。针对于Cgo。
CgoLDFLAGS
需要传递给链接器的标记的数组。针对于Cgo。
CgoPkgConfig
pkg-config的名称的数组。针对于Cgo。
表0-10代码包结构体中与依赖信息有关的字段
Imports
代码包中的源码文件显示导入的依赖包的导入路径的数组。
Deps
所有的依赖包(包括间接依赖)的导入路径的数组。
表0-11代码包结构体中与错误信息有关的字段
Incomplete
代码包是否是完整的,也即在载入或分析代码包及其依赖包时是否有错误发生。
Error
*PackageError类型
载入或分析代码包时发生的错误。
*PackageError类型的数组([]*PackageError)
载入或分析代码包的依赖包时发生的错误。
表0-12代码包结构体中与测试源码文件有关的字段
TestGoFiles
代码包中的测试源码文件的数组。
TestImports
代码包中的测试源码文件显示导入的依赖包的导入路径的数组。
XTestGoFiles
代码包中的外部测试源码文件的数组。
XTestImports
代码包中的外部测试源码文件显示导入的依赖包的导入路径的数组。
代码包结构体中定义的字段很多,但有些时候我们只需要查看其中的一些字段。那要怎么做呢?标记-f可以满足这个需求。比如这样:
hc@ubt:~$golist-f{{.ImportPath}}cnet/ctcpcnet/ctcp
实际上,-f标记的默认值就是{{.ImportPath}}。这也正是我们在使用不加任何标记的golist命令时依然能看到指定代码包的导入路径的原因了。
标记-f的值需要满足标准库的代码包````text/template中定义的语法。比如,{{.S}}代表根结构体的S字段的值。在golist命令的场景下,这个根结构体就是指定的代码包所对应的结构体。如果S字段的值也是一个结构体的话,那么{{.S.F}}就代表根结构体的S字段的值中的F字段的值。如果我们要查看cnet/ctcp包中的命令源码文件和库源码文件的列表,可以这样使用-f```标记:
hc@ubt:~$golist-f{{.GoFiles}}cnet/ctcp[base.gotcp.go]
如果我们想查看不完整的代码包cnet的错误提示信息,还可以这样:
hc@ubt:~$golist-e-f{{.Error.Err}}cnetnoGosourcefilesinD:\Kanbox\gitrepo\goc2p\src\cnet
我们还可以利用代码包text/template中定义的强大语法让golist命令输出定制化更高的代码包信息。比如:
hc@ubt:~$golist-e-f'Thepackage{{.ImportPath}}is{{if.Incomplete}}incomplete!{{else}}complete.{{end}}'cnetThepackagecnetisincomplete!hc@ubt:~$golist-f'Theimportsofpackage{{.ImportPath}}is[{{join.Imports","}}].'cnet/ctcpTheimportsofpackagecnet/ctcpis[bufio,errors,logging,net,sync,time].
另外,-tags标记也可以被golist接受。它与我们在讲gobuild命令时提到的-tags标记是一致的。读者可以查看代码包```go/build``的文档以了解细节。
golist命令很有用。它可以为我们提供指定代码包的更深层次的信息。这些信息往往是我们无法从源码文件中直观看到的。
命令gofix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。这里所说的版本即Go语言的版本。代码包的所有Go语言源码文件不包括其子代码包(如果有的话)中的文件。修正操作包括把对旧程序调用的代码更换为对新程序调用的代码、把旧的语法更换为新的语法,等等。
这个工具其实非常有用。在编程语言的升级和演进的过程中,难免会对过时的和不够优秀的语法及标准库进行改进。这样的改进对于编程语言的向后兼容性是个挑战。我们在前面提到过向后兼容这个词。简单来说,向后兼容性就是指新版本的编程语言程序能够正确识别和解析用该编程语言的旧版本编写的程序和软件,以及在新版本的编程语言的运行时环境中能够运行用该编程语言的旧版本编写的程序和软件。对于Go语言来说,语法的改变和标准库的变更都会使得用旧版本编写的程序无法在新版本环境中编译通过。这就等于破坏了Go语言的向后兼容性。对于一个编程语言、程序库或基础软件来说,向后兼容性是非常重要的。但有时候为了让软件更加优秀,软件的开发者或维护者不得不在向后兼容性上做出一些妥协。这是一个在多方利益之间进行权衡的结果。本小节所讲述的工具正是Go语言的创造者们为了不让这种妥协给语言使用者带来困扰和额外的工作量而编写的自动化修正工具。这也充分体现了Go语言的软件工程哲学。下面让我们来详细了解它们的使用方法和内部机理。
命令gofix其实是命令gotoolfix的简单封装。这甚至比gofmt命令对gofmt命令的封装更简单。像其它的Go命令一样,gofix命令会先对作为参数的代码包导入路径进行验证,以确保它是正确有效的。像在本小节开始处描述的那样,gofix命令会把有效代码包中的所有Go语言源码文件作为多个参数传递给gotoolfix命令。实际上,gofix命令本身不接受任何标记,它会把加入的所有标记都原样传递给gotoolfix命令。gotoolfix命令可接受的标记如下表。
表0-15gotoolfix命令的标记说明
<
tableclass="tabletable-borderedtable-stripedtable-condensed">标记名称标记描述-diff不将修正后的内容写入文件,而只打印修正前后的内容的对比信息到标准输出。-r只对目标源码文件做有限的修正操作。该标记的值即为允许的修正操作的名称。多个名称之间用英文半角逗号分隔。-force使用此标记后,即使源码文件中的代码已经与Go语言的最新版本相匹配了,也会强行执行指定的修正操作。该标记的值就是需要强行执行的修正操作的名称,多个名称之间用英文半角逗号分隔。
table>
在默认情况下,gotoolfix命令程序会在目标源码文件上执行所有的修正操作。多个修正操作的执行会按照每个修正操作中标示的操作建立日期以从早到晚的顺序进行。我们可以通过执行gotoolfix-来查看gotoolfix命令的使用说明以及当前支持的修正操作。
值得一提的是,上述的修正操作都是依靠Go语言的标准库代码包go及其子包中提供的功能来完成的。实际上,gotoolfix命令程序在执行修正操作之前,需要先将目标源码文件中的内容解析为一个抽象语法树实例。这一功能其实就是由代码包go/parser提供的。而在这个抽象语法树实例中的各个元素的结构体类型的定义以及检测、访问和修改它们的方法则由代码包go/ast提供。有兴趣的读者可以阅读这些代码包中的代码。这对于深入理解Go语言对代码的静态处理过程是非常有好处的。
回到正题。与gofmt命令相同,gotoolfix命令也有交互模式。我们同样可以通过执行不带任何参数的命令来进入到这个模式。但是与gofmt命令不同的是,我们在gotoolfix命令的交互模式中输入的代码必须是完整的,即必须要符合Go语言源码文件的代码组织形式。当我们输入了不完整的代码片段时,命令程序将显示错误提示信息并退出。示例如下:
hc@ubt:~$gotoolfix-r='netipv6zone'a:=&net.TCPAddr{ip4,8080}standardinput:1:1:expected'package',found'IDENT'a
相对于上面的示例,我们必须要这样输入源码才能获得正常的结果:
hc@ubt:~$gotoolfix-r='netipv6zone'packagemainimport("fmt""net")funcmain(){addr:=net.TCPAddr{"127.0.0.1",8080}fmt.Printf("TCPAddr:%s\n",addr)}standardinput:fixednetipv6zonepackagemainimport("fmt""net")funcmain(){addr:=net.TCPAddr{IP:"127.0.0.1",Port:8080}fmt.Printf("TCPAddr:%s\n",addr)}
上述示例的输出结果中有这样一行提示信息:“standardinput:fixednetipv6zone”。其中,“standardinput”表明源码是从标准输入而不是源码文件中获取的,而“fixednetipv6zone”则表示名为netipv6zone的修正操作发现输入的源码中有需要修正的地方,并且已经修正完毕。另外,我们还可以看到,输出结果中的代码已经经过了格式化。
命令govet是一个用于检查Go语言源码中静态错误的简单工具。与大多数Go命令一样,govet命令可以接受-n标记和-x标记。-n标记用于只打印流程中执行的命令而不真正执行它们。-n标记也用于打印流程中执行的命令,但不会取消这些命令的执行。示例如下:
hc@ubt:~$govet-npkgtool/usr/local/go/pkg/tool/linux_386/vetgolang/goc2p/src/pkgtool/envir.gogolang/goc2p/src/pkgtool/envir_test.gogolang/goc2p/src/pkgtool/fpath.gogolang/goc2p/src/pkgtool/ipath.gogolang/goc2p/src/pkgtool/pnode.gogolang/goc2p/src/pkgtool/util.gogolang/goc2p/src/pkgtool/util_test.go
govet命令的参数既可以是代码包的导入路径,也可以是Go语言源码文件的绝对路径或相对路径。但是,这两种参数不能混用。也就是说,govet命令的参数要么是一个或多个代码包导入路径,要么是一个或多个Go语言源码文件的路径。
govet命令是gotoolvet命令的简单封装。它会首先载入和分析指定的代码包,并把指定代码包中的所有Go语言源码文件和以“.s”结尾的文件的相对路径作为参数传递给gotoolvet命令。其中,以“.s”结尾的文件是汇编语言的源码文件。如果govet命令的参数是Go语言源码文件的路径,则会直接将这些参数传递给gotoolvet命令。
如果我们直接使用gotoolvet命令,则其参数可以传递任意目录的路径,或者任何Go语言源码文件和汇编语言源码文件的路径。路径可以是绝对的也可以是相对的。
gotoolvet命令的作用是检查Go语言源代码并且报告可疑的代码编写问题。比如,在调用Printf函数时没有传入格式化字符串,以及某些不标准的方法签名,等等。该命令使用试探性的手法检查错误,因此并不能保证报告的问题确实需要解决。但是,它确实能够找到一些编译器没有捕捉到的错误。
gotoolvet命令程序在被执行后会首先解析标记并检查标记值。gotoolvet命令支持的所有标记如下表。
表0-16gotoolvet命令的标记说明
-all
进行全部检查。如果有其他检查标记被设置,则命令程序会将此值变为false。默认值为true。
-asmdecl
对汇编语言的源码文件进行检查。默认值为false。
-assign
检查赋值语句。默认值为false。
-atomic
检查代码中对代码包sync/atomic的使用是否正确。默认值为false。
-buildtags
检查编译标签的有效性。默认值为false。
-composites
检查复合结构实例的初始化代码。默认值为false。
-compositeWhiteList
是否使用复合结构检查的白名单。仅供测试使用。默认值为true。
-methods
检查那些拥有标准命名的方法的签名。默认值为false。
-printf
检查代码中对打印函数的使用是否正确。默认值为false。
-printfuncs
需要检查的代码中使用的打印函数的名称的列表,多个函数名称之间用英文半角逗号分隔。默认值为空字符串。
-rangeloops
检查代码中对在```range```语句块中迭代赋值的变量的使用是否正确。默认值为false。
-structtags
检查结构体类型的字段的标签的格式是否标准。默认值为false。
-unreachable
查找并报告不可到达的代码。默认值为false。
在阅读上面表格中的内容之后,读者可能对这些标签的具体作用及其对命令程序检查步骤的具体影响还很模糊。不过没关系,我们下面就会对它们进行逐一的说明。
-all标记
如果标记-all有效(标记值不为false),那么命令程序会对目标文件进行所有已知的检查。实际上,标记-all的默认值就是true。也就是说,在执行gotoolvet命令且不加任何标记的情况下,命令程序会对目标文件进行全面的检查。但是,只要有一个另外的标记(-compositeWhiteList和-printfuncs这两个标记除外)有效,命令程序就会把标记-all设置为false,并只会进行与有效的标记对应的检查。
-assign标记
如果标记-assign有效(标记值不为false),则命令程序会对目标文件中的赋值语句进行自赋值操作检查。什么叫做自赋值呢?简单来说,就是将一个值或者实例赋值给它本身。像这样:
vars1string="S1"s1=s1//自赋值
或者
s1,s2:="S1","S2"s2,s1=s2,s1//自赋值
检查程序会同时遍历等号两边的变量或者值。在抽象语法树的语境中,它们都被叫做表达式节点。检查程序会检查等号两边对应的表达式是否相同。判断的依据是这两个表达式节点的字符串形式是否相同。在当前的场景下,这种相同意味着它们的变量名是相同的。如前面的示例。
file,err:=os.Open(wp)
很显然,这个赋值语句肯定不是自赋值语句。因此,不需要对此种情况进行检查。如果等号右边并不是对函数或方法调用的表达式,并且等号两边的表达式数量也不相等,那么势必会在编译时引发错误,也不必检查。
-atomic标记
如果标记-atomic有效(标记值不为false),则命令程序会对目标文件中的使用代码包sync/atomic进行原子赋值的语句进行检查。原子赋值语句像这样:
vari32int32i32=0newi32:=atomic.AddInt32(&i32,3)fmt.Printf("i32:%d,newi32:%d.\n",i32,newi32)
函数AddInt32会原子性的将变量i32的值加3,并返回这个新值。因此上面示例的打印结果是:
i32:3,newi32:3
i32=1i32=atomic.AddInt32(&i32,3)_,i32=5,atomic.AddInt32(&i32,3)i32,_=atomic.AddInt32(&i32,1),5
上面示例中的后三行赋值语句都属于原子赋值语句,但它们都破坏了原子赋值的原子性。以第二行的赋值语句为例,等号左边的atomic.AddInt32(&i32,3)的作用是原子性的将变量i32的值增加3。但该语句又将函数的结果值赋值给变量i32,这个二次赋值属于对变量i32的重复赋值,也使原本拥有原子性的赋值操作被拆分为了两个步骤的非原子操作。如果在对变量i32的第一次原子赋值和第二次非原子的重复赋值之间又有另一个程序对变量i32进行了原子赋值,那么当前程序中的这个第二次赋值就破坏了那两次原子赋值本应有的顺序性。因为,在另一个程序对变量i32进行原子赋值后,当前程序中的第二次赋值又将变量i32的值设置回了之前的值。这显然是不对的。所以,上面示例中的第二行代码应该改为:
atomic.AddInt32(&i32,3)
并且,对第三行和第四行的代码也应该有类似的修改。检查程序如果在目标文件中查找到像上面示例的第二、三、四行那样的语句,就会打印出相应的错误信息。
另外,上面所说的导致原子性被破坏的重复赋值语句还有一些类似的形式。比如:
i32p:=&i32*i32p=atomic.AddUint64(i32p,1)
这与之前的示例中的代码的含义几乎是一样。另外还有:
varcounterstruct{Nuint32}counter.N=atomic.AddUint64(&counter.N,1)
和
ns:=[]uint32{10,20}ns[0]=atomic.AddUint32(&ns[0],1)nps:=[]*uint32{&ns[0],&ns[1]}*nps[0]=atomic.AddUint32(nps[0],1)
在最近的这两个示例中,虽然破坏原子性的重复赋值操作因结构体类型或者数组类型的介入显得并不那么直观了,但依然会被检查程序发现并及时打印错误信息。
顺便提一句,对于原子赋值语句和普通赋值语句,检查程序都会忽略掉对等号两边的表达式的个数不相等的赋值语句的检查。
-buildtags标记
如果一个在文件头部的单行注释中的编译标签通过了上述的这些检查,则说明它的格式是正确无误的。由于只有在文件头部的单行注释中编译标签才会被编译器认可,所以检查程序只会查找和检查源码文件中的第一个多行注释或代码行之前的内容。
-composites标记和-compositeWhiteList标记
如果标记-composites有效(标记值不为false),则命令程序会对目标文件中的复合字面量进行检查。请看如下示例:
typecounterstruct{namestringnumberint}...c:=counter{name:"c1",number:0}
在上面的示例中,代码counter{name:"c1",number:0}是对结构体类型counter的初始化。如果复合字面量中涉及到的类型不在当前代码包内部且未在所属文件中被导入,那么检查程序不但会打印错误信息还会将退出代码设置为1,并且取消后续的检查。退出代码为1意味着检查程序已经报告了一个或多个问题。这个问题比仅仅引起错误信息报告的问题更加严重。
在通过上述检查的前提下,如果复合字面量中包含了对结构体类型的字段的赋值但却没有指明字段名,像这样:
varv=flag.Flag{"Name","Usage",nil,//Value"DefValue",}
那么检查程序也会打印错误信息,以提示在复合字面量中包含有未指明的字段赋值。
这有一个例外,那就是当标记-compositeWhiteList有效(标记值不为false)的时候。只要类型在白名单中,即使其初始化语句中含有未指明的字段赋值也不会被提示。这是出于什么考虑呢?先来看下面的示例:
typesliceType[]string...st1:=sliceType{"1","2","3"}
上面示例中的sliceType{"1","2","3"}也属于复合字面量。但是它初始化的类型实际上是一个切片值,只不过这个切片值被别名化并被包装为了另一个类型而已。在这种情况下,复合字面量中的赋值不需要指明字段,事实上这样的类型也不包含任何字段。白名单中所包含的类型都是这种情况。它们是在标准库中的包装了切片值的类型。它们不需要被检查,因为这种情况是合理的。
在默认情况下,标记-compositeWhiteList是有效的。也就是说,检查程序不会对它们的初始化代码进行检查,除非我们在执行gotoolvet命令时显示的将-compositeWhiteList标记的值设置为false。
-methods标记
如果标记-methods有效(标记值不为false),则命令程序会对目标文件中的方法定义进行规范性的进行检查。这里所说的规范性是狭义的。
typeMySeekerstruct{//忽略字段定义}func(self*MySeeker)Seek(whenceint,offsetint64)(retint64,errerror){//想实现接口类型io.Seeker中的唯一方法,但是却把参数的顺序写颠倒了。//忽略实现代码}funcNewMySeekerio.Seeker{return&MySeeker{/*忽略字段初始化*/}//这里会引发一个运行时错误。//由于MySeeker的Seek方法的签名写错了,所以MySeeker不是io.Seeker的实现。}
这种运行时错误看起来会比较诡异,并且错误排查也会相对困难,所以应该尽量避免。-methods标记所对应的检查就是为了达到这个目的。检查程序在发现目标文件中某个方法的名字被包含在规范化方法字典中但其签名与对应的描述不对应的时候,就会打印错误信息并设置退出代码为1。
我在这里附上在规范化方法字典中列出的方法的信息:
表0-17规范化方法字典中列出的方法
方法名称
参数类型
结果类型
所属接口
唯一方法
Format
"fmt.State","rune"
<无>
fmt.Formatter
是
GobDecode
"[]byte"
"error"
gob.GobDecoder
GobEncode
"[]byte","error"
gob.GobEncoder
MarshalJSON
json.Marshaler
Peek
"int"
image.reader
否
ReadByte
io.ByteReader
ReadFrom
"io.Reader"
"int64","error"
io.ReaderFrom
ReadRune
"rune","int","error"
io.RuneReader
Scan
"fmt.ScanState","rune"
fmt.Scanner
Seek
"int64","int"
io.Seeker
UnmarshalJSON
json.Unmarshaler
UnreadByte
io.ByteScanner
UnreadRune
io.RuneScanner
WriteByte
"byte"
io.ByteWriter
WriteTo
"io.Writer"
io.WriterTo
-printf标记和-printfuncs标记
标记-printf旨在目标文件中检查各种打印函数使用的正确性。而标记-printfuncs及其值则用于明确指出需要检查的打印函数。-printfuncs标记的默认值为空字符串。也就是说,若不明确指出检查目标则检查所有打印函数。可被检查的打印函数如下表:
表0-18格式化字符串中动词的格式要求
函数全小写名称
支持格式化
可自定义输出
自带换行
error
fatal
fprint
fprintln
panic
panicln
println
sprint
sprintln
errorf
fatalf
fprintf
panicf
printf
sprintf
以字符串格式化功能来区分,打印函数可以分为可打印格式化字符串的打印函数(以下简称格式化打印函数)和非格式化打印函数。对于格式化打印函数来说,其第一个参数必是格式化表达式,也可被称为模板字符串。而其余参数应该为需要被填入模板字符串的变量。像这样:
fmt.Printf("Hello,%s!\n","Harry")//会输出:Hello,Harry!
而非格式化打印函数的参数则是一个或多个要打印的内容。比如:
fmt.Println("Hello,","Harry!")//会输出:Hello,Harry!
以指定输出目的地功能区分,打印函数可以被分为可自定义输出目的地的的打印函数(以下简称自定义输出打印函数)和标准输出打印函数。对于自定义输出打印函数来说,其第一个函数必是其打印的输出目的地。比如:
fmt.Fprintf(os.Stdout,"Hello,%s!\n","Harry")//会在标准输出设备上输出:Hello,Harry!
上面示例中的函数fmt.Fprintf既能够让我们自定义打印的输出目的地,又能够格式化字符串。此类打印函数的第一个参数的类型应为io.Writer接口类型。只要某个类型实现了该接口类型中的所有方法,就可以作为函数Fprintf的第一个参数。例如,我们还可以使用代码包bytes中的结构体Buffer来接收打印函数打印的内容。像这样:
varbuffbytes.Bufferfmt.Fprintf(&buff,"Hello,%s!\n","Harry")fmt.Print("Buffercontent:",buff.String())//会在标准输出设备上输出:Buffercontent:Hello,Harry!
而标准输出打印函数则只能将打印内容到标准输出设备上。就像函数fmt.Printf和fmt.Println所做的那样。
对于格式化打印函数,检查程序会进行如下检查:
fmt.Printf("Hello,%s!\n","Harry")
在这个示例中,格式化字符串中的“%s”就是我们所说的动词,“%”就是动词的前导符。它相当于一个需要被填的空。一般情况下,在格式化字符串中被填的空的数量应该与后续参数的数量相同。但是可以出现在格式化字符串中没有动词并且在格式化字符串之后没有额外参数的情况。在这种情况下,该格式化打印函数就相当于一个非格式化打印函数。例如,下面这个语句会导致此步检查不通过:
fmt.Printf("Hello!\n","Harry")
表0-19格式化字符串中动词的格式要求
动词
合法的附加标记
允许的参数类型
简要说明
b
“”,“-”,“+”,“.”和“0”
int或float
用于二进制表示法。
c
“-”
rune或int
用于单个字符的Unicode表示法。
d
int
用于十进制表示法。
e
float
用于科学记数法。
E
f
用于控制浮点数精度。
F
g
用于压缩浮点数输出。
G
用于动态选择浮点数输出格式。
o
“”,“-”,“+”,“.”,“0”和“#”
用于八进制表示法。
p
“-”和“#”
pointer
用于表示指针地址。
q
rune,int或string
用于生成带双引号的字符串形式的内容。
s
用于生成字符串形式的内容。
t
bool
用于生成与布尔类型对应的字符串值。(“true”或“false”)
T
任何类型
用于用Go语法表示任何值的类型。
U
用于针对Unicode的表示法。
v
以默认格式格式化任何值。
x
以十六进制、全小写的形式格式化每个字节。
X
以十六进制、全大写的形式格式化每个字节。
对于非格式化打印函数,检查程序会进行如下检查:
fmt.Println("Hello!\n")
常常是由于程序编写人员的笔误。实际上,事实确实如此。如果我们确实想连续输入多个换行,应该这样写:
fmt.Println("Hello!")fmt.Println()
至此,我们详细介绍了gotoolvet命令中的检查程序对打印函数的所有步骤和内容。打印函数的功能非常简单,但是gotoolvet命令对它的检查却很细致。从中我们可以领会到一些关于打印函数的最佳实践。
-rangeloops标记
如果标记-rangeloop有效(标记值不为false),那么命令程序会对使用range进行迭代的for代码块进行检查。我们之前提到过,使用for语句需要注意两点:
mySlice:=[]string{"A","B","C"}forindex,value:=rangemySlice{gofunc(){fmt.Printf("Index:%d,Value:%s\n",index,value)}()}
在Go语言的并发编程模型中,并没有线程的概念,但却有一个特有的概念——Goroutine。Goroutine也可被称为Go例程或简称为Go程。关于Goroutine的详细介绍在第6章和第7章。我们现在只需要知道它是一个可以被并发执行的代码块。
myDict:=make(map[string]int)myDict["A"]=1myDict["B"]=2myDict["C"]=3forkey,value:=rangemyDict{deferfunc(){fmt.Printf("Key:%s,Value:%d\n",key,value)}()}
另一方面,当检查程序发现在带有range子句的for代码块中迭代出的数据并没有赋值给标识符所代表的变量时,则会忽略对这一代码块的检查。比如像这样的代码:
funcnonIdentRange(slc[]string){l:=len(slc)temp:=make([]string,l)l--for_,temp[l]=rangeslc{//忽略了使用切片值temp的代码。ifl>0{l--}}}
据此,我们知道如果在可能被延迟处理的代码块中直接使用迭代中的临时变量,那么就可能会造成与编程人员意图不相符的结果。如果由此问题使程序的最终结果出现偏差甚至使程序报错的话,那么看起来就会非常诡异。这种隐晦的错误在排查时也是非常困难的。这种不正确的代码编写方式应该彻底被避免。这也是检查程序对迭代代码块进行检查的最终目的。如果检查程序发现了上述的不正确的代码编写方式,就会打印出错误信息以提醒编程人员。
-structtags标记
如果标记``-structtags有效(标记值不为false```),那么命令程序会对结构体类型的字段的标签进行检查。我们先来看下面的代码:
typePersonstruct{XMLNamexml.Name`xml:"person"`Idint`xml:"id,attr"`FirstNamestring`xml:"name>first"`LastNamestring`xml:"name>last"`Ageint`xml:"age"`Heightfloat32`xml:"height,omitempty"`MarriedboolAddressCommentstring`xml:",comment"`}
严格来讲,结构体类型的字段的标签应该满足如下要求:
检查程序首先会对结构体类型的字段标签的内容做去引号处理,也就是把最外面的双引号或者反引号去除。如果去除失败,则检查程序会打印错误信息并设置退出代码为1,同时忽略后续检查。如果去引号处理成功,检查程序则会根据前面的规则对标签的内容进行检查。如果检查出问题,检查程序同样会打印出错误信息并设置退出代码为1。
-unreachable标记
如果标记``-unreachable有效(标记值不为false```),那么命令程序会在函数或方法定义中查找死代码。死代码就是永远不会被访问到的代码。例如:
funcdeadCode1()int{print(1)return2println()//这里存在死代码}
在上面示例中,函数deadCode1中的最后一行调用打印函数的语句就是死代码。检查程序如果在函数或方法中找到死代码,则会打印错误信息以提醒编码人员。我们把这段代码放到命令源码文件deadcode_demo.go中,并在main函数中调用它。现在,如果我们编译这个命令源码文件会马上看到一个编译错误:“missingreturnatendoffunction”。显然,这个错误侧面的提醒了我们,在这个函数中存在死代码。实际上,我们在修正这个问题之前它根本就不可能被运行,所以也就不存在任何隐患。但是,如果在这个函数不需要结果的情况下又会如何呢?我们稍微改造一下上面这个函数:
funcdeadCode1(){print(1)returnprintln()//这里存在死代码}
hc@ubt:~$gotoolvetdeadcode_demo.godeadcode_demo.go:10:unreachablecode
gotoolvet命令中的检查程序对于死代码的判定有几个依据,如下:
funcdeadCode2(){print(1)panic(2)println()//这里存在死代码}
或
funcdeadCode3(){L:{print(1)gotoL}println()//这里存在死代码}
funcdeadCode4(){print(1)return{//这里存在死代码}}
则后面的语句或代码块就会被判定为死代码。但检查程序仅会在错误提示信息中包含第一行死代码的位置。
funcdeadCode5(xint){print(1)ifx==1{panic(2)}else{return}println()//这里存在死代码}
注意,只要其中一个分支不包含流程中断语句,就不能判定后面的代码为死代码。像这样:
funcdeadCode5(xint){print(1)ifx==1{panic(2)}elseifx==2{return}println()//这里并不是死代码}
funcdeadCode6(){for{for{break}}println()//这里存在死代码}
funcdeadCode7(){for{for{}break//这里存在死代码}println()}
而我们对这两个函数稍加改造后,就会消除gotoolvet命令发出的死代码告警。如下:
funcdeadCode6(){x:=1forx==1{for{break}}println()//这里存在死代码}
以及
funcdeadCode7(){x:=1for{forx==1{}break//这里存在死代码}println()}
我们只是加了一个显式的中断条件就能够使之通过死代码检查。但是,请注意!这两个函数中在被改造后仍然都包含死循环代码!这说明检查程序并不对中断条件的逻辑进行检查。
funcdeadCode8(cchanint){print(1)select{case<-c:print(2)panic(3)}println()//这里存在死代码}
funcdeadCode9(cchanint){L:print(1)select{case<-c:print(2)panic(3)casec<-1:print(4)gotoL}println()//这里存在死代码}
另外,在空的select语句块之后的代码也会被认为是死代码。比如:
funcdeadCode10(){print(1)select{}println()//这里存在死代码}
funcdeadCode11(cchanint){print(1)select{case<-c:print(2)panic(3)default:select{}}println()//这里存在死代码}
上面这两个示例中的语句select{}都会引发一个运行时错误:“fatalerror:allgoroutinesareasleep-deadlock!”。这就是死锁!关于这个错误的详细说明在第7章。
funcdeadCode14(xint){print(1)switchx{case1:print(2)panic(3)default:return}println(4)//这里存在死代码}
我们知道,关键字fallthrough可以使流程从switch代码块中的一个case转移到下一个case或defaultcase。死代码也可能由此产生。例如:
funcdeadCode15(xint){print(1)switchx{case1:print(2)fallthroughdefault:return}println(3)//这里存在死代码}
在上面的示例中,第一个case总会把流程转移到第二个case,而第二个case中的最后一条语句为return语句,所以流程永远不会转移到语句println(3)上。因此,println(3)语句会被判定为死代码。如果我们把fallthrough语句去掉,那么就可以消除这个死代码判定。实际上,只要某一个case或者defaultcase中的最后一条语句是break语句,就不会有死代码的存在。当然,这个break语句本身不能是死代码。另外,与select代码块不同的是,空的switch代码块并不会使它后面的代码成为死代码。
综上所述,死代码的判定虽然看似比较复杂,但其实还是有原则可循的。我们应该在编码过程中就避免编写可能会造成死代码的代码。如果我们实在不确定死代码是否存在,也可以使用gotoolvet命令来检查。不过,需要提醒读者的是,不存在死代码并不意味着不存在造成死循环的代码。当然,造成死循环的代码也并不一定就是错误的代码。但我们仍然需要对此保持警觉。
-asmdecl标记
如果标记``-asmdecl有效(标记值不为false```),那么命令程序会对汇编语言的源码文件进行检查。对汇编语言源码文件及相应编写规则的解读已经超出了本书的范围,所以我们并不在这里对此项检查进行描述。如果读者有兴趣的话,可以查看此项检查的程序的源码文件asmdecl.go。它在Go语言安装目录的子目录src/cmd/vet下。
至此,我们对govet命令和gotoolvet命令进行了全面详细的介绍。之所以花费如此大的篇幅来介绍这两个命令,不仅仅是为了介绍此命令的使用方法,更是因为此命令程序的检查工作涉及到了很多我们在编写Go语言代码时需要避免的“坑”。由此我们也可以知晓应该怎样正确的编写Go语言代码。同时,我们也应该在开发Go语言程序的过程中经常使用gotoolvet命来检查代码。
我们可以使用gotoolpprof命令来交互式的访问概要文件的内容。命令将会分析指定的概要文件,并会根据我们的要求为我们提供高可读性的输出信息。
在Go语言中,我们可以通过标准库的代码包runtime和runtime/pprof中的程序来生成三种包含实时性数据的概要文件,分别是CPU概要文件、内存概要文件和程序阻塞概要文件。下面我们先来分别介绍用于生成这三种概要文件的API的用法。
CPU概要文件
在介绍CPU概要文件的生成方法之前,我们先来简单了解一下CPU主频。CPU的主频,即CPU内核工作的时钟频率(CPUClockSpeed)。CPU的主频的基本单位是赫兹(Hz),但更多的是以兆赫兹(MHz)或吉赫兹(GHz)为单位。时钟频率的倒数即为时钟周期。时钟周期的基本单位为秒(s),但更多的是以毫秒(ms)、微妙(us)或纳秒(ns)为单位。在一个时钟周期内,CPU执行一条运算指令。也就是说,在1000Hz的CPU主频下,每1毫秒可以执行一条CPU运算指令。在1MHz的CPU主频下,每1微妙可以执行一条CPU运算指令。而在1GHz的CPU主频下,每1纳秒可以执行一条CPU运算指令。
funcstartCPUProfile(){if*cpuProfile!=""{f,err:=os.Create(*cpuProfile)iferr!=nil{fmt.Fprintf(os.Stderr,"Cannotcreatecpuprofileoutputfile:%s",err)return}iferr:=pprof.StartCPUProfile(f);err!=nil{fmt.Fprintf(os.Stderr,"Cannotstartcpuprofile:%s",err)f.Close()return}}}
在函数startCPUProfile中,我们首先创建了一个用于存放CPU使用情况记录的文件。这个文件就是CPU概要文件,其绝对路径由*cpuProfile的值表示。然后,我们把这个文件的实例作为参数传入到函数```pprof.StartCPUProfile``中。如果此函数没有返回错误,就说明记录操作已经开始。需要注意的是,只有CPU概要文件的绝对路径有效时此函数才会开启记录操作。
如果我们想要在某一时刻停止CPU使用情况记录操作,就需要调用下面这个函数:
funcstopCPUProfile(){if*cpuProfile!=""{pprof.StopCPUProfile()//把记录的概要信息写到已指定的文件}}
在这个函数中,并没有代码用于CPU概要文件写入操作。实际上,在启动CPU使用情况记录操作之后,运行时系统就会以每秒100次的频率将取样数据写入到CPU概要文件中。pprof.StopCPUProfile函数通过把CPU使用情况取样的频率设置为0来停止取样操作。并且,只有当所有CPU使用情况记录都被写入到CPU概要文件之后,pprof.StopCPUProfile函数才会退出。从而保证了CPU概要文件的完整性。
内存概要文件
内存概要文件用于保存在用户程序执行期间的内存使用情况。这里所说的内存使用情况,其实就是程序运行过程中堆内存的分配情况。Go语言运行时系统会对用户程序运行期间的所有的堆内存分配进行记录。不论在取样的那一时刻、堆内存已用字节数是否有增长,只要有字节被分配且数量足够,分析器就会对其进行取样。开启内存使用情况记录的方式如下:
funcstartMemProfile(){if*memProfile!=""&&*memProfileRate>0{runtime.MemProfileRate=*memProfileRate}}
我们可以看到,开启内存使用情况记录的方式非常简单。在函数startMemProfile中,只有在*memProfile和*memProfileRate的值有效时才会进行后续操作。*memProfile的含义是内存概要文件的绝对路径。*memProfileRate的含义是分析器的取样间隔,单位是字节。当我们将这个值赋给int类型的变量runtime.MemProfileRate时,就意味着分析器将会在每分配指定的字节数量后对内存使用情况进行取样。实际上,即使我们不给runtime.MemProfileRate变量赋值,内存使用情况的取样操作也会照样进行。此取样操作会从用户程序开始时启动,且一直持续进行到用户程序结束。runtime.MemProfileRate变量的默认值是512*1024,即512K个字节。只有当我们显式的将0赋给runtime.MemProfileRate变量之后,才会取消取样操作。
在默认情况下,内存使用情况的取样数据只会被保存在运行时内存中,而保存到文件的操作只能由我们自己来完成。请看如下代码:
funcstopMemProfile(){if*memProfile!=""{f,err:=os.Create(*memProfile)iferr!=nil{fmt.Fprintf(os.Stderr,"Cannotcreatememprofileoutputfile:%s",err)return}iferr=pprof.WriteHeapProfile(f);err!=nil{fmt.Fprintf(os.Stderr,"Cannotwrite%s:%s",*memProfile,err)}f.Close()}}
从函数名称上看,stopMemProfile函数的功能是停止对内存使用情况的取样操作。但是,它只做了将取样数据保存到内存概要文件的操作。在stopMemProfile函数中,我们调用了函数pprof.WriteHeapProfile,并把代表内存概要文件的文件实例作为了参数。如果pprof.WriteHeapProfile函数没有返回错误,就说明数据已被写入到了内存概要文件中。
需要注意的是,对内存使用情况进行取样的程序会假定取样间隔在用户程序的运行期间内都是一成不变的,并且等于runtime.MemProfileRate变量的当前值。因此,我们应该在我们的程序中只改变内存取样间隔一次,且应尽早改变。比如,在命令源码文件的main函数的开始处就改变它。
程序阻塞概要文件
程序阻塞概要文件用于保存用户程序中的Goroutine阻塞事件的记录。我们来看开启这项操作的方法:
funcstartBlockProfile(){if*blockProfile!=""&&*blockProfileRate>0{runtime.SetBlockProfileRate(*blockProfileRate)}}
与开启内存使用情况记录的方式类似,在函数startBlockProfile中,当*blockProfile和*blockProfileRate的值有效时,我们会设置对Goroutine阻塞事件的取样间隔。*blockProfile的含义为程序阻塞概要文件的绝对路径。*blockProfileRate的含义是分析器的取样间隔,单位是次。函数runtime.SetBlockProfileRate的唯一参数是int类型的。它的含义是分析器会在每发生几次Goroutine阻塞事件时对这些事件进行取样。如果我们不显式的使用runtime.SetBlockProfileRate函数设置取样间隔,那么取样间隔就为1。也就是说,在默认情况下,每发生一次Goroutine阻塞事件,分析器就会取样一次。与内存使用情况记录一样,运行时系统对Goroutine阻塞事件的取样操作也会贯穿于用户程序的整个运行期。但是,如果我们通过runtime.SetBlockProfileRate函数将这个取样间隔设置为0或者负数,那么这个取样操作就会被取消。
我们在程序结束之前可以将被保存在运行时内存中的Goroutine阻塞事件记录存放到指定的文件中。代码如下:
funcstopBlockProfile(){if*blockProfile!=""&&*blockProfileRate>=0{f,err:=os.Create(*blockProfile)iferr!=nil{fmt.Fprintf(os.Stderr,"Cannotcreateblockprofileoutputfile:%s",err)return}iferr=pprof.Lookup("block").WriteTo(f,0);err!=nil{fmt.Fprintf(os.Stderr,"Cannotwrite%s:%s",*blockProfile,err)}f.Close()}}
在创建程序阻塞概要文件之后,stopBlockProfile函数会先通过函数pprof.Lookup将保存在运行时内存中的内存使用情况记录取出,并在记录的实例上调用WriteTo方法将记录写入到文件中。
更多的概要文件
我们可以通过pprof.Lookup函数取出更多种类的取样记录。如下表:
表0-20可从pprof.Lookup函数中取出的记录
取样频率
goroutine
活跃Goroutine的信息的记录。
仅在获取时取样一次。
threadcreate
系统线程创建情况的记录。
heap
堆内存分配情况的记录。
默认每分配512K字节时取样一次。
block
Goroutine阻塞事件的记录。
默认每发生一次阻塞事件时取样一次。
在上表中,前两种记录均为一次取样的记录,具有即时性。而后两种记录均为多次取样的记录,具有实时性。实际上,后两种记录“heap”和“block”正是我们前面讲到的内存使用情况记录和程序阻塞情况记录。
我们可以通过下面这个函数分别将四种记录输出到文件。
funcSaveProfile(workDirstring,profileNamestring,ptypeProfileType,debugint){absWorkDir:=getAbsFilePath(workDir)ifprofileName==""{profileName=string(ptype)}profilePath:=filepath.Join(absWorkDir,profileName)f,err:=os.Create(profilePath)iferr!=nil{fmt.Fprintf(os.Stderr,"Cannotcreateprofileoutputfile:%s",err)return}iferr=pprof.Lookup(string(ptype)).WriteTo(f,debug);err!=nil{fmt.Fprintf(os.Stderr,"Cannotwrite%s:%s",profilePath,err)}f.Close()}
函数SaveProfile有四个参数。第一个参数是概要文件的存放目录。第二个参数是概要文件的名称。第三个参数是概要文件的类型。其中,类型ProfileType只是为string类型起的一个别名而已。这样是为了对它的值进行限制。它的值必须为“goroutine”、“threadcreate”、“heap”或“block”中的一个。我们现在来重点说一下第四个参数。参数debug控制着概要文件中信息的详细程度。这个参数也就是传给结构体pprof.Profile的指针方法WriteTo的第二个参数。而pprof.Profile结构体的实例的指针由函数pprof.Lookup产生。下面我们看看参数debug的值与写入概要文件的记录项内容的关系。
表0-21参数debug对概要文件内容的影响
记录\debug
小于等于0
等于1
大于等于2
为每个记录项提供调用栈中各项的以十六进制表示的内存地址。
在左边提供的信息的基础上,为每个记录项的调用栈中各项提供与内存地址对应的带代码包导入路径的函数名和源码文件路径及源码所在行号。
以高可读的方式提供各活跃Goroutine的状态信息和调用栈信息。
同上。
同左。
在左边提供的信息的基础上,为每个记录项的调用栈中各项提供与内存地址对应的带代码包导入路径的函数名和源码文件路径及源码所在行,并提供内存状态信息。
从上表可知,当debug的值小于等于0时,运行时系统仅会将每个记录项中的基本信息写入到概要文件中。记录项的基本信息只包括其调用栈中各项的以十六进制表示的内存地址。debug的值越大,我们能从概要文件中获取的信息越多。但是,gotoolpprof命令会无视那些除基本信息以外的附加信息。实际上,运行时系统在向概要文件中写入附加信息时会在最左边加入“#”,以表示当前行为注释行。也正因为有了这个前缀,gotoolpprof命令才会略过对这些附加信息的解析。这其中有一个例外,那就是当debug大于等于2时,Goroutine记录并不是在基本信息的基础上附加信息,而是完全以高可读的方式写入各活跃Goroutine的状态信息和调用栈信息。并且,在所有行的最左边都没有前缀“#”。显然,这个概要文件是无法被gotoolpprof命令解析的。但是它对于我们来说会更加直观和有用。
至此,我们已经介绍了使用标准库代码包runtime和runtime/pprof中的程序生成概要文件的全部方法。在上面示例中的所有代码都被保存在goc2p项目的代码包basic/prof中。代码包basic/prof中的这些程序非常易于使用。不过由于Go语言目前没有类似停机钩子(ShutdownHook)的API(应用程序接口),所以代码包basic/prof中的程序目前只能以侵入式的方式被使用。
pprof工具
我们在上一小节中提到过,任何以gotool开头的Go命令内部指向的特殊工具都被保存在目录$GOROOT/pkg/tool/$GOOS_$GOARCH/中。我们把这个目录叫做Go工具目录。与其他特殊工具不同的是,pprof工具并不是用Go语言编写的,而是由Perl语言编写的。(Perl是一种通用的、动态的解释型编程语言)与Go语言不同,Perl语言可以直接读取源码并运行。正因为如此,pprof工具的源码文件被直接保存在了Go工具目录下。而对于其它Go工具,存在此目录的都是经过编译而生成的可执行文件。我们可以直接用任意一种文本查看工具打开在Go工具目录下的pprof工具的源码文件pprof。实际上,这个源码文件拷贝自Google公司发起的开源项目gperftools。此项目中包含了很多有用的工具。这些工具可以帮助开发者创建更健壮的应用程序。pprof就是其中的一个非常有用的工具。
表0-22对代码包basic/prof的API有效的标记
-cpuprofile
指定CPU概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。
-blockprofile
指定程序阻塞概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。
-blockprofilerate
定义其值为n。此标记指定每发生n次Goroutine阻塞事件时,进行一次取样操作。
-memprofile
指定内存概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。
-memprofilerate
定义其值为n。此标记指定每分配n个字节的堆内存时,进行一次取样操作。
下面我们使用gorun命令运行改造后的命令源码文件showpds.go。示例如下:
hc@ubt:~/golang/goc2p$mkdirpprofhc@ubt:~/golang/goc2p$cdhelper/pdshc@ubt:~/golang/goc2p/helper/pds$gorunshowpds.go-p="runtime"cpuprofile="../../../pprof/cpu.out"-blockprofile="../../../pprof/block.out"-blockprofilerate=1-memprofile="../../../pprof/mem.out"-memprofilerate=10Thepackagenodeof'runtime':{/usr/local/go/src/pkg/runtime[][]false}Thedependencystructureofpackage'runtime':runtime->unsafe
在上面的示例中,我们使用了所有的对代码包basic/prof的API有效的标记。另外,标记-p是对命令源码文件showpds.go有效的。其含义是指定要解析依赖关系的代码包的导入路径。
现在我们来查看一下goc2p项目目录下的pprof子目录:
hc@ubt:~/golang/goc2p/helper/pds$ls../../../pprofblock.outcpu.outmem.out
这个目录中的三个文件分别对应了三种包含实时性数据的概要文件。这也证明了我们对命令源码文件showpds.go的改造是有效的。
好了,一切准备工作就绪。现在,我们就来看看gotoolpprof命令都能做什么。首先,我们来编译命令源码文件showpds.go。
hc@ubt:~/golang/goc2p/helper/pds$gobuildshowpds.gohc@ubt:~/golang/goc2p/helper/pds$lsshowpdsshowpds.go
现在我们就以可执行文件showpds和pprof目录下的CPU概要文件cpu.out作为参数来执行gotoolpprof命令。实际上,我们通过gotoolpprof命令进入的就是pprof工具的交互模式的界面。
hc@ubt:~/golang/goc2p/helper/pds$gotoolpprofshowpds../../../pprof/cpu.outWelcometopprof!Forhelp,type'help'.(pprof)
我们可以在提示符“(pprof)”后面输入一些命令来查看概要文件。pprof工具在交互模式下支持的命令如下表。
表0-23pprof工具在交互模式下支持的命令
参数
标签
gv
[focus]
将当前概要文件以图形化和层次化的形式显示出来。当没有任何参数时,在概要文件中的所有抽样都会被显示。如果指定了focus参数,则只显示调用栈中有名称与此参数相匹配的函数或方法的抽样。focus参数应该是一个正则表达式。
web
与gv命令类似,web命令也会用图形化的方式来显示概要文件。但不同的是,web命令是在一个Web浏览器中显示它。如果你的Web浏览器已经启动,那么它的显示速度会非常快。如果想改变所使用的Web浏览器,可以在Linux下设置符号链接/etc/alternatives/gnome-www-browser或/etc/alternatives/x-www-browser,或在OSX下改变SVG文件的关联Finder。
list
[routine_regexp]
weblist
在Web浏览器中显示与list命令的输出相同的内容。它与list命令相比的优势是,在我们点击某行源码时还可以显示相应的汇编代码。
top[N]
[--cum]
disasm
显示名称与参数“routine_regexp”相匹配的函数或方法的反汇编代码。并且,在显示的内容中还会标注有相应的取样计数。
callgrind
[filename]
利用callgrind工具生成统计文件。在这个文件中,说明了程序中函数的调用情况。如果未指定“filename”参数,则直接调用kcachegrind工具。kcachegrind可以以可视化的方式查看callgrind工具生成的统计文件。
help
显示帮助信息。
quit
退出gotoolpprof命令。Ctrl-d也可以达到同样效果。
在上面表格中的绝大多数命令(除了help和quit)都可以在其所有参数和标签后追加一个或多个参数,以表示想要忽略显示的函数或方法的名称。我们需要在此类参数上加入减号“-”作为前缀,并且多个参数之间需要以空格分隔。当然,我们也可以用正则表达式替代函数或方法的名称。追加这些约束之后,任何调用栈中包含名称与这类参数相匹配的函数或方法的抽样都不会出现在命令的输出内容中。下面我们对这几个命令进行逐一说明。
gv命令
对于命令gv的用法,请看如下示例:
hc@ubt:~/golang/goc2p/helper/pds$gotoolpprofshowpds../../../pprof/cpu.outWelcometopprof!Forhelp,type'help'.(pprof)gvTotal:101samplessh:1:dot:notfoundgotoolpprof:signal:brokenpipe
其中,“(pprof)”是pprof工具在交互模式下的提示符。
从输出信息中我们可以看到,gv命令并没有正确的被执行。原因是没有找到命令dot。经查,这个命令属于一个开源软件Graphviz。Graphviz的核心功能是图表的可视化。我们可以通过命令sudoapt-getinstallgraphviz来安装这个软件。注意,上面这条命令仅可用于Debian的Linux发行版及其衍生版。如果是在Redhat的Linux发行版及其衍生版下,可以使用命令“yuminstallgraphviz”来安装Graphviz。安装好Graphviz后,我们再来执行gv命令。
(pprof)gvTotal:101samplesgv-scale0(pprof)sh:1:gv:notfound
现在,输出信息有提示我们没有找到命令gv。gv是自由软件工程项目GNU(GNU'sNotUnix)中的一款开源软件,用来以图形化的方式查看PDF文档。我们以同样的方式安装它。在Debian的Linux发行版及其衍生版下,执行命令sudoapt-getinstallgv,在Redhat的Linux发行版及其衍生版下,执行命令yuminstallgv。软件gv被安装好后,我们再次执行gv命令。在运行着图形界面软件的Linux操作系统下,会弹出这样一个窗口。如图5-3。
图片1.3pprof工具的gv命令的执行结果
图0-3pprof工具的gv命令的执行结果
现在,我们把视线放在主要的图形上。此图形由矩形和有向线段组成。在此图形的大多数矩形中都包含三行信息。第一行是函数的名字。第二行包含了该函数的本地取样计数(在括号左边的数字)及其在取样总数中所占的比例(在括号内的百分比)。第三行则包含了该函数的累积取样计数(括号左边的数字)及其在取样总数中所占的比例(在括号内的百分比)。
首先,读者需要搞清楚两个相似但不相同的概念,即:本地取样计数和累积取样计数。本地取样计数的含义是当前函数在取样中直接出现的次数。累积取样计数的含义是当前函数以及当前函数直接或间接调用的函数在取样中直接出现的次数。所以,存在这样一种场景:对于一个函数来说,它的本地取样计数是0。因为它没有在取样中直接出现过。但是,由于它直接或间接调用的函数频繁的直接出现在取样中,所以这个函数的累积取样计数却会很高。我们以上图中的函数mian.main为例。由于main.main函数在所有取样中都没有直接出现过,所以它的本地取样计数为0。但又由于它是命令源码文件中的入口函数,程序中其他的函数都直接或间接的被它调用。所以,它的累积取样计数是所有函数中最高的,达到了22。注意,不论是本地取样计数还是累积取样计数都没有把函数对自身的调用计算在内。函数对自身的调用又被称为递归调用。
最后需要说明的是,图形中的有向线段表示函数之间的调用关系。有向线段旁边的数字为线段起始位置的函数对线段末端位置的函数的调用计数。这里所说的调用计数其实是以函数的累积取样计数为依托的。更具体的讲,如果有一个从函数A到函数B的有向线段且旁边的数字为10,那么就说明在函数B的累加取样计数中有10次计数是由函数A对函数B的直接调用所引起的。也由于这个原因,函数A对函数B的调用计数必定小于等于函数B的累积取样计数。
至此,我们已经对概要图中的所有元素都进行了说明,相信读者已经能够读懂它了。那么,我们怎样通过概要图对程序进行分析呢?
现在我们已经明确了在概要图中出现的一个函数的本地取样计数、累积取样计数和调用计数的概念和含义以及它们之间的关系。这三个计数是我们分析程序性能的重要依据。
在上述几个分析过程中的所有建议都不是绝对的。程序优化是一个复杂的过程,在很多时候都需要在多个指标或多个解决方案之间进行权衡和博弈。
在这几个分析过程的描述中,我们多次提到了list命令。现在我们就来对它进行说明。先来看一个示例:
(pprof)listShowDepStructTotal:23samplesROUTINE======================main.ShowDepStructin/home/hc/golang/goc2p/src/helper/pds/showpds.go020Totalsamples(flat/cumulative)..44:}..45:fmt.Printf("Thedependencystructureofpackage'%s':\n",pkgImportPath)..46:ShowDepStruct(pn,"")..47:}..48:---..49:funcShowDepStruct(pnode*pkgtool.PkgNode,prefixstring){..50:varbufbytes.Buffer..51:buf.WriteString(prefix)..52:importPath:=pnode.ImportPath().253:buf.WriteString(importPath).154:deps:=pnode.Deps()..55://fmt.Printf("P_NODE:'%s',DEP_LEN:%d\n",importPath,len(deps))..56:iflen(deps)==0{.557:fmt.Printf("%s\n",buf.String())..58:return..59:}..60:buf.WriteString(ARROWS)..61:for_,v:=rangedeps{.1262:ShowDepStruct(v,buf.String())..63:}..64:}---..65:..66:funcgetPkgImportPath()string{..67:iflen(pkgImportPathFlag)>0{..68:returnpkgImportPathFlag..69:}(pprof)
我们在pprof工具的交互界面中输入了命令listShowDepStruct之后得到了很多输出信息。其中,ShowDepStruct为参数routine_regexp的值。输出信息的第一行告诉我们CPU概要文件中的取样一共有23个。这与我们之前讲解gv命令时看到的一样。输出信息的第二行显示,与我们提供的程序正则表达式(也就是参数routine_regexp)的值匹配的函数是main.ShowDepStruct,并且这个函数所在的源码文件的绝对路径是/home/hc/golang/goc2p/src/helper/pds/showpds.go。输出信息中的第三行告诉我们,在main.ShowDepStruct函数体中的代码的本地取样计数的总和是0,而累积取样计数的总和是20。在第三行最右边的括号中,flat代表本地取样计数,而cumulative代表累积取样计数。这是对该行最左边的那两个数字(也就是0和20)的含义的提示。从输出信息的第四行开始是对上述源码文件中的代码的截取,其中包含了main.ShowDepStruct函数的源码。list命令在这些代码的左边添加了对应的行号,这让我们查找代码更加容易。另外,在代码行号左边的对应位置上显示了每行代码的本地取样计数和累积取样计数。如果计数为0,则用英文句号“.”代替。这使得我们可以非常方便的找到存在计数值的代码行。
一般情况下,每行代码对应的本地取样计数和累积取样计数都应该与我们用gv命令生成的函数调用关系图中的计数相同。但是,如果一行代码中存在多个函数调用的话,那么在代码行号左边的计数值就会有偏差。比如,在上述示例中,第62行代码ShowDepStruct(v,buf.String())的累积取样计数是12。但是从之前的函数调用关系图中我们得知,函数main.ShowDepStruct的累积取样计数是10。它们之间的偏差是2。实际上,在程序被执行的时候,第62行代码是由两个操作步骤组成的。第一个步骤是执行函数调用buf.String()并获得结果。第二个步骤是,调用函数ShowDepStruct,同时将变量v``和执行第一个步骤所获得的结果作为参数传入。所以,这2个取样计数应该归咎于第62行代码中的函数调用子句buf.String()。也就是说,第62行代码的累积取样计数由两部分组成,即函数main.ShowDepStruct的累积取样计数和函数bytes.(*Buffer).String的累积取样计数。同理,示例中的第57行代码fmt.Printf("%s\n",buf.String())```的累积取样计数也存在偏差。读者可以试着分析一下原因。
由于篇幅原因,我们只显示了部分输出内容。disasm命令与list命令的输出内容有几分相似。实际上,disasm命令在输出函数main.ShowDepStruct的源码的同时还在每一行代码的下面列出了与这行代码对应的汇编指令。并且,命令还在每一行的最左边的对应位置上标注了该行汇编指令的本地取样计数和累积取样计数,同样以英文句号“.”代表计数为0的情况。另外,在汇编指令的左边且仅与汇编指令以一个冒号相隔的并不是像Go语言代码行中那样的行号,而是汇编指令对应的内存地址。
.1062:ShowDepStruct(v,buf.String()).1080490f2:CALLmain.ShowDepStruct(SB).262:ShowDepStruct(v,buf.String()).28049142:CALLruntime.slicebytetostring(SB)
其中的第一行和第三行说明了第62行代码的累积取样计数的组成,而第二行和第四行说明了存在这样的组成的原因。其中,汇编指令CALLmain.ShowDepStruct(SB)的累积取样计数为10。也就是说,调用main.ShowDepStruct函数期间分析器进行了10次取样。而汇编指令runtime.slicebytetostring(SB)的累积取样计数为2,意味着在调用函数runtime.slicebytetostring期间分析器进行了2次取样。但是,runtime.slicebytetostring函数又是做什么用的呢?实际上,runtime.slicebytetostring函数正是被函数bytes.(*Buffer).String函数调用的。它实现的功能是把元素类型为byte的切片转换为字符串。综上所述,确实像我们之前说的那样,命令源码文件showpds.go中的第62行代码ShowDepStruct(v,buf.String())的累积取样计数12由函数main.ShowDepStruct的累积取样计数10和函数bytes.(*Buffer).String的累积取样计数2组成。
bash(pprof)topTotal:23samples521.7%21.7%521.7%runtime.findfunc521.7%43.5%521.7%stkbucket313.0%56.5%313.0%os.(*File).write14.3%60.9%14.3%MHeap_AllocLocked14.3%65.2%14.3%getaddrbucket14.3%69.6%28.7%runtime.MHeap_Alloc14.3%73.9%14.3%runtime.SizeToClass14.3%78.3%14.3%runtime.aeshashbody14.3%82.6%14.3%runtime.atomicload6414.3%87.0%14.3%runtime.convT2E(pprof)
在默认情况下,top命令输出的列表中只包含本地取样计数最大的前十个函数。如果我们想自定义这个列表的项数,那么需要在top命令后面紧跟一个项数值。比如:命令top5会输出行数为5的列表,命令top20会输出行数为20的列表,等等。
如果我们在top命令后加入标签--cum,那么输出的列表就是以累积取样计数为顺序的。示例如下:
(pprof)top20--cumTotal:23samples00.0%0.0%23100.0%gosched000.0%0.0%2295.7%main.main00.0%0.0%2295.7%runtime.main00.0%0.0%1669.6%runtime.mallocgc00.0%0.0%1252.2%pkgtool.(*PkgNode).Grow00.0%0.0%1147.8%runtime.MProf_Malloc00.0%0.0%1043.5%main.ShowDepStruct00.0%0.0%1043.5%pkgtool.getImportsFromPackage00.0%0.0%834.8%cnew00.0%0.0%834.8%makeslice100.0%0.0%834.8%runtime.cnewarray00.0%0.0%730.4%gostringsize00.0%0.0%730.4%runtime.slicebytetostring00.0%0.0%626.1%pkgtool.getImportsFromGoSource00.0%0.0%626.1%runtime.callers14.3%4.3%626.1%runtime.gentraceback00.0%4.3%626.1%runtime.makeslice521.7%26.1%521.7%runtime.findfunc521.7%47.8%521.7%stkbucket00.0%47.8%417.4%fmt.Fprintf(pprof)
(pprof)top20--cum-fmt\..*-os\..*Ignoringsamplesincallstacksthatmatch'fmt\..*|os\..*'Total:23samplesAfterignoring'fmt\..*|os\..*':15samplesof23(65.2%)00.0%0.0%1565.2%gosched000.0%0.0%1460.9%main.main00.0%0.0%1460.9%runtime.main00.0%0.0%1252.2%runtime.mallocgc00.0%0.0%834.8%pkgtool.(*PkgNode).Grow00.0%0.0%730.4%gostringsize00.0%0.0%730.4%pkgtool.getImportsFromPackage00.0%0.0%730.4%runtime.MProf_Malloc00.0%0.0%730.4%runtime.slicebytetostring00.0%0.0%626.1%main.ShowDepStruct00.0%0.0%626.1%pkgtool.getImportsFromGoSource00.0%0.0%521.7%cnew00.0%0.0%521.7%makeslice100.0%0.0%521.7%runtime.cnewarray00.0%0.0%417.4%runtime.callers14.3%4.3%417.4%runtime.gentraceback00.0%4.3%313.0%MCentral_Grow00.0%4.3%313.0%runtime.MCache_Alloc00.0%4.3%313.0%runtime.MCentral_AllocList313.0%17.4%313.0%runtime.findfunc(pprof)
在上面的示例中,我们通过命令top20获取累积取样计数最大的20个函数的信息,同时过滤掉了来自代码包fmt和os中的函数。
我们要详细讲解的最后一个命令是callgrind。pprof工具可以将概要转化为强大的Valgrind工具集中的组件Callgrind支持的格式。Valgrind是可运行在Linux操作系统上的用来成分析程序性能及程序中的内存泄露错误的强力工具。而作为其中组件之一的Callgrind的功能是收集程序运行时的一些数据、函数调用关系等信息。由此可知,Callgrind工具的功能基本上与我们之前使用标准库代码包runtime的API对程序运行情况进行取样的操作是一致的。
我们可以通过callgrind命令将概要文件的内容转化为Callgrind工具可识别的格式并保存到文件中。示例如下:
(pprof)callgrindcpu.callgrindWritingcallgrindfileto'cpu.callgrind'.(pprof)
文件cpu.callgrind是一个普通文本文件,所以我们可以使用任何文本查看器来查看其中的内容。但更方便的是,我们可以使用callgrind命令直接查看到图形化的数据。现在我们来尝试一下:
(pprof)callgrindWritingcallgrindfileto'/tmp/pprof2641.0.callgrind'.Starting'kcachegrind/tmp/pprof2641.0.callgrind&'(pprof)sh:1:kcachegrind:notfound
我们没有在callgrind命令后添加任何作为参数的统计文件路径。所以callgrind命令会自行使用kcachegrind工具以可视化的方式显示统计数据。然而,我们的系统中还没有安装kcachegrind工具。
安装好kcachegrind工具之后,我们再来执行callgrind命令:
bash(pprof)callgrindWritingcallgrindfileto'/tmp/pprof2641.1.callgrind'.Starting'kcachegrind/tmp/pprof2641.1.callgrind&'(pprof)
从命令输出的提示信息可以看出,实际上callgrind命令把统计文件保存到了Linux的临时文件夹/tmp中。然后使用kcachegrind工具进行查看。下图为在pprof工具交互模式下执行callgrind命令后弹出的kcachegrind工具界面。
图片1.4使用kcachegrind工具查看概要数据
图0-4使用kcachegrind工具查看概要数据
从上图中我们可以看到,kcachegrind工具对数据的展现非常直观。总体上来说,界面被分为了左右两栏。在左栏中的是概要文件中记录的函数的信息列表。列表一共有五列,从左到右的含义分别是函数的累积取样计数在总取样计数中的百分比、函数的本地取样计数在总取样计数中的百分比、函数被调用的总次数(包括递归调用)、函数的名称以及函数所在的源码文件的名称。而在界面的右栏,我们查看在左栏中选中的行的详细信息。kcachegrind工具的功能非常强大。不过由于对它的介绍超出了本书的范围,所以我们就此暂告一个段落。
我们刚刚提到过,不加任何参数callgrind命令的执行分为两个步骤——生成统计文件和使用kcachegrind工具查看文件内容。还记得我们在之前已经生成了一个名为统计文件cpu.callgrind吗?其实,我们可以使用命令kcachegrindcpu.callgrind直接对它进行查看。执行这个命令后所弹出的kcachegrind工具界面与我们之前看到的完全一致。
到现在为止,我们又介绍了两个可以更直观的统计和查看概要文件中数据的命令。top命令让我们可以在命令行终端中查看这些统计信息。而callgrind命令使我们通过kcachegrind工具查看概要文件的数据成为了可能。这两个命令都让我们可以宏观的、从不同维度的来查看和分析概要文件。它们都是非常给力的统计辅助工具。
除了上面详细讲述的那些命令之外,pprof工具在交互模式下还支持少许其它的命令。这在表5-23中也有所体现。这些命令有的只是主要命令的另一种形式(比如web命令和weblist命令),而有的只是为了提供辅助功能(比如help命令和quit命令)。
在本小节中,我们只使用gotoolpprof命令对CPU概要文件进行了查看和分析。读者可以试着对内存概要文件和程序阻塞概要文件进行分析。
相对于普通的编程方式来讲,并发编程都是复杂的。所以,我们就更需要像pprof这样的工具为我们保驾护航。大家可以将本小节当作一篇该工具的文档,并在需要时随时查看。
cgo也是一个Go语言自带的特殊工具。一般情况下,我们使用命令gotoolcgo来运行它。这个工具可以使我们创建能够调用C语言代码的Go语言源码文件。这使得我们可以使用Go语言代码去封装一些C语言的代码库,并提供给Go语言代码或项目使用。
在执行gotoolcgo命令的时候,我们需要加入作为目标的Go语言源码文件的路径。这个路径可以是绝对路径也可以是相对路径。但是,作者强烈建议在目标源码文件所属的代码包目录下执行gotoolcgo命令并以目标源码文件的名字作为参数。因为,gotoolcgo命令会在当前目录(也就是我们执行gotoolcgo命令的目录)中生成一个名为_obj的子目录。该目录下会包含一些Go源码文件和C源码文件。这个子目录及其包含的文件理应被保存在目标代码包目录之下。至于原因,我们稍后再做解释。
我们现在来看可以作为gotoolcgo命令参数的Go语言源码文件。这个源码文件必须要包含一行只针对于代码包C的导入语句。其实,Go语言标准库中并不存在代码包C。代码包C是一个伪造的代码包。导入这个代码包是为了告诉cgo工具在这个源码文件中需要调用C代码,同时也是给予cgo所产生的代码一个专属的命名空间。除此之外,我们还需要在这个代码包导入语句之前加入一些注释,并且在注释行中写出我们真正需要使用的C语言接口文件的名称。像这样:
//#include
在Go语言的规范中,把在代码包C导入语句之前的若干注释行叫做序文(preamble)。在引入了C语言的标准代码库stdlib.h之后,我们就可以在后面的源码中调用这个库中的接口了。像这样:
funcRandom()int{returnint(C.rand())}funcSeed(iint){C.srand(C.uint(i))}
我们把上述的这些Go语言代码写入Go语言的库源码文件rand.go中,并将这个源码文件保存在goc2项目的代码包basic/cgo/lib的对应目录中。
在Go语言源码文件rand.go中对代码包C有四处引用,分别是三个函数调用语句C.rand、C.srand和C.uint,以及一个导入语句import"C"。其中,在Go语言函数Random中调用了C语言标准库代码中的函数rand并返回了它的结果。但是,C语言的rand函数返回的结果的类型是C语言中的int类型。在cgo工具的环境中,C语言中的int类型与C.int相对应。作为一个包装C语言接口的函数,我们必须将代码包C的使用限制在当前代码包内。也就是说,我们必须对当前代码包之外的Go代码隐藏代码包C。这样做也是为了遵循代码隔离原则。我们在设计接口或者接口适配程序的时候经常会用到这种方法。因此,rand函数的结果的类型必须是Go语言的。所以,我们在这里使用函数int对C.int类型的C语言接口的结果进行了转换。当然,为了更清楚的表达,我们也可以将函数Random中的代码returnint(C.rand())拆分成两行,像这样:
varrC.int=C.rand()returnint(r)
而Go语言函数Seed则恰好相反。C语言标准代码库中的函数srand接收一个参数,且这个参数的类型必须为C语言的uint类型,即C.uint。而Go语言函数Seed的参数为Go语言的int类型。为此,我们需要使用代码包C的函数unit对其进行转换。
实际上,标准C语言的数字类型都在cgo工具中有对应的名称,包括:C.char、C.schar(有符号字符类型)、C.uchar(无符号字符类型)、C.short、C.ushort(无符号短整数类型)、C.int、C.uint(无符号整数类型)、C.long、C.ulong(无符号长整数类型)、C.longlong(对应于C语言的类型longlong,它是在C语言的C99标准中定义的新整数类型)、C.ulonglong(无符号的longlong类型)、C.float和C.double。另外,C语言类型void*对应于Go语言的类型unsafe.Pointer。
如果想直接访问C语言中的struct、union或enum类型的话,就需要在名称前分别加入前缀struct_、union_或enum_。比如,我们需要在Go源码文件中访问C语言代码中的名为command的struct类型的话,就需要这样写:C.struct_command。那么,如果我们想在Go语言代码中访问C语言类型struct中的字段需要怎样做呢?解决方案是,同样以C语言类型struct的实例名以及选择符“.”作为前导,但需要在字段的名称前加入下划线“_”。例如,如果command1是名为command的C语言struct类型的实例名,并且这个类型中有一个名为name的字段,那么我们在Go语言代码中访问这个字段的方式就是command1._name。需要注意的是,我们不能在Go的struct类型中嵌入C语言类型的字段。这与我们在前面所说的代码隔离原则具有相同的意义。
在上面展示的库源码文件rand.go中有多处对C语言函数的访问。实际上,任何C语言的函数都可以被Go语言代码调用。只要在源码文件中导入了代码包C。并且,我们还可以同时取回C语言函数的结果,以及一个作为错误提示信息的变量。这与我们在Go语言中同时获取多个函数结果的方法一样。同样的,我们可以使用下划线“_”直接丢弃取回的值。这在调用无结果的C语言函数时非常有用。请看下面的例子:
packagecgo/*#cgoLDFLAGS:-lm#include
上面这段代码被保存在了Go语言库源码文件math.go中,并与源码文件rand.go在同一个代码包目录。在Go语言函数Sqrt中的C.sqrt是一个在C语言标准代码库math.h中的函数。它会返回参数的平方根。但是在第一行代码中,我们接收由函数C.sqrt返回的两个值。其中,第一个值即为C语言函数sqrt的结果。而第二个值就是我们上面所说的那个作为错误提示信息的变量。实际上,这个变量的类型是Go语言的error接口类型。它包装了一个C语言的全局变量errno。这个全局变量被定义在了C语言代码库errno.h中。cgo工具在为我们生成C语言源码文件时会默认引入两个C语言标准代码库,其中一个就是errno.h。所以我们并不用在Go语言源码文件中使用指令符#include显式的引入这个代码库。cgo工具默认为我们引入的另一个是C语言标准代码库string.h。它包含了很多用于字符串处理和内存处理的函数。
在我们以“C.*”的形式调用C语言代码库时,有一点需要特别注意。在C语言中,如果一个函数的参数是一个具有固定尺寸的数组,那么实际上这个函数所需要的是指向这个数组的第一个元素的指针。C编译器能够正确识别和处理这个调用惯例。它可以自行获取到这个指针并传给函数。但是,这在我们使用cgo工具调用C语言代码库时是行不通的。在Go语言中,我们必须显式的将这个指向数组的第一个元素的指针传递给C语言的函数,像这样:``C.func1(&x[0])````。
另一个需要特别注意的地方是,在C语言中没有像Go语言中独立的字符串类型。C语言使用最后一个元素为‘\0’的字符数组来代表字符串。在Go语言的字符串和C语言的字符串之间进行转换的时候,我们就需要用到代码包C中的C.C.CString、C.GoString和C.GoStringN等函数。这些转换操作是通过对字符串数据的拷贝来完成的。Go语言内存管理器并不能感知此类内存分配操作。因为它们是由C语言代码引发的。所以,我们在使用与C.CString函数类似的会导致内存分配操作的函数之后,需要调用代码包C的free函数以手动的释放内存。这里有一个小技巧,即我们可以把对C.free函数的调用放到defer语句中或者放入在defer之后的匿名函数中。这样就可以保证在退出当前函数之前释放这些被分配的内存了。请看下面这个示例:
funcPrint(sstring){cs:=C.CString(s)deferC.free(unsafe.Pointer(cs))C.myprint(cs)}
上面这段代码被存放在goc2p项目的代码包basic/cgo/lib的库源码文件print.go中。其中的函数C.myprint是我们在该库源码文件的序文中自定义的。关于这种C语言函数定义方式,我们一会儿再解释。在这段代码中,我们首先把Go语言的字符串转换为了C语言的字符串。注意,变量cs的值实际上是指向字符串(在C语言中,字符串由字符数组代表)中第一个字符的指针。在cgo工具对应的上下文环境中,cs变量的类型是*C.Char。然后,我们通过defer语句和C.free函数保证由C语言代码分配的内存得以释放。请注意子语句unsafe.Pointer(cs)。正因为cs变量在C语言中的类型是指针类型,且与之相对应的Go语言类型是unsafe.Pointer。所以,我们需要先将其转换为Go语言可以识别的类型再作为参数传递给函数C.free。最后,我们将这个字符串打印到标准输出。
再次重申,我们在使用C.CString函数将Go语言的字符串转换为C语言字符串后,需要显式的调用C.free函数以释放用于数据拷贝的内存。而最佳实践是,将在defer语句中调用C.free函数。
在前面我们已经提到过,在导入代码包C的语句之上可以加入若干个为cgo工具而写的若干注释行(也被叫做序文)。并且,以#include和一个空格开始的注释行可以用来引入一个C语言的接口文件。我们也把序文中这种形式的字符串叫做指令符。指令符#cgo的用途是为编译器和连接器提供标记。这些标记在编译当前源码文件中涉及到代码包C的那部分代码时会被用到。
标记CFLAGS和LDFLAGS``可以被放在指令符#cgo```之后,并用于定制编译器gcc的行为。gcc(GNUCompilerCollection,GNU编译器套装),是一套由GNU开发的开源的编程语言编译器。它是GNU项目的关键部分,也是类Unix操作系统(也包括Linux操作系统)中的标准编译器。gcc(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。gcc原名为GNUC语言编译器(GNUCCompiler),因为它原本只能处理C语言。不过,gcc变得可以处理更多的语言。现在,gcc中包含了很多针对特定编程语言的编译器。我们在本节第一小节的末尾提及的gccgo就是这款套件中针对Go语言的编译器。标记CFLAGS可以指定用于gcc中的C编译器的选项。它尝尝用于指定头文件(.h文件)的路径。而标记LDFLAGS则可以指定gcc编译器会用到的一些优化参数,也可以用来告诉链接器需要用到的C语言代码库文件的位置。
为了清晰起见,我们可以把这些标记及其值拆分成多个注释行,并均以指令符#cgo作为前缀。另外,在指令符#cgo和标记之间,我们也可以加入一些可选的内容,即环境变量GOOS和GOARCH中的有效值。这样,我们就可以使这些标记只在某些操作系统和/或某些计算架构的环境下起作用了。示例如下:
//#cgoCFLAGS:-DPNG_DEBUG=1//#cgolinuxCFLAGS:-DLINUX=1//#cgoLDFLAGS:-lpng//#include
在上面的示例中,序文由四个注释行组成。第一行注释的含义是预定义一个名为PNG_DEBUG的宏并将它的值设置为1。而第二行注释的意思是,如果在Linux操作系统下,则预定义一个名为LINUX的宏并将它的值设置为1。第三行注释是与链接器有关的。它告诉链接器需要用到一个库名为png的代码库文件。最后,第四行注释引入了C语言的标准代码库png.h。
如果我们有一些在所有场景下都会用到的CFLAGS标记或LDFLAGS标记的值,那么就可以把它们分别作为环境变量CGO_CFLAGS和CGO_LDFLAGS的值。而对于需要针对某个导入了“C”的代码包的标记值就只能连同指令符#cgo一起放入Go语言源码文件的注释行中了。
相信读者对指令符#cgo和#include的用法已经有所了解了。
实际上,我们几乎可以在序文中加入任何C代码。像这样:
/*#cgoLDFLAGS:-lsqlite3#include
上面这段代码摘自开源项目gosqlite的Go语言源码文件sqlite.go。gosqlite项目是一个开源数据SQLite的Go语言版本的驱动代码库。实际上,它只是把C语言版本的驱动代码库进行了简单的封装。在Go语言的世界里,这样的封装随处可见,尤其是在Go语言发展早期。因为,这样可以非常方便的重用C语言版本的客户端程序,而大多数软件都早已拥有这类程序了。并且,封装C语言版本的代码库与从头开发一个Go语言版本的客户端程序相比,无论从开发效率还是运行效率上来讲都会是非常迅速的。现在让我们看看在上面的序文中都有些什么。很显然,在上面的序文中直接出现了两个C语言的函数my_bind_text和my_bind_blob。至于为什么要把C语言函数直接写到这里,在它们前面的注释中已经予以说明。大意翻译如下:这些包装函数是必要的,这是因为SQLITE_TRANSIENT是一个指针常量,而cgo并不能正确的翻译它们。看得出来,这是一种备选方案,只有在cgo不能帮我们完成工作时才会被选用。不管怎样,在序文中定义的这两个函数可以直接在当前的Go语言源码文件中被使用。具体的使用方式同样是通过“C.*”的形式来调用。比如源码文件sqlite.go中的代码:
rv:=C.my_bind_text(s.stmt,C.int(i+1),cstr,C.int(len(str)))
rv:=C.my_bind_blob(s.stmt,C.int(i+1),unsafe.Pointer(p),C.int(len(v)))
我们再来看看我们之前提到过的库源码文件print.go(位于goc2p项目的代码包basic/cgo/lib之中)的序文:
/*#include
我们在序文中定义一个名为myprint的函数。在这个函数中调用了C语言的函数printf。自定义函数myprint充当了类似于适配器的角色。之后,我们就可以在后续的代码中直接使用这个自定义的函数了:
C.myprint(cs)
关于在序文中嵌入C语言代码的方法我们就介绍到这里。
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$gotoolcgorand.gohc@ubt:~/golang/goc2p/src/basic/cgo/lib$ls_objrand.gohc@ubt:~/golang/goc2p/src/basic/cgo/lib$ls_obj_cgo_defun.c_cgo_export.h_cgo_gotypes.go_cgo_.orand.cgo2.c_cgo_export.c_cgo_flags_cgo_main.crand.cgo1.go
子目录_obj中一共包含了九个文件。
其中,cgo工具会把作为参数的Go语言源码文件rand.go转换为四个文件。其中包括两个Go语言源码文件rand.cgo1.go和_cgo_gotypes.go,以及两个C语言源码文件_cgo_defun.c和rand.cgo2.c。
文件rand.cgo1.go用于存放cgo工具对原始源码文件rand.go改写后的内容。改写具体细节包括去掉其中的代码包C导入语句,以及替换涉及到代码包C的语句,等等。最后,这些替换后的标识符所对应的Go语言的函数、类型或变量的定义,将会被写入到文件_cgo_gotypes.go中。
需要说明的是,替换涉及到代码包C的语句的具体做法是根据xxx的种类将标识符C.xxx替换为_Cfunc_xxx或者_Ctype_xxx。比如,作为参数的源码文件rand.go中存在如下语句:
C.srand(C.uint(i))
cgo工具会把它改写为:
_Cfunc_srand(_Ctype_uint(i))
其中,标识符C.srand被替换为_Cfunc_srand,而标识符C.uint被替换为了_Ctype_uint。并且,新的标识符_Cfunc_srand和_Ctype_uint的定义将会在文件_cgo_gotypes.go中被写明:
type_Ctype_uintuint32type_Ctype_void[0]bytefunc_Cfunc_srand(_Ctype_uint)_Ctype_void
其中,类型_Ctype_void可以表示空的参数列表或空的结果列表。
文件_cgo_defun.c中包含了相应的C语言函数的定义和实现。例如,C语言函数_Cfunc_srand的实现如下:
#pragmacgo_import_static_cgo_54716c7dc6a7_Cfunc_srandvoid_cgo_54716c7dc6a7_Cfunc_srand(void*);void·_Cfunc_srand(struct{uint8x[4];}p){runtime·cgocall(_cgo_54716c7dc6a7_Cfunc_srand,&p);}
其中,十六进制数“54716c7dc6a7”是cgo工具由于作为参数的源码文件的内容计算得出的哈希值。这个十六进制数作为了函数名称_cgo_54716c7dc6a7_Cfunc_srand的一部分。这样做是为了生成一个唯一的名称以避免冲突。我们看到,在源码文件_cgo_defun.c中只包含了函数_cgo_54716c7dc6a7_Cfunc_srand的定义。而其实现被写入到了另一个C语言源码文件中。这个文件就是rand.cgo2.c。函数_cgo_54716c7dc6a7_Cfunc_srand对应的实现代码如下:
void_cgo_f290d3e89fd1_Cfunc_srand(void*v){struct{unsignedintp0;}__attribute__((__packed__))*a=v;srand(a->p0);}
这个函数从指向函数_Cfunc_puts的参数帧中抽取参数,并调用系统C语言函数srand,最后将结果存储在帧中并返回。
下面我们对在子目录_obj中存放的其余几个文件进行简要说明:
在上述的源码文件中,文件rand.cgo1.go和_cgo_gotypes.go将会在构建代码包时被Go官方Go语言编译器(6g、8g或5g)编译。文件_cgo_defun.c会在构建代码包时被Go官方的C语言的编译器(6c、8c或5c)编译。而文件rand.cgo2.c、_cgo_export.c和_cgo_main.c则会被gcc编译器编译。
如果我们在执行gotoolcgo命令时加入多个Go语言源码文件作为参数,那么在当前目录的_obj子目录下会出现与上述参数数量相同的x.cgo1.go文件和x.cgo2.c文件。其中,x为作为参数的Go语言源码文件主文件名。
通过上面的描述,我们基本了解了由cgo工具生成的文件的内容和用途。
与其它go命令一样,我们在执行gotoolcgo命令的时候还可以加入一些标记。如下表。
表0-24gotoolcgo命令可接受的标记
默认值
-cdefs
false
-godefs
-objdir
""
gcc编译的目标文件所在的路径。若未自定义则为当前目录下的_obj子目录。
-dynimport
如果值不为空字符串,则打印为其值所代表的文件生成的动态导入数据到标准输出。
-dynlinker
记录在dynimport模式下的动态链接器信息。
-dynout
将-dynimport的输出(如果有的话)写入到其值所代表的文件中。
-gccgo
生成可供gccgo编译器使用的文件。
-gccgopkgpath
对应于gccgo编译器的-fgo-pkgpath选项。
-gccgoprefix
对应于gccgo编译器的-fgo-prefix选项。
-debug-define
-debug-gcc
打印gcc调用信息到标准输出。
-import_runtime_cgo
true
在生成的代码中加入语句“importruntime/cgo”。
-import_syscall
在生成的代码中加入语句“importsyscall”。
在上表中,我们把标记分为了五类并在它们之间以空行分隔。
在第一类标记中,-cdefs标记和-godefs标记都可以打印相应的代码到标准输出,并且使cgo工具不生成相应的源码文件。cgo工具在获取目标源码文件内容之后会改写其中的内容,包括去掉代码包C的导入语句,以及对代码包C的调用语句中属于代码包C的类型、函数和变量进行等价替换。如果我们加入了标记-cdefs或-godefs,那么cgo工具随后就会把改写后的目标源码打印到标准输出了。需要注意的是,我们不能同时使用这两个标记。使用这两个标记打印出来的源码内容几乎相同,而最大的区别也只是格式方面的。
首先,我们创建一个命令源码文件cgo_demo.go,并把它存放在goc2p项目的代码包basic/cgo对应的目录下。命令源码文件cgo_demo.go的内容如下:
packagemainimport(cgolib"basic/cgo/lib""fmt")funcmain(){input:=float32(2.33)output,err:=cgolib.Sqrt(input)iferr!=nil{fmt.Errorf("Error:%s\n",err)}fmt.Printf("Thesquarerootof%fis%f.\n",input,output)}
在这个命令源码文件中,我们调用了goc2p项目的代码包basic/cgo/lib中的函数Sqrt。这个函数是被保存在库源码文件math.go中的。而在文件math.go中,我们导入了代码包C。也就是说,命令源码文件cgo_demo.go间接的依赖了代码包C。现在,我们使用gobuild命令将这个命令源码文件编译成ELF格式的可执行文件。然后,我们就能够使用gotoolcgo-dynimport命令查看其中的导入信息了。请看如下示例:
如果我们再加入一个标记-dynlinker,那么在命令的输出信息还会包含动态链接器的信息。示例如下:
hc@ubt:~/golang/goc2p/src/basic/cgo$gotoolcgo-dynimport='cgo_demo'-dynlinker#pragmacgo_dynamic_linker"/lib/ld-linux.so.2"<省略部分输出内容>
如果我们在命令gotoolcgo-dynimport后加入标记-dynout,那么命令的输出信息将会写入到指定的文件中,而不是被打印到标准输出。比如命令gotoolcgo-dynimport='cgo_demo'-dynlinker-dynout='cgo_demo.di'就会将可执行文件cgo_demo中的导入信息以及动态链接器信息转储到当前目录下的名为“cgo_demo.di”的文件中。
第四类标记包含了-gccgo、-gccgopkgpath和-gccgoprefix。它们都与编译器gccgo有关。标记-gccgo的作用是使cgo工具生成可供gccgo编译器使用的源码文件。这些源码文件会与默认情况下生成的源码文件在内容上有一些不同。实际上,到目前为止,cgo工具还不能很好的与gccgo编译器一同使用。但是,按照gccgo编译器的主要开发者IanLanceTaylor的话来说,gccgo编译器并不需要cgo工具,也不应该使用gcc工具。不管怎样,这种情况将会在Go语言的1.3版本中得到改善。
第五类标记用于打印调试信息,包括标记-debug-define和-debug-gcc。gcc工具不但会生成新的Go语言源码文件以保存其对目标源码改写后的内容,还会生成若干个C语言源码文件。cgo工具为了编译这些C语言源码文件,就会用到gcc编译器。在加入-debug-gcc标记之后,gcc编译器的输出信息就会被打印到标准输出上。另外,gcc编译器在对C语言源码文件进行编译之后会产生一个结果文件。这个结果文件就是在obj子目录下的名为_cgo.o的文件。
至此,我们在本小节讨论的都是Go语言代码如果通过cgo工具调用标准C语言编写的函数。其实,我们利用cgo工具还可以把Go语言编写的函数暴露给C语言代码。
Go语言可以使它的函数被C语言代码所用。这是通过使用一个特殊的注释“//export”来实现的。示例如下:
packagecgo/*#include
我们把上面示例中的内容保存到名为go_export.go的文件中,并放到goc2p项目的basic/cgo/lib代码包中。现在我们使用gotoolcgo来处理这个源码文件。如下:
hc@ubt:~/golang/goc2p/basic/cgo/lib$gotoolcgogo_export.go
之后,我们会发现在_obj子目录下的C语言头文件_cgo_export.h中包含了这样一行代码:
externvoidGoFunction1();
这说明C语言代码已经可以对函数GoFunction1进行调用了。现在我们使用gobuild命令构建goc2p项目的代码包basic/cgo,如下:
hc@ubt:~/golang/goc2p/basic/cgo/lib$gobuild#basic/cgo/lib/tmp/go-build477634277/basic/cgo/lib/_obj/go_export.cgo2.o:Infunction`_cgo_cc103c85817e_Cfunc_CFunction1':./go_export.go:34:undefinedreferenceto`CFunction1'collect2:ldreturn1
构建并没有成功完成。根据错误提示信息我们获知,C语言函数CFunction1未被定义。这个问题的原因是我们并没有在Go语言源码文件go_export.go的序文中写入C语言函数CFunction1的实现,也即未对它进行定义。我们之前说过,在这种情况下,对应函数的定义应该被放到其它Go语言源码文件的序文或者C语言源码文件中。现在,我们在当前目录下创建一个新的Go语言源码文件go_export_def.go。其内容如下:
packagecgo/*#include
这个文件是专门用于存放C语言函数定义的。注意,由于C语言函数printf来自C语言标准代码库stdio.h,所以我们需要在序文中使用指令符#include将它引入。保存好源码文件go_export_def.go之后,我们重新使用gotoolcgo命令处理这两个文件,如下:
hc@ubt:~/golang/goc2p/basic/cgo/lib$gotoolcgogo_export.gogo_export_def.go
然后,我们再次执行gobuild命令构建代码包basic/cgo/lib:
hc@ubt:~/golang/goc2p/basic/cgo/lib$gobuild
显然,这次的构建成功完成。当然单独构建代码包basic/cgo/lib并不是必须的。我们在这里是为了检查该代码包中的代码(包括Go语言代码和C语言代码)是否都能够被正确编译。
还记得goc2p项目的代码包basic/cgo中的命令源码文件cgo_demo.go。现在我们在它的main函数的最后加入一行新代码:cgo.CallCFunc(),即调用在代码包``basic/cgo/lib```中的库源码文件go_export.go的函数。然后,我们运行这个命令源码文件:
hc@ubt:~/golang/goc2p/basic/cgo$goruncgo_demo.goThesquarerootof2.330000is1.526434.ABCCFunction1()iscalled.GoFunction1()iscalled.
从输出的信息可以看出,我们定义的C语言函数CFunction1和Go语言函数GoFunction1都已被调用,并且调用顺序正如我们所愿。这个例子也说明,我们可以非常方便的使用cgo工具完成如下几件事:
综上所述,cgo工具不但可以使Go语言直接使用现存的非常丰富的C语言代码库,还可以使用Go语言代码扩展现有的C语言代码库。
至此,我们介绍了怎样独立的使用cgo工具。但实际上,我们可以直接使用标准go命令构建、安装和运行导入了代码包C的代码包和源码文件。标准go命令能够认出代码包C的导入语句并自动使用cgo工具进行处理。示例如下:
hc@ubt:~/golang/goc2p/src/basic/cgo$rm-rflib/_objhc@ubt:~/golang/goc2p/src/basic/cgo$goruncgo_demo.goThesquarerootof2.330000is1.526434.ABCCFunction1()iscalled.GoFunction1()iscalled.
命令goenv用于打印Go语言的环境信息。其中的一些信息我们在之前已经多次提及,但是却没有进行详细的说明。在本小节,我们会对这些信息进行深入介绍。我们先来看一看goenv命令情况下都会打印出哪些Go语言通用环境信息。
表0-25goenv命令可打印出的Go语言通用环境信息
CGO_ENABLED
指明cgo工具是否可用的标识。
GOARCH
程序构建环境的目标计算架构。
GOBIN
存放可执行文件的目录的绝对路径。
GOCHAR
程序构建环境的目标计算架构的单字符标识。
GOEXE
可执行文件的后缀。
GOHOSTARCH
程序运行环境的目标计算架构。
GOOS
程序构建环境的目标操作系统。
GOHOSTOS
程序运行环境的目标操作系统。
GOPATH
工作区目录的绝对路径。
GORACE
GOROOT
Go语言的安装目录的绝对路径。
GOTOOLDIR
Go工具目录的绝对路径。
下面我们对这些环境信息进行逐一说明。
通过上一小节的介绍,相信读者对cgo工具已经很熟悉了。我们提到过,标准go命令可以自动的使用cgo工具对导入了代码包C的代码包和源码文件进行处理。这里所说的“自动”并不是绝对的。因为当环境变量CGO_ENABLED被设置为0时,标准go命令就不能处理导入了代码包C的代码包和源码文件了。请看下面的示例:
hc@ubt:~/golang/goc2p/src/basic/cgo$exportCGO_ENABLED=0hc@ubt:~/golang/goc2p/src/basic/cgo$gobuild-xWORK=/tmp/go-build775234613
我们临时把环境变量CGO_ENABLED的值设置为0,然后执行gobuild命令并加入了标记-x。标记-x会让命令程序将运行期间所有实际执行的命令都打印到标准输出。但是,在执行命令之后没有任何命令被打印出来。这说明对代码包basic/cgo的构建操作并没有被执行。这是因为,构建这个代码包需要用到cgo工具,但cgo工具已经被禁用了。下面,我们再来运行调用了代码包basic/cgo中函数的命令源码文件cgo_demo.go。也就是说,命令源码文件cgo_demo.go间接的导入了代码包C。还记得吗?这个命令源码文件被存放在goc2p项目的代码包basic/cgo中。示例如下:
hc@ubt:~/golang/goc2p/src/basic/cgo$exportCGO_ENABLED=0hc@ubt:~/golang/goc2p/src/basic/cgo$gorun-workcgo_demo.goWORK=/tmp/go-build856581210#command-line-arguments./cgo_demo.go:4:can'tfindimport:"basic/cgo/lib"
在上面的示例中,我们在执行gorun命令时加入了两个标记——-a和-work。标记-a会使命令程序强行重新构建所有的代码包(包括涉及到的标准库),即使它们已经是最新的了。标记-work会使命令程序将临时工作目录的绝对路径打印到标准输出。命令程序输出的错误信息显示,命令程序没有找到代码包basic/cgo。其原因是由于代码包basic/cgo无法被构建。所以,命令程序在临时工作目录和工作区中都找不到代码包basic/cgo对应的归档文件cgo.a。如果我们使用命令ll/tmp/go-build856581210查看临时工作目录,也找不到名为basic的目录。
不过,如果我们在环境变量CGO_ENABLED的值为1的情况下生成代码包basic/cgo对应的归档文件cgo.a,那么无论我们之后怎样改变环境变量CGO_ENABLED的值也都可以正确的运行命令源码文件cgo_demo.go。即使我们在执行gorun命令时加入标记-a也是如此。因为命令程序依然可以在工作区中找到之前在我们执行goinstall命令时生成的归档文件cgo.a。示例如下:
hc@ubt:~/golang/goc2p/src/basic/cgo$exportCGO_ENABLED=1hc@ubt:~/golang/goc2p/src/basic/cgo$goinstall../basic/cgohc@ubt:~/golang/goc2p/src/basic/cgo$exportCGO_ENABLED=0hc@ubt:~/golang/goc2p/src/basic/cgo$gorun-a-workcgo_demo.goWORK=/tmp/go-build130612063Thesquarerootof2.330000is1.526434.ABCCFunction1()iscalled.GoFunction1()iscalled.
由此可知,只要我们事先成功安装了引用了代码包C的代码包,即生成了对应的代码包归档文件,即使cgo工具在之后被禁用,也不会影响到其它Go语言代码对该代码包的使用。当然,命令程序首先会到临时工作目录中寻找需要的代码包归档文件。
关于cgo工具还有一点需要特别注意,即:当存在交叉编译的情况时,cgo工具一定是不可用的。在标准go命令的上下文环境中,交叉编译意味着程序构建环境的目标计算架构的标识与程序运行环境的目标计算架构的标识不同,或者程序构建环境的目标操作系统的标识与程序运行环境的目标操作系统的标识不同。在这里,我们可以粗略认为交叉编译就是在当前的计算架构和操作系统下编译和构建Go语言代码并生成针对于其他计算架构或/和操作系统的编译结果文件和可执行文件。
GOARCH的值的含义是程序构建环境的目标计算架构的标识,也就是程序在构建或安装时所对应的计算架构的名称。在默认情况下,它会与程序运行环境的目标计算架构一致。即它的值会与GOHOSTARCH的值是相同。但如果我们显式的设置了环境变量GOARCH,则它的值就会是这个环境变量的值。
GOBIN的值为存放可执行文件的目录的绝对路径。它的值来自环境变量GOBIN。在我们使用gotoolinstall命令安装命令源码文件时生成的可执行文件会存放于这个目录中。
GOCHAR的值是程序构建环境的目标计算架构的单字符标识。它的值会根据GOARCH的值来设置。当GOARCH的值为386时,GOCHAR的值就是8。当GOARCH的值为amd64时GOCHAR的值就是6。而GOCHAR的值5与GOARCH的值arm相对应。
GOCHAR主要有两个用途,列举如下:
GOEXE的值会被作为可执行文件的后缀。它的值与GOOS的值存在一定关系,即只有GOOS的值为“windows”时GOEXE的值才会是“.exe”,否则其值就为空字符串“”。这与在各个操作系统下的可执行文件的默认后缀是一致的。
GOHOSTOS的值的含义是程序运行环境的目标操作系统的标识,也即程序在运行时所在的计算机系统的操作系统的名称。与GOHOSTARCH类似,它的值在不同的操作系统下是固定不变的,同样不需要显式的设置。
这个环境信息我们在之前已经提到过很多次。它的值指明了Go语言工作区目录的绝对路径。我们需要显式的设置环境变量GOPATH。如果有多个工作区,那么多个工作区的绝对路径之间需要用分隔符分隔。在windows操作系统下,这个分隔符为“;”。在其它操作系统下,这个分隔符为“:”。注意,GOPATH的值不能与GOROOT的值相同。
数据竞争检测需要被显式的开启。还记得标记-race吗?我们可以通过在执行一些标准go命令时加入这个标记来开启数据竞争检测。在这种情况下,GORACE的值就会被使用到了。支持-race标记的标准go命令包括:gotest命令、gorun命令、gobuild命令和goinstall命令。
GORACE的值形如“option1=val1option2=val2”,即:选项名称与选项值之间以等号“=”分隔,多个选项之间以空格“”分隔。数据竞争检测的选项包括log_path、exitcode、strip_path_prefix和history_size。为了设置GORACE的值,我们需要设置环境变量GORACE。或者,我们也可以在执行go命令时临时设置它,像这样:
hc@ubt:~/golang/goc2p/src/cnet/ctcp$GORACE="log_path=/home/hc/golang/goc2p/race/reportstrip_path_prefix=home/hc/golang/goc2p/"gotest-race
关于数据竞争检测的更多细节我们将会在本书的第三部分予以说明。
GOROOT会是我们在安装Go语言时第一个碰到Go语言环境变量。它的值指明了Go语言的安装目录的绝对路径。但是,只有在非默认情况下我们才需要显式的设置环境变量GOROOT。这里所说的默认情况是指:在Windows操作系统下我们把Go语言安装到c:\Go目录下,或者在其它操作系统下我们把Go语言安装到/usr/local/go目录下。另外,当我们不是通过二进制分发包来安装Go语言的时候,也不需要设置环境变量GOROOT的值。比如,在Windows操作系统下,我们可以使用MSI软件包文件来安装Go语言。
GOTOOLDIR的值指明了Go工具目录的绝对路径。根据GOROOT、GOHOSTOS和GOHOSTARCH来设置。其值为$GOROOT/pkg/tool/$GOOS_$GOARCH。关于这个目录,我们在之前也提到过多次。
除了上面介绍的这些通用的Go语言环境信息,还两个针对于非Plan9操作系统的环境信息。它们是CC和GOGCCFLAGS。环境信息CC的值是操作系统默认的C语言编译器的命令名称。环境信息GOGCCFLAGS的值则是Go语言在使用操作系统的默认C语言编译器对C语言代码进行编译时加入的参数。
如果我们要有针对性的查看上述的一个或多个环境信息,可以在goenv命令的后面加入它们的名字并执行之。在goenv命令和环境信息名称之间需要用空格分隔,多个环境信息名称之间也需要用空格分隔。示例如下:
hc@ubt:~$goenvGOARCHGOCHAR3868
上例的goenv命令的输出信息中,每一行对一个环境信息的值,且其顺序与我们输入的环境信息名称的顺序一致。比如,386为环境信息GOARCH,而8则是环境信息GOCHAR的值。
goenv命令能够让我们对当前的Go语言环境进行简要的了解。通过它,我们也可以对当前安装的Go语言的环境设置进行简单的检查。