您的位置 首页 golang

GO 编程:Golang标准命令详解(一)

Go语言的1.5版本在标准命令方面有了重大变更。这倒不是说它们的用法有多大的变化,而是说它们的底层支持已经大变样了。让我们先来对比一下 $GOROOT/pkg/tool/<平台相关目录> 中的内容。以下简称此目录为Go工具目录。

插播: 平台相关目录即以_命名的目录,用于存放因特定平台的不同而不同的代码包归档文件或 可执行文件 。其中,代表特定平台的操作系统代号,而则代表特定平台的计算架构代号。使用 go env 命令便可查看它们在你的计算机中的实际值。

1.4版本的Go工具目录的内容如下:

 5a        5l        6g        8c        addr2line dist      objdump   tour
5c        6a        6l        8g        cgo       fix       pack      vet
5g        6c        8a        8l        cover     nm        pprof     yacc  

下面是Go 1.5版本的:

 addr2line asm       compile   dist      fix       nm        pack      tour      vet
api       cgo       cover     doc       link      objdump   pprof     trace     yacc  

可以看到,1.5版本的目录内容精简了不少。这是因为Go 1.5的编译器、链接器都已经完全用Go语言重写了。而在这之前,它们都是用 C语言 写的,因此不得不为每类平台编写不同的程序并生成不同的文件。例如,8g、6g和5g分别是gc编译器在x86(32bit)、x86-64(64bit)和 ARM 计算架构的计算机上的实现程序。相比之下,用Go语言实现的好处就是,编译器和链接器都将是跨平台的了。简要来说,Go 1.5版本的目录中的文件compile即是统一后的编译器,而文件link则是统一后的链接器。

本教程并不会讲解Go语言的编译器、链接器以及其它工具是怎样被编写出来的,并只会关注于怎样用好包含它们在内的Go语言自带的命令和工具。

为了让讲解更具关联性,也为了让读者能够更容易地理解这些命令和工具,本教程并不会按照这些命令的字典顺序描述它们,而会按照我们在实际开发过程中通常的使用顺序以及它们的重要程度来逐一进行说明。现在,我们就先从 go build 命令开始。

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 install go run go test 等命令同样是有效的。下表列出了其中比较常用的标记。

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

标记名称

标记描述

-a

强行对所有涉及到的代码包(包含标准库中的代码包)进行重新构建,即使它们已经是最新的了。

-n

打印编译期间所用到的其它命令,但是并不真正执行它们。

-p n

指定编译过程中执行各任务的并行数量(确切地说应该是并发数量)。在默认情况下,该数量等于 CPU 的逻辑核数。但是在 darwin/arm 平台(即iPhone和iPad所用的平台)下,该数量默认是 1

-race

开启竞态条件的检测。不过此标记目前仅在 Linux /amd64 freebsd /amd64 darwin/amd64 windows/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

此标记用于指定当前使用的编译器的名称。其值可以为 gc GCC go 。其中,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语言自带工具(如 vet asm 等)的方式。

go install

命令 go install 用于编译并安装指定的代码包及它们的依赖包。当指定的代码包的依赖包还没有被编译和安装时,该命令会先去处理依赖包。与 go build 命令一样,传给 go install 命令的代码包参数应该以导入路径的形式提供。并且, go build 命令的绝大多数标记也都可以用于 go install 命令。实际上, go install 命令只比 go build 命令多做了一件事,即:安装编译后的结果文件到指定目录。

在对 go install 命令进行详细说明之前,让我们先回顾一下goc2p的目录结构。为了节省篇幅,我在这里隐藏了代码包中的源码文件。如下:

 $HOME/golang/goc2p:
    bin/
    pkg/
    src/
        cnet/
        logging/
        helper/
            ds/
        pkgtool/  

我们看到,goc2p项目中有三个子目录,分别是 bin 目录、pkg目录和src目录。现在只有src目录中包含了一些目录,而其他两个目录都是空的。

现在,我们来看看安装代码包的规则。

安装代码包

如果 go install 命令后跟的代码包中仅包含库源码文件,那么 go install 命令会把编译后的结果文件保存在源码文件所在工作区的pkg目录下。对于仅包含库源码文件的代码包来说,这个结果文件就是对应的代码包归档文件(也叫静态链接库文件,名称以.a结尾)。相比之下,我们在使用 go build 命令对仅包含库源码文件的代码包进行编译时,是不会在当前工作区的src目录以及pkg目录下产生任何结果文件的。结果文件会出于编译的目的被生成在临时目录中,但并不会使当前工作区目录产生任何变化。

如果我们在执行 go install 命令时不后跟任何代码包参数,那么命令将试图编译当前目录所对应的代码包。比如,我们现在要安装代码包 pkgtool

 hc@ubt:~/golang/goc2p/src/pkgtool$ go install -v -work
WORK=D:\cygwin\tmp\go-build758586887
pkgtool  

我们在前面说过,执行 go install 命令后会对指定代码包先编译再安装。其中,编译代码包使用了与 go build 命令相同的程序。所以,执行 go install 命令后也会首先建立一个名称以go-build为前缀的临时目录。如果我们想强行重新安装指定代码包及其依赖包,那么就需要加入标记 -a :

 hc@ubt:~/golang/goc2p/src/pkgtool$ go install -a -v -work
WORK=/tmp/go-build014992994
runtime
errors
sync/atomic
unicode
unicode/utf8
sort
sync
io
syscall
strings
bytes
bufio
time
os
path/filepath
pkgtool  

可以看到,代码包 pkgtool 仅仅依赖了Go语言标准库中的代码包。

现在我们再来查看一下goc2p项目目录:

 $HOME/golang/goc2p:
    bin/
    pkg/
        linux_386/
            pkgtool.a
        src/  

现在pkg目录中多了一个子目录。读过0.0节的读者应该已经知道,linux 386被叫做平台相关目录。它的名字可以由`${GOOS} ${GOARCH} 来得到。其中, ${GOOS} ${GOARCH} 分别是当前操作系统中的环境变量GOOS和GOARCH的值。如果它们不存在,那么Go语言就会使用其内部的预定值。上述示例在计算架构为386且操作系统为Linux的计算机上运行。所以,这里的平台相关目录即为linux_386。我们还看到,在goc2p项目中的平台相关目录下存在一个文件,名称是pkgtool.a。这就是代码包 pkgtool`的归档文件,文件名称是由代码包名称与“.a”后缀组合而来的。

实际上,代码包的归档文件并不都会被直接保存在pkg目录的平台相关目录下,还可能被保存在这个平台相关目录的子目录下。 下面我们来安装 cnet/ctcp 包:

 hc@ubt:~/golang/goc2p/src/pkgtool$ go install -a -v -work ../cnet/ctcp
WORK=/tmp/go-build083178213
runtime
errors
sync/atomic
unicode
unicode/utf8
math
sync
sort
io
syscall
internal/singleflight
bytes
strings
strconv
bufio
math/rand
time
reflect
os
fmt
log
runtime/cgo
logging
net
cnet/ctcp  

请注意,我们是在代码包 pkgtool 对应的目录下安装 cnet/ctcp 包的。我们使用了一个目录相对路径。

实际上,这种提供代码包位置的方式被叫做本地代码包路径方式,也是被所有Go命令接受的一种方式,这包括之前已经介绍过的 go build 命令。但是需要注意的是,本地代码包路径只能以目录相对路径的形式呈现,而不能使用目录绝对路径。请看下面的示例:

 hc@ubt:~/golang/goc2p/src/cnet/ctcp$ go install -v -work ~/golang/goc2p/src/cnet/ctcp
can't load package: package /home/hc/golang/goc2p/src/cnet/ctcp: import "/home/hc/golang/goc2p/src/cnet/ctcp": cannot import absolute path  

从上述示例中的命令提示信息我们可知,以目录绝对路径的形式提供代码包位置是不会被Go命令认可的。

这是由于Go认为本地代码包路径的表示只能以“./”或“../”开始,再或者直接为“.”或“..”,而代码包的代码导入路径又不允许以“/”开始。所以,这种用绝对路径表示代码包位置的方式也就不能被支持了。

上述规则适用于所有Go命令。读者可以自己尝试一下,比如在执行 go build 命令时分别以代码包导入路径、目录相对路径和目录绝对路径的形式提供代码包位置,并查看执行结果。

我们已经通过上面的示例强行的重新安装了 cnet/ctcp 包及其依赖包。现在我们再来看一下goc2p的项目目录:

 $HOME/golang/goc2p:
    bin/
    pkg/
        linux_386/
            /cnet
                ctcp.a
            logging.a
            pkgtool.a
    src/  

我们发现在pkg目录的平台相关目录下多了一个名为cnet的目录,而在这个目录下的就是名为ctcp.a的代码包归档文件。由此我们可知,代码包归档文件的存放目录的相对路径(相对于当前工作区的pkg目录的平台相关目录)即为代码包导入路径除去最后一个元素后的路径。而代码包归档文件的名称即为代码包导入路径中的最后一个元素再加“.a”后缀。再举一个例子,如果代码包导入路径为x/y/z,则它的归档文件存放路径的相对路径即为x/y/,而这个归档文件的名称即为z.a。

回顾代码包 pkgtool 的归档文件的存放路径。因为它的导入路径中只有一个元素,所以其归档文件就被直接存放到了goc2p项目的pkg目录的平台相关目录下了。

此外,我们还发现pkg目录的平台相关目录下还有一个名为logging.a的文件。很显然,我们并没有显式的安装代码包 logging 。这是因为 go install 命令在安装指定的代码包之前,会先去安装指定代码包的依赖包。当依赖包被正确安装后,指定的代码包的安装才会开始。由于代码包 cnet/ctcp 依赖于goc2p项目(即当前工作区)中的代码包 logging ,所以当代码包 logging 被成功安装之后,代码包 cnet/ctcp 才会被安装。

还有一个问题:上述的安装过程涉及到了那么多代码包,那为什么goc2p项目的pkg目录中只包含该项目中代码包的归档文件呢?实际上, go install 命令会把标准库中的代码包的归档文件存放到Go语言安装目录的pkg子目录中,而把指定代码包依赖的第三方项目的代码包的归档文件存放到当前工作区的pkg目录下。这样就实现了Go语言标准库代码包的归档文件与用户代码包的归档文件,以及处在不同工作区的用户代码包的归档文件之间的分离。

安装命令源码文件

除了安装代码包之外, go install 命令还可以安装命令源码文件。为了看到安装命令源码文件是goc2p项目目录的变化,我们先把该目录还原到原始状态,即清除bin子目录和pkg子目录下的所有目录和文件。然后,我们来安装代码包 helper/ds 下的命令源码文件showds.go,如下:

 hc@ubt:~/golang/goc2p/src$ go install helper/ds/showds.go
go install: no install location for .go files listed on command line (GOBIN not set)  

这次我们没能成功安装。该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命令的源码文件找到了其根本原因。在上一小节我们提到过,在环境变量GOPATH中包含多个工作区目录路径时,我们需要在编译命令源码文件前先对环境变量GOBIN进行设置。实际上,这个环境变量所指的目录路径就是命令程序生成的结果文件的存放目录。 go install 命令会把相应的可执行文件放置到这个目录中。

由于命令 go build 在编译库源码文件后不会产生任何结果文件,所以自然也不用会在意结果文件的存放目录。在该命令编译单一的命令源码文件或者包含一个命令源码文件和多个库源码文件时,在结果文件存放目录无效的情况下会将结果文件(也就是可执行文件)存放到执行该命令时所在的目录下。因此,即使环境变量GOBIN的值无效,我们在执行 go build 命令时也不会见到这个错误提示信息。

然而, go install 命令中一个很重要的步骤就是将结果文件(归档文件或者可执行文件)存放到相应的目录中。所以, go install 命令在安装命令源码文件时,如果环境变量GOBIN的值无效,则它会在最后检查结果文件存放目录的时候发现这一问题,并打印与上述示例所示内容类似的错误提示信息,最后直接退出。

这个错误提示信息在我们安装多个库源码文件时也有可能遇到。示例如下:

 hc@ubt:~/golang/goc2p/src/pkgtool$ go install envir.go fpath.go ipath.go pnode.go util.go
go install: no install location for .go files listed on command line (GOBIN not set)  

而且,在我们为环境变量GOBIN设置了正确的值之后,这个错误提示信息仍然会出现。这是因为,只有在安装命令源码文件的时候,命令程序才会将环境变量GOBIN的值作为结果文件的存放目录。而在安装库源码文件时,在命令程序内部的代表结果文件存放目录路径的那个变量不会被赋值。最后,命令程序会发现它依然是个无效的空值。所以,命令程序会同样返回一个关于“无安装位置”的错误。这就引出一个结论,我们只能使用安装代码包的方式来安装库源码文件,而不能在 go install 命令罗列并安装它们。另外, go install 命令目前无法接受标记 -o 以自定义结果文件的存放位置。这也从侧面说明了 go install 命令不支持针对库源码文件的安装操作。

至此,我们对怎样用 go install 命令来安装代码包以及命令源码文件进行了说明。如果你已经熟知了 go build 命令,那么理解这些内容应该不在话下。

go get

 hc@ubt:~$ go get github.com/hyper-carrot/go_lib/logging  

命令 go get 可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。在上面这个示例中,我们从著名的代码托管站点Github上下载了一个项目(或称代码包),并安装到了环境变量GOPATH中包含的第一个工作区中。与此同时,我们也知道了这个代码包的导入路径就是github.com/hyper-carrot/go_lib/logging。

一般情况下,为了分离自己与第三方的代码,我们会设置两个或更多的工作区。我们现在有一个目录路径为/home/hc/golang/lib的工作区,并且它是环境变量GOPATH值中的第一个目录路径。注意,环境变量GOPATH中包含的路径不能与环境变量GOROOT的值重复。好了,如果我们使用 go get 命令下载和安装代码包,那么这些代码包都会被安装在上面这个工作区中。我们暂且把这个工作区叫做Lib工作区。在我们运行 go get github.com/hyper-carrot/go_lib/logging 之后,这个代码包就应该会被保存在Lib工作的src目录下,并且已经被安装妥当,如下所示:

 /home/hc/golang/lib:
    bin/
    pkg/
        linux_386/
            github.com/
                hyper-carrot/
                    go_lib/
                        logging.a  
    src/
        github.com/
            hyper-carrot/
                go_lib/
                    logging/
    ...  

另一方面,如果我们想把一个项目上传到Github网站(或其他代码托管网站)上并被其他人使用的话,那么我们就应该把这个项目当做一个代码包来看待。其实我们在之前已经提到过原因, go get 命令会将项目下的所有子目录和源码文件存放到第一个工作区的src目录下,而src目录下的所有子目录都会是某个代码包导入路径的一部分或者全部。也就是说,我们应该直接在项目目录下存放子代码包和源码文件,并且直接存放在项目目录下的源码文件所声明的包名应该与该项目名相同(除非它是命令源码文件)。这样做可以让其他人使用 go get 命令从Github站点上下载你的项目之后直接就能使用它。

实际上,像goc2p项目这样直接以项目根目录的路径作为工作区路径的做法是不被推荐的。之所以这样做主要是想让读者更容易的理解Go语言的工程结构和工作区概念,也可以让读者看到另一种项目结构。当然,如果你的项目使用了gb这样的工具那就是另外一回事了。这样的项目的根目录就应该被视为一个工作区(但是你不必把它加入到GOPATH环境变量中)。它应该由 git clone 下载到Go语言工作区之外的某处,而不是使用 go get 命令。

远程导入路径分析

实际上, go get 命令所做的动作也被叫做代码包远程导入,而传递给该命令的作为代码包导入路径的那个参数又被叫做代码包远程导入路径。

go get 命令不仅可以从像Github这样著名的代码托管站点上下载代码包,还可以从任何命令支持的代码版本控制系统(英文为Version Control System,简称为VCS)检出代码包。任何代码托管站点都是通过某个或某些代码版本控制系统来提供代码上传下载服务的。所以,更严格地讲, go get 命令所做的是从代码版本控制系统的远程仓库中检出/更新代码包并对其进行编译和安装。

该命令所支持的VCS的信息如下表:

表0-2 go get 命令支持的VCS

名称

主命令

说明

Mercurial

hg

Mercurial是一种轻量级分布式版本控制系统,采用Python语言实现,易于学习和使用,扩展性强。

Git

git

Git最开始是Linux Torvalds为了帮助管理 Linux 内核开发而开发的一个开源的分布式版本控制软件。但现在已被广泛使用。它是被用来进行有效、高速的各种规模项目的版本管理。

Subversion

svn

Subversion是一个版本控制系统,也是第一个将分支概念和功能纳入到版本控制模型的系统。但相对于Git和Mercurial而言,它只算是传统版本控制系统的一员。

Bazaar

bzr

Bazaar是一个开源的分布式版本控制系统。但相比而言,用它来作为VCS的项目并不多。

go get 命令在检出代码包之前必须要知道代码包远程导入路径所对应的版本控制系统和远程仓库的URL。

如果该代码包在本地工作区中已经存在,则会直接通过分析其路径来确定这几项信息。 go get 命令支持的几个版本控制系统都有一个共同点,那就是会在检出的项目目录中存放一个元数据目录,名称为“.”前缀加其主命令名。例如,Git会在检出的项目目录中加入一个名为“.git”的子目录。所以,这样就很容易判定代码包所用的版本控制系统。另外,又由于代码包已经存在,我们只需通过代码版本控制系统的更新命令来更新代码包,因此也就不需要知道其远程仓库的URL了。对于已存在于本地工作区的代码包,除非要求强行更新代码包,否则 go get 命令不会进行重复下载。如果想要强行更新代码包,可以在执行 go get 命令时加入 -u 标记。这一标记会稍后介绍。

如果本地工作区中不存在该代码包,那么就只能通过对代码包远程导入路径进行分析来获取相关信息了。首先, go get 命令会对代码包远程导入路径进行静态分析。为了使分析过程更加方便快捷, go get 命令程序中已经预置了几个著名代码托管网站的信息。如下表:

表0-3 预置的代码托管站点的信息

名称

主域名

支持的VCS

代码包远程导入路径示例

Bitbucket

bitbucket.org

Git, Mercurial

bitbucket.org/user/project
bitbucket.org/user/project/sub/directory

GitHub

github.com

Git

github.com/user/project
github.com/user/project/sub/directory

Google Code Project Hosting

code.google.com

Git, Mercurial, Subversion

code.google.com/p/project
code.google.com/p/project/sub/directory
code.google.com/p/project.subrepository
code.google.com/p/project.subrepository/sub/directory

Launchpad

launchpad.net

Bazaar

launchpad.net/project
launchpad.net/project/series
launchpad.net/project/series/sub/directory
launchpad.net/~user/project/branch
launchpad.net/~user/project/branch/sub/directory

IBM DevOps Services

hub.jazz.net

Git

hub.jazz.net/git/user/project
hub.jazz.net/git/user/project/sub/directory

一般情况下,代码包远程导入路径中的第一个元素就是代码托管网站的主域名。在静态分析的时候, go get 命令会将代码包远程导入路径与预置的代码托管站点的主域名进行匹配。如果匹配成功,则在对代码包远程导入路径的初步检查后返回正常的返回值或错误信息。如果匹配不成功,则会再对代码包远程导入路径进行动态分析。至于动态分析的过程,我就不在这里详细展开了。

如果对代码包远程导入路径的静态分析或/和动态分析成功并获取到对应的版本控制系统和远程仓库URL,那么 go get 命令就会进行代码包检出或更新的操作。随后, go get 命令会在必要时以同样的方式检出或更新这个代码包的所有依赖包。

自定义代码包远程导入路径

如果你想把你编写的(被托管在不同的代码托管网站上的)代码包的远程导入路径统一起来,或者不希望让你的代码包中夹杂某个代码托管网站的域名,那么你可以选择自定义你的代码包远程导入路径。这种自定义的实现手段叫做“导入注释”。导入注释的写法示例如下:

 package analyzer // import "hypermind.cn/talon/analyzer"  

代码包 analyzer 实际上属于我的一个网络爬虫项目。这个项目的代码被托管在了Github网站上。它的网址是:。如果用标准的导入路径来下载 analyzer 代码包的话,命令应该这样写 go get github.com/hyper-carrot/talon/analyzer 。不过,如果我们像上面的示例那样在该代码包中的一个源码文件中加入导入注释的话,这样下载它就行不通了。我们来看一看这个导入注释。

导入注释的写法如同一条代码包导入语句。不同的是,它出现在了单行注释符 // 的右边,因此Go语言编译器会忽略掉它。另外,它必须出现在源码文件的第一行语句(也就是代码包声明语句)的右边。只有符合上述这两个位置条件的导入注释才是有效的。再来看其中的引号部分。被双引号包裹的应该是一个符合导入路径语法规则的字符串。其中, hypermind.cn 是我自己的一个域名。实际上,这也是用来替换掉我想隐去的代码托管网站域名及部分路径(这里是 github.com/hyper-carrot )的那部分。在 hypermind.cn 右边的依次是我的项目的名称以及要下载的那个代码包的相对路径。这些与其标准导入路径中的内容都是一致的。为了清晰起见,我们再来做下对比。

 github.com/hyper-carrot/talon/analyzer // 标准的导入路径
hypermind.cn           /talon/analyzer // 导入注释中的导入路径  

你想用你自己的域名替换掉标准导入路径中的哪部分由你自己说了算。不过一般情况下,被替换的部分包括代码托管网站的域名以及你在那里的用户ID就可以了。这足以达到我们最开始说的那两个目的。

虽然我们在talon项目中的所有代码包中都加入了类似的导入注释,但是我们依然无法通过 go get hypermind.cn/talon/analyzer 命令来下载这个代码包。因为域名 hypermind.cn 所指向的网站并没有加入相应的处理逻辑。具体的实现步骤应该是这样的:

  1. 编写一个可处理HTTP请求的程序。这里无所谓用什么编程语言去实现。当然,我推荐你用Go语言去做。
  2. 将这个处理程序与 hypermind.cn/talon 这个路径关联在一起,并总是在作为响应的HTML文档的头中写入下面这行内容:
    1. <meta name = “go-import” content = “hypermind.cn/talon git #34; >
  3. hypermind.cn/talon/analyzer熟悉HTML的读者都应该知道,这行内容会被视为HTML文档的元数据。它实际上 go get 命令的文档中要求的写法。它的模式是这样的:
 <meta name="go-import" content="import-prefix vcs repo-root">  

实际上, content 属性中的 import-prefix 的位置上应该填入我们自定义的远程代码包导入路径的前缀。这个前缀应该与我们的处理程序关联的那个路径相一致。而 vcs 显然应该代表与版本控制系统有关的标识。还记得表0-2中的主命令列吗?这里的填入内容就应该该列中的某一项。在这里,由于talon项目使用的是Git,所以这里应该填入 git 。至于 repo-root ,它应该是与该处理程序关联的路径对应的Github网站的URL。在这里,这个路径是 hypermind.cn/talon ,那么这个URL就应该是 。后者也是talon项目的实际网址。

好了,在我们做好上述处理程序之后, go get hypermind.cn/talon/analyzer 命令的执行结果就会是正确的。 analyzer 代码包及其依赖包中的代码会被下载到GOPATH环境变量中的第一个工作区目录的src子目录中,然后被编译并安装。

注意,具体的代码包源码存放路径会是/home/hc/golang/lib/src/hypermind.cn/talon/analyzer。也就是说,存放路径(包括代码包源码文件以及相应的归档文件的存放路径)会遵循导入注释中的路径(这里是 hypermind.cn/talon/analyzer ),而不是原始的导入路径(这里是 github.com/hyper-carrot/talon/analyzer )。另外,我们只需在talon项目的每个代码包中的某一个源码文件中加入导入注释,但这些导入注释中的路径都必须是一致的。在这之后,我们就只能使用 hypermind.cn/talon/ 作为talon项目中的代码包的导入路径前缀了。一个反例如下:

 hc@ubt:~$ go get github.com/hyper-carrot/talon/analyzer
package github.com/hyper-carrot/talon/analyzer: code in directory /home/hc/golang/lib/src/github.com/hyper-carrot/talon/analyzer expects import "hypermind.cn/talon/analyzer"  

与自定义的代码包远程导入路径有关的内容我们就介绍到这里。从中我们也可以看出,Go语言为了让使用者的项目与代码托管网站隔离所作出的努力。只要你有自己的网站和一个不错的域名,这就很容易搞定并且非常值得。这会在你的代码包的使用者面前强化你的品牌,而不是某个代码托管网站的。当然,使你的代码包导入路径整齐划一是最直接的好处。

OK,言归正传,我下面继续关注 go get 这个命令本身。

命令特有标记

go get 命令可以接受所有可用于 go build 命令和 go install 命令的标记。这是因为 go get 命令的内部步骤中完全包含了编译和安装这两个动作。另外, go get 命令还有一些特有的标记,如下表所示:

表0-4 go get 命令的特有标记说明

标记名称

标记描述

-d

让命令程序只执行下载动作,而不执行安装动作。

-f

仅在使用 -u 标记时才有效。该标记会让命令程序忽略掉对已下载代码包的导入路径的检查。如果下载并安装的代码包所属的项目是你从别人那里Fork过来的,那么这样做就尤为重要了。

-fix

让命令程序在下载代码包后先执行修正动作,而后再进行编译和安装。

-insecure

允许命令程序使用非安全的scheme(如HTTP)去下载指定的代码包。如果你用的代码仓库(如公司内部的Gitlab)没有HTTPS支持,可以添加此标记。请在确定安全的情况下使用它。

-t

让命令程序同时下载并安装指定的代码包中的测试源码文件中依赖的代码包。

-u

让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。

为了更好的理解这几个特有标记,我们先清除Lib工作区的src目录和pkg目录中的所有子目录和文件。现在我们使用带有 -d 标记的 go get 命令来下载同样的代码包:

 hc@ubt:~$ go get -d github.com/hyper-carrot/go_lib/logging  

现在,让我们再来看一下Lib工作区的目录结构:

 /home/hc/golang/lib:
    bin/
    pkg/
    src/
        github.com/
        hyper-carrot/
        go_lib/
            logging/
    ...  

我们可以看到, go get 命令只将代码包下载到了Lib工作区的src目录,而没有进行后续的编译和安装动作。这个加入 -d 标记的结果。

再来看 -fix 标记。我们知道,绝大多数计算机编程语言在进行升级和演进过程中,不可能保证100%的向后兼容(Backward Compatibility)。在计算机世界中,向后兼容是指在一个程序或者代码库在更新到较新的版本后,用旧的版本程序创建的软件和系统仍能被正常操作或使用,或在旧版本的代码库的基础上编写的程序仍能正常编译运行的能力。Go语言的开发者们已想到了这点,并提供了官方的代码升级工具—— fix fix 工具可以修复因Go语言规范变更而造成的语法级别的错误。关于 fix 工具,我们将放在本节的稍后位置予以说明。

假设我们本机安装的Go语言版本是1.5,但我们的程序需要用到一个很早之前用Go语言的0.9版本开发的代码包。那么我们在使用 go get 命令的时候可以加入 -fix 标记。这个标记的作用是在检出代码包之后,先对该代码包中不符合Go语言1.5版本的语言规范的语法进行修正,然后再下载它的依赖包,最后再对它们进行编译和安装。

标记 -u 的意图和执行的动作都比较简单。我们在执行 go get 命令时加入 -u 标记就意味着,如果在本地工作区中已存在相关的代码包,那么就是用对应的代码版本控制系统的更新命令更新它,并进行编译和安装。这相当于强行更新指定的代码包及其依赖包。我们来看如下示例:

 hc@ubt:~$ go get -v github.com/hyper-carrot/go_lib/logging  

因为我们在之前已经检出并安装了这个代码包,所以我们执行上面这条命令后什么也没发生。还记得加入标记 -v 标记意味着会打印出被构建的代码包的名字吗?现在我们使用标记 -u 来强行更新代码包:

 hc@ubt:~$ go get -v -u  github.com/hyper-carrot/go_lib/logging
github.com/hyper-carrot/go_lib (download)  

其中,“(download)”后缀意味着命令从远程仓库检出或更新了该行显示的代码包。如果我们要查看附带 -u go get 命令到底做了些什么,还可以加上一个 -x 标记,以打印出用到的命令。读者可以自己试用一下它。

智能的下载

命令 go get 还有一个很值得称道的功能。在使用它检出或更新代码包之后,它会寻找与本地已安装Go语言的版本号相对应的标签(tag)或分支(branch)。比如,本机安装Go语言的版本是1.x,那么 go get 命令会在该代码包的远程仓库中寻找名为“go1”的标签或者分支。如果找到指定的标签或者分支,则将本地代码包的版本切换到此标签或者分支。如果没有找到指定的标签或者分支,则将本地代码包的版本切换到主干的最新版本。

前面我们说在执行 go get 命令时也可以加入 -x 标记,这样可以看到 go get 命令执行过程中所使用的所有命令。不知道读者是否已经自己尝试了。下面我们还是以代码包 github.com/hyper-carrot/go_lib 为例,并且通过之前示例中的命令的执行此代码包已经被检出到本地。这时我们再次更新这个代码包:

 hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib
github.com/hyper-carrot/go_lib (download)
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git fetch
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git show-ref
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git checkout origin/master
WORK=/tmp/go-build034263530  

在上述示例中, go get 命令通过 git fetch 命令将所有远程分支更新到本地,而后有用 git show-ref 命令列出本地和远程仓库中记录的代码包的所有分支和标签。最后,当确定没有名为“go1”的标签或者分支后, go get 命令使用 git checkout origin/master 命令将代码包的版本切换到主干的最新版本。下面,我们在本地增加一个名为“go1”的标签,看看 go get 命令的执行过程又会发生什么改变:

 hc@ubt:~$ cd ~/golang/lib/src/github.com/hyper-carrot/go_lib
hc@ubt:~/golang/lib/src/github.com/hyper-carrot/go_lib$ git tag go1
hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib
github.com/hyper-carrot/go_lib (download)
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git fetch
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git show-ref
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git show-ref tags/go1 origin/go1
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git checkout tags/go1
WORK=/tmp/go-build636338114  

将这两个示例进行对比,我们会很容易发现它们之间的区别。第二个示例的命令执行过程中使用 git show-ref 查看所有分支和标签,当发现有匹配的信息又通过 git show-ref tags/go1 origin/go1 命令进行精确查找,在确认无误后将本地代码包的版本切换到标签“go1”之上。

命令 go get 的这一功能是非常有用的。我们的代码在直接或间接依赖某些同时针对多个Go语言版本开发的代码包时,可以自动的检出其正确的版本。也可以说, go get 命令内置了一定的代码包多版本依赖管理的功能。

到这里,我向大家介绍了 go get 命令的使用方式。 go get 命令与之前介绍的两个命令一样,是我们编写Go语言程序、构建Go语言项目时必不可少的辅助工具。

go clean

执行 go clean 命令会删除掉执行其它命令时产生的一些文件和目录,包括:

  1. 在使用 go build 命令时在当前代码包下生成的与包名同名或者与Go源码文件同名的可执行文件。在Windows下,则是与包名同名或者Go源码文件同名且带有“.exe”后缀的文件。
  2. 在执行 go test 命令并加入 -c 标记时在当前代码包下生成的以包名加“.test”后缀为名的文件。在Windows下,则是以包名加“.test.exe”后缀为名的文件。我们会在后面专门介绍 go test 命令。
  3. 如果执行 go clean 命令时带有标记 -i ,则会同时删除安装当前代码包时所产生的结果文件。如果当前代码包中只包含库源码文件,则结果文件指的就是在工作区的pkg目录的相应目录下的归档文件。如果当前代码包中只包含一个命令源码文件,则结果文件指的就是在工作区的bin目录下的可执行文件。
  4. 还有一些目录和文件是在编译Go或C源码文件时留在相应目录中的。包括:“_obj”和“_test”目录,名称为“_testmain.go”、“test.out”、“build.out”或“a.out”的文件,名称以“.5”、“.6”、“.8”、“.a”、“.o”或“.so”为后缀的文件。这些目录和文件是在执行 go build 命令时生成在临时目录中的。如果你忘记了这个临时目录是怎么回事儿,可以再回顾一下前面关于 go build 命令的介绍。临时目录的名称以 go-build 为前缀。
  5. 如果执行 go clean 命令时带有标记 -r ,则还包括当前代码包的所有依赖包的上述目录和文件。

我们再以goc2p项目的 logging 为例。为了能够反复体现每个标记的作用,我们会使用标记 n 。使用标记 -n 会让命令在执行过程中打印用到的系统命令,但不会真正执行它们。如果想既打印命令又执行命令则需使用标记 -x 。现在我们来试用一下 go clean 命令:

 hc@ubt:~/golang/goc2p/src$ go clean -x logging   
cd /home/hc/golang/goc2p/src/logging
rm -f logging logging.exe logging.test logging.test.exe  

现在,我们加上标记 -i

 hc@ubt:~/golang/goc2p/src$ go clean -x -i logging   
cd /home/hc/golang/goc2p/src/logging
rm -f logging logging.exe logging.test logging.test.exe
rm -f /home/hc/golang/goc2p/pkg/linux_386/logging.a  

如果再加上标记 -r 又会打印出哪些命令呢?请读者自己试一试吧。

go doc与godoc

go doc

go doc 命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。

插播: 所谓Go语言的程序实体,是指变量、常量、函数、结构体以及接口。而程序实体的标识符即是代表它们的名称。标识符又分非限定标识符和限定标识符。其中,限定标识符一般用于表示某个代码包中的程序实体或者某个结构体类型中的方法或字段。例如,标准库代码包 io 中的名为 EOF 的变量用限定标识符表示即 io.EOF 。又例如,如果我有一个 sync.WaitGroup 类型的变量 wg 并且想调用它的 Add 方法,那么可以这样写 wg.Add() 。其中, wg.Add 就是一个限定标识符,而后面的 () 则代表了调用操作。

下面说明怎样使用 go doc 命令。先来看一下 go doc 命令可接受的标记。

表0-5 go doc 命令的标记说明

标记名称

标记描述

-c

加入此标记后会使 go doc 命令区分参数中字母的大小写。默认情况下,命令是大小写不敏感的。

-cmd

加入此标记后会使 go doc 命令同时打印出 main 包中的可导出的程序实体(其名称的首字母大写)的文档。默认情况下,这部分文档是不会被打印出来的。

-u

加入此标记后会使 go doc 命令同时打印出不可导出的程序实体(其名称的首字母小写)的文档。默认情况下,这部分文档是不会被打印出来的。

这几个标记的意图都非常简单和明确,大家可以根据实际情况选用。

go doc 命令可以后跟一个或两个参数。当然,我们也可以不附加任务参数。如果不附加参数,那么 go doc 命令会试图打印出当前目录所代表的代码包的文档及其中的包级程序实体的列表。

例如,我要在goc2p项目的 loadgen 代码包所在目录中运行 go doc 命令的话,那么就会是这样:

 hc@ubt:~/golang/goc2p/src/loadgen$ go doc
package loadgen // import "loadgen"

func NewGenerator(
    caller lib.Caller,
    timeoutNs time.Duration,
    lps uint32,
    durationNs time.Duration,
    resultCh chan *lib.CallResult) (lib.Generator, error)  

如果你需要指定代码包或程序实体,那么就需要在 go doc 命令后附上参数了。例如,只要我本地的goc2p项目的所在目录存在于GOPATH环境变量中,我就可以在任意目录中敲入 go doc loadgen 。如此得到的输出一定是与上面那个示例一致的。

看过 loadgen 代码包中源码的读者会知道,其中只有一个可导出的程序实体,即 NewGenerator 函数。这也是上述示例中如此输出的原因。该代码包中的结构体类型 myGenerator 是不可导出,但是我们只需附加 -u 标记便可查看它的文档了:

 hc@ubt:~$ go doc -u loadgen.myGenerator
type myGenerator struct {
    caller      lib.Caller           // 调用器。
    timeoutNs   time.Duration        // 处理超时时间,单位:纳秒。
    lps         uint32               // 每秒载荷量。
    durationNs  time.Duration        // 负载持续时间,单位:纳秒。
    concurrency uint32               // 并发量。
    tickets     lib.GoTickets        // Goroutine票池。
    stopSign    chan byte            // 停止信号的传递通道。
    cancelSign  byte                 // 取消发送后续结果的信号。
    endSign     chan uint64          // 完结信号的传递通道,同时被用于传递调用执行计数。
    callCount   uint64               // 调用执行计数。
    status      lib.GenStatus        // 状态。
    resultCh    chan *lib.CallResult // 调用结果通道。
}

    载荷发生器的实现。

func (gen *myGenerator) Start()
func (gen *myGenerator) Status() lib.GenStatus
func (gen *myGenerator) Stop() (uint64, bool)
func (gen *myGenerator) asyncCall()
func (gen *myGenerator) genLoad(throttle <-chan time.Time)
func (gen *myGenerator) handleStopSign(callCount uint64)
func (gen *myGenerator) init() error
func (gen *myGenerator) interact(rawReq *lib.RawReq) *lib.RawResp
func (gen *myGenerator) sendResult(result *lib.CallResult) bool  

如此一来, loadgen.myGenerator 类型的文档、字段和方法都尽收眼底。注意,这里我们使用到了限定标识符。下面再进一步,如果你只想查看 loadgen.myGenerator 类型的 init 方法的文档,那么只要续写这个限定标识符就可以了,像这样:

 hc@ubt:~$ go doc -u loadgen.myGenerator.init
func (gen *myGenerator) init() error

    初始化载荷发生器。  

注意,结构体类型中的字段的文档是无法被单独打印的。另外, go doc 命令根据参数查找代码包或程序实体的顺序是:先Go语言根目录(即GOROOT所环境变量指定的那个目录)后工作区目录(即GOPATH环境变量包含的那些目录)。并且,在前者或后者中, go doc 命令的查找顺序遵循字典序。因此,如果某个工作区目录中的代码包与标准库中的包重名了,那么它是无法被打印出来的。 go doc 命令只会打印出第一个匹配的代码包或程序实体的文档。

我们在前面说过, go doc 命令还可以接受两个参数。这是一种更加精细的指定代码包或程序实体的方式。一个显著的区别是,如果你想打印标准库代码包 net/http 中的结构体类型 Request 的文档,那么可以这样敲入 go doc 命令:

 go doc http.Request  

注意,这里并没有写入 net/http 代码包的导入路径,而只是写入了其中的最后一个元素 http 。但是如果你把 http.Request 拆成两个参数(即 http Request )的话,命令程序就会什么也查不到了。因为这与前一种用法的解析方式是不一样的。正确的做法是,当你指定两个参数时,作为第一个参数的代码包名称必须是完整的导入路径,即:在敲入命令 go doc net/http Request 后,你会得到想要的结果。

最后,在给定两个参数时, go doc 会打印出所有匹配的文档,而不是像给定一个参数时那样只打印出第一个匹配的文档。这对于查找只有大小写不同的多个方法(如 New new )的文档来说非常有用。

godoc

命令 godoc 是一个很强大的工具,同样用于展示指定代码包的文档。在Go语言的1.5版本中,它是一个内置的标准命令。

该命令有两种模式可供选择。如果在执行命令时不加入 -http 标记,则该命令就以命令行模式运行。在打印纯文本格式的文档到标准输出后,命令执行就结束了。比如,我们用命令行模式查看代码包fmt的文档:

 hc@ubt:~$ godoc fmt  

为了节省篇幅,我们在这里略去了文档查询结果。读者可以自己运行一下上述命令。在该命令被执行之后,我们就可以看到编排整齐有序的文档内容了。这包括代码包 fmt 及其中所有可导出的包级程序实体的声明、文档和例子。

有时候我们只是想查看某一个函数或者结构体类型的文档,那么我们可以将这个函数或者结构体的名称加入命令的后面,像这样:

 hc@ubt:~$ godoc fmt Printf  

或者:

 hc@ubt:~$ godoc os File  

如果我们想同时查看一个代码包中的几个函数的文档,则仅需将函数或者结构体名称追加到命令后面。比如我们要查看代码包 fmt 中函数 Printf 和函数 Println 的文档:

 hc@ubt:~$ godoc fmt Printf Println  

如果我们不但想在文档中查看可导出的程序实体的声明,还想看到它们的源码,那么我们可以在执行 godoc 命令的时候加入标记 -src ,比如这样:

 hc@ubt:~$ godoc -src fmt Printf  

Go语言为程序使用示例代码设立了专有的规则。我们在这里暂不讨论这个规则的细节。只需要知道正因为有了这个专有规则,使得 godoc 命令可以根据这些规则提取相应的示例代码并把它们加入到对应的文档中。如果我们想在查看代码包 net 中的结构体类型 Listener 的文档的同时查看关于它的示例代码,那么我们只需要在执行命令时加入标记 -ex 。使用方法如下:

 hc@ubt:~$ godoc -ex net/http FileServer  

注意,我们在使用 godoc 命令时,只能把代码包和程序实体的标识符拆成两个参数。也就是说, godoc 命令不支持前文所述的 go doc 命令的单参数用法。

在实际的Go语言环境中,我们可能会遇到一个命令源码文件所产生的可执行文件与代码包重名的情况。比如,这里介绍的标准命令 go 和官方代码包 go 。现在我们要明确的告诉 godoc 命令要查看可执行文件go的文档,我们需要在名称前加入 cmd/ 前缀:

 hc@ubt:~$ godoc cmd/go  

另外,如果我们想查看HTML格式的文档,就需要加入标记 -html 。当然,这样在命令行模式下的查看效果是很差的。但是,如果仔细查看的话,可以在其中找到一些相应源码的链接地址。

一般情况下, godoc 命令会去Go语言根目录和环境变量GOPATH包含的工作区目录中查找代码包。我们可以通过加入标记 -goroot 来制定一个Go语言根目录。这个被指定的Go语言根目录仅被用于当次命令的执行。示例如下:

 hc@ubt:~$ godoc -goroot="/usr/local/go" fmt  

现在让我们来看看另外一种模式。如果我们在执行命令时加上 -http 标记则会启用另一模式。这种模式被叫做Web服务器模式,它以Web页面的形式提供Go语言文档。

我们使用如下命令启动这个文档Web服务器:

 hc@ubt:~/golang/goc2p$ godoc -http=:6060  

标记 -http 的值 :6060 表示启动的Web服务器使用本机的6060端口。之后,我们就可以通过在网络浏览器的地址栏中输入来查看以网页方式展现的Go文档了。

图0-1 本机的Go文档Web服务首页

这与Go语言官方站点的Web服务页面如出一辙。这使得我们在不方便访问Go语言官方站点的情况下也可以查看Go语言文档。并且,更便利的是,通过本机的Go文档Web服务,我们还可以查看所有本机工作区下的代码的文档。比如,goc2p项目中的代码包 pkgtool 的页面如下图:

图0-2 goc2p项目中的pkgtool包的Go文档页面

现在,我们在本机开启Go文档Web服务器,端口为9090。命令如下:

 hc@ubt:~$ godoc -http=:9090 -index  

注意,要使用 -index 标记开启搜索索引。这个索引会在服务器启动时创建并维护。如果不加入此标记,那么无论在Web页面还是命令行终端中都是无法进行查询操作的。

索引中提供了标识符和全文本搜索信息(通过正则表达式为可搜索性提供支持)。全文本搜索结果显示条目的最大数量可以通过标记 -maxresults 提供。标记 -maxresults 默认值是10000。如果不想提供如此多的结果条目,可以设置小一些的值。甚至,如果不想提供全文本搜索结果,可以将标记 -maxresults 的值设置为0,这样服务器就只会创建标识符索引,而根本不会创建全文本搜索索引了。标识符索引即为对程序实体名称的索引。

正因为在使用了 -index 标记的情况下文档服务器会在启动时创建索引,所以在文档服务器启动之后还不能立即提供搜索服务,需要稍等片刻。在索引为被创建完毕之前,我们的搜索操作都会得到提示信息“Indexing in progress: result may be inaccurate”。

如果我们在本机用 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 fmt  

命令 godoc 还有很多可用的标记,但在通常情况下并不常用。读者如果有兴趣,可以在命令行环境下敲入 godoc 并查看其文档。

go run

go run 命令可以编译并运行命令源码文件。由于它其中包含了编译动作,因此它也可以接受所有可用于 go build 命令的标记。除了标记之外, go run 命令只接受Go源码文件作为参数,而不接受代码包。与 go build 命令和 go install 命令一样, go run 命令也不允许多个命令源码文件作为参数,即使它们在同一个代码包中也是如此。而原因也是一致的,多个命令源码文件会都有main函数声明。

如果命令源码文件可以接受参数,那么在使用 go run 命令运行它的时候就可以把它的参数放在它的文件名后面,像这样:

 hc@ubt:~/golang/goc2p/src/helper/ds$ go run showds.go -p ~/golang/goc2p  

在上面的示例中,我们使用 go run 命令运行命令源码文件showds.go。这个命令源码文件可以接受一个名称为“p”的参数。我们用“-p”这种形式表示“p”是一个参数名而不是参数值。它与源码文件名之间需要用空格隔开。参数值会放在参数名的后面,两者成对出现。它们之间也要用空格隔开。如果有第二个参数,那么第二个参数的参数名与第一个参数的参数值之间也要有一个空格。以此类推。

go run 命令只能接受一个命令源码文件以及若干个库源码文件(必须同属于 main 包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么 go run 命令就只会打印错误提示信息并退出,而不会继续执行。

在通过参数检查后, go run 命令会将编译参数中的命令源码文件,并把编译后的可执行文件存放到临时工作目录中。

编译和运行过程

为了更直观的体现出 go run 命令中的操作步骤,我们在执行命令时加入标记 -n ,用于打印相关命令而不实际执行。现在让我们来模拟运行goc2p项目中的代码包helper/ds的命令源码文件showds.go。示例如下:

 hc@ubt:~/golang/goc2p/src/helper/ds$ go run -n showds.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/go1.5/pkg/tool/linux_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid df49387da030ad0d3bebef3f046d4013f8cb08d3 -D _/home/hc/golang/goc2p/src/helper/ds -I $WORK -pack ./showds.go
cd .
/usr/local/go1.5/pkg/tool/linux_amd64/link -o $WORK/command-line-arguments/_obj/exe/showds -L $WORK -w -extld=clang -buildmode=exe -buildid=df49387da030ad0d3bebef3f046d4013f8cb08d3 $WORK/command-line-arguments.a
$WORK/command-line-arguments/_obj/exe/showds  

在上面的示例中并没有显示针对命令源码文件showds.go的依赖包进行编译和运行的相关打印信息。这是因为该源码文件的所有依赖包已经在之前被编译过了。

现在,我们来逐行解释这些被打印出来的信息。

以前缀“#”开始的是注释信息。我们看到信息中有三行注释信息,并在中间行出现了内容“command-line-arguments”。我们在讲 go build 命令的时候说过,编译命令在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包时,会在内部生成一个名为“command-line-arguments”的虚拟代码包。所以这里的注释信息就是要告诉我们下面的几行信息是关于虚拟代码包“command-line-arguments”的。

打印信息中的“$WORK”表示临时工作目录的绝对路径。为了存放对虚拟代码包“command-line-arguments”的编译结果,命令在临时工作目录中创建了名为command-line-arguments的子目录,并在其下又创建了_obj子目录和_obj/exe子目录。

然后,命令程序使用Go语言工具目录 compile 命令对命令源码文件showds.go进行了编译,并把结果文件存放到了$WORK目录下,名为command-line-arguments.a。其中, compile 是Go语言自带的编程工具。

在编译成功之后,命令程序使用链接命令 link 生成最终的可执行文件,并将其存于$WORK/command-line-arguments/_obj/exe/目录中。打印信息中的最后一行表示,命令运行了生成的可执行文件。

通过对这些打印出来的命令的解读,我们了解了临时工作目录的用途以和内容。

在上面的示例中,我们只是让 go run 命令打印出运行命令源码文件showds.go过程中需要执行的命令,而没有真正运行它。如果我们想真正运行命令源码文件showds.go并且想知道临时工作目录的位置,就需要去掉标记 -n 并且加上标记 -work 。当然,如果依然想看到过程中执行的命令,可以加上标记 -x 。如果读者已经看过之前我们对 go build 命令的介绍,就应该知道标记 -x 与标记 -n 一样会打印出过程执行的命令,但不同的这些命令会被真正的执行。调整这些标记之后的命令就像这样:

 hc@ubt:~/golang/goc2p/src/helper/ds$ go run -x -work showds.go  

当命令真正执行后,临时工作目录中就会出现实实在在的内容了,像这样:

 /tmp/go-build604555989:
  command-line-arguments/
    _obj/
      exe/
        showds
  command-line-arguments.a  

由于上述命令中包含了 -work 标记,所以我们可以从其输出中找到实际的工作目录(这里是/tmp/go-build604555989)。有意思的是,我们恰恰可以通过运行命令源码文件showds.go来查看这个临时工作目录的目录树:

 hc@ubt:~/golang/goc2p/src/helper/ds$ go run showds.go -p /tmp/go-build604555989  

读者可以自己试一试。

我们在前面介绍过,命令源码文件如果可以接受参数,则可以在执行 go run 命令运行这个命令源码文件时把参数名和参数值成对的追加在后面。实际上,如果在命令后追加参数,那么在最后执行生成的可执行文件的时候也会追加一致的参数。例如,如果这样执行命令:

 hc@ubt:~/golang/goc2p/src/helper/ds$ go run -n showds.go -p ~/golang/goc2p  

那么带 -x -n 标记的命令程序打印的最后一个命令就是:

 $WORK/command-line-arguments/_obj/exe/showds -p /home/hc/golang/goc2p  

可见, go run 命令会把追加到命令源码文件后面的参数原封不动的传给对应的可执行文件。

以上简要展示了一个命令源码文件从编译到运行的全过程。请记住, go run 命令包含了两个动作:编译命令源码文件和运行对应的可执行文件。

go test

go test 命令用于对Go语言编写的程序进行测试。这种测试是以代码包为单位的。当然,这还需要测试源码文件的帮助。关于怎样编写并写好Go程序测试代码,我们会在本章的第二节加以详述。在这里,我们只讨论怎样使用命令启动测试。

go test 命令会自动测试每一个指定的代码包。当然,前提是指定的代码包中存在测试源码文件。关于测试源码文件方面的知识,在我的图书《Go并发编程实战》中有详细介绍。测试源码文件是名称以“_test.go”为后缀的、内含若干测试函数的源码文件。测试函数一般是以“Test”为名称前缀并有一个类型为“testing.T”的参数声明的函数.

现在,我们来测试goc2p项目中的几个代码包。在使用 go test 命令时指定代码包的方式与其他命令无异——使用代码包导入路径。如果需要测试多个代码包,则需要在它们的导入路径之间加入空格以进行分隔。示例如下:

 hc@ubt:~$ go test basic cnet/ctcp pkgtool
ok      basic    0.012s
ok      cnet/ctcp    2.014s
ok      pkgtool    0.014s  

go test 命令在执行完所有的代码包中的测试文件之后,会以代码包为单位打印出测试概要信息。在上面的示例中,对应三个代码包的三行信息的第一列都是“ok”。这说明它们都通过了测试。每行的第三列显示运行相应测试所用的时间,以秒为单位。我们还可以在代码包目录下运行不加任何参数的运行 go test 命令。其作用和结果与上面的示例是一样的。

另外,我们还可以指定测试源码文件来进行测试。这样的话, go test 命令只会执行指定文件中的测试,像这样:

     hc@ubt:~/golang/goc2p/src/pkgtool$ go test envir_test.go
# command-line-arguments
./envir_test.go:25: undefined: GetGoroot
./envir_test.go:40: undefined: GetAllGopath
./envir_test.go:81: undefined: GetSrcDirs
./envir_test.go:83: undefined: GetAllGopath
./envir_test.go:90: undefined: GetGoroot
FAIL    command-line-arguments [build failed]  

我们看到,与指定源码文件进行编译或运行一样,命令程序会为指定的源码文件生成一个虚拟代码包——“command-line-arguments”。但是,测试并没有通过。但其原因并不是测试失败,而是编译失败。对于运行这次测试的命令程序来说,测试源码文件envir_test.go是属于代码包“command-line-arguments”的。并且,这个测试源码文件中使用了库源码文件envir.go中的函数。但是,它却没有显示导入这个库源码文件所属的代码包。这显然会引起编译错误。如果想解决这个问题,我们还需要在执行命令时加入这个测试源码文件所测试的那个源码文件。示例如下:

 hc@ubt:~/golang/goc2p/src/pkgtool$ go test envir_test.go envir.go
ok      command-line-arguments    0.010s  

现在,我们故意使代码包 pkgtool 中的某个测试失败。现在我们再来运行测试:

 hc@ubt:~$ go test basic cnet/ctcp pkgtool
ok      basic   0.010s
ok      cnet/ctcp       2.015s
--- FAIL: TestGetSrcDirs (0.00 seconds)
        envir_test.go:85: Error: The src dir '/usr/local/go/src/pkg' is incorrect.
FAIL
FAIL    pkgtool 0.009s  

我们通过以上示例中的概要信息获知,测试源码文件中envir_test.go的测试函数 TestGetSrcDirs 中的测试失败了。在包含测试失败的测试源码文件名的那一行信息中,紧跟测试源码文件名的用冒号分隔的数字是错误信息所处的行号,在行号后面用冒号分隔的是错误信息。这个错误信息的内容是用户自行编写的。另外,概要信息的最后一行以“FAIL”为前缀。这表明针对代码包pkgtool的测试未通过。未通过的原因在前面的信息中已有描述。

一般情况下,我们会把测试的源码文件与被测试的源码文件放在同一个代码包中。并且,这些源码文件中声明的包名也都是相同的。除此之外我们还有一种选择,那就是测试源码文件中声明的包名可以是所属包名再加“_test”后缀。我们把这种测试源码文件叫做包外测试源码文件。不过,包外测试源码文件存在一个弊端,那就是在它们的测试函数中无法测试被测源码文件中的包级私有的程序实体,比如包级私有的变量、函数和结构体类型。这是因为这两者的所属代码包是不相同的。所以,我们一般很少会编写包外测试源码文件。

关于标记

go test 命令的标记处理部分是庞大且繁杂的,以至于使Go语言的开发者们不得不把这一部分的逻辑从 go test 命令程序主体中分离出来并建立单独的源码文件。因为 go test 命令中包含了编译动作,所以它可以接受可用于 go build 命令的所有标记。另外,它还有很多特有的标记。这些标记的用于控制命令本身的动作,有的用于控制和设置测试的过程和环境,还有的用于生成更详细的测试结果和统计信息。

可用于 go test 命令的几个比较常用的标记是 -c -i -o 。这两个就是用于控制 go test 命令本身的动作的标记。详见下表。

表0-6 go test 命令的标记说明

标记名称

标记描述

-c

生成用于运行测试的可执行文件,但不能执行它。这个可执行文件会被命名为“pkg.test”,其中的“pkg”即为被测试代码包的导入路径的最后一个元素的名称。

-i

安装/重新安装运行测试所需的依赖包,但不编译和运行测试代码。

-o

指定用于运行测试的可执行文件的名称。追加该标记不会影响测试代码的运行,除非同时追加了标记 -c -i

上述这几个标记可以搭配使用。搭配使用的目的可以是让 go test 命令既安装依赖包又编译测试代码,但不运行测试。也就是说,让命令程序跑一遍运行测试之前的所有流程。这可以测试一下测试过程。注意,在加入 -c 标记后,命令程序会把用于运行测试的可执行文件存放到当前目录下。

go list

go list 命令的作用是列出指定的代码包的信息。与其他命令相同,我们需要以代码包导入路径的方式给定代码包。被给定的代码包可以有多个。这些代码包对应的目录中必须直接保存有Go语言源码文件,其子目录中的文件不算在内。否则,代码包将被看做是不完整的。现在我们来试用一下:

 hc@ubt:~$ go list cnet/ctcp pkgtool
cnet/ctcp
pkgtool  

我们看到,在不加任何标记的情况下,命令的结果信息中只包含了我们指定的代码包的导入路径。我们刚刚提到,作为参数的代码包必须是完整的代码包。例如:

 hc@ubt:~$ go list cnet pkgtool
can't load package: package cnet: no buildable Go source files in /home/hc/golang/goc2p/src/cnet/
pkgtool  

这时, go list 命令报告了一个错误——代码包 cnet 对应的目录下没有Go源码文件。但是命令还是把代码包pkgtool的导入路径打印出来了。然而,当我们在执行 go list 命令并加入标记 -e 时,即使参数中包含有不完整的代码包,命令也不会提示错误。示例如下:

 hc@ubt:~$ go list -e cnet pkgtool
cnet
pkgtool  

标记 -e 的作用是以容错模式加载和分析指定的代码包。在这种情况下,命令程序如果在加载或分析的过程中遇到错误只会在内部记录一下,而不会直接把错误信息打印出来。我们为了看到错误信息可以使用 -json 标记。这个标记的作用是把代码包的结构体实例用JSON的样式打印出来。

这里解释一下,JSON的全称是Javascript Object Notation。它一种轻量级的承载数据的格式。JSON的优势在于语法简单、短小精悍,且非常易于处理。JSON还是一种纯文本格式,独立于编程语言。正因为如此,得到了绝大多数编程语言和浏览器的支持,应用非常广泛。Go语言当然也不例外,在它的标准库中有专门用于处理和转换JSON格式的数据的代码包 encoding/json 。关于JSON格式的具体内容,读者可以去它的官方网站查看说明。

在了解了这些基本概念之后,我们来试用一下 -json 标记。示例如下:

 hc@ubt:~$ go list -e -json cnet
    {
            "Dir": "/home/hc/golang/goc2p/src/cnet",
            "ImportPath": "cnet",
            "Stale": true,
            "Root": "/home/hc/golang/goc2p",
            "Incomplete": true,
            "Error": {
                    "ImportStack": [
                            "cnet"
                    ],
                    "Pos": "",
                    "Err": "no Go source files in /home/hc/golang/goc2p/src/cnet"
            }
    }  

在上述JSON格式的代码包信息中,对于结构体中的字段的显示是不完整的。因为命令程序认为我们指定 cnet 就是不完整的。在名为 Error 的字段中,我们可以看到具体说明。 Error 字段的内容其实也是一个结构体。在JSON格式下,这种嵌套的结构体被完美的展现了出来。 Error 字段所指代的结构体实例的 Err 字段说明了 cnet 不完整的原因。这与我们在没有使用 -e 标记的情况下所打印出来的错误提示信息是一致的。我们再来看 Incomplete 字段。它的值为 true 。这同样说明 cnet 是一个不完整的代码包。

实际上,在从这个代码包结构体实例到JSON格式文本的转换过程中,所有的值为其类型的空值的字段都已经被忽略了。

现在我们使用带 -json 标记的 go list 命令列出代码包 cnet/ctcp 的信息:

 hc@ubt:~$ go list -json cnet/ctcp
{
    "Dir": "/home/hc/golang/github/goc2p/src/cnet/ctcp",
    "ImportPath": "cnet/ctcp",
    "Name": "ctcp",
    "Target": "/home/hc/golang/github/goc2p/pkg/darwin_amd64/cnet/ctcp.a",
    "Stale": true,
    "Root": "/home/hc/golang/github/goc2p",
    "GoFiles": [
        "base.go",
        "tcp.go"
    ],
    "Imports": [
        "bufio",
        "bytes",
        "errors",
        "logging",
        "net",
        "sync",
        "time"
    ],
    "Deps": [
        "bufio",
        "bytes",
        "errors",
        "fmt",
        "internal/singleflight",
        "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",
        "runtime",
        "strings",
        "sync",
        "testing",
        "time"
    ]
}  

由于 cnet/ctcp 包是一个完整有效的代码包,所以我们不使用 -e 标记也是没有问题的。在上面打印的 cnet/ctcp 包的信息中没有 Incomplete 字段。这是因为完整的代码包中的 Incomplete 字段的其类型的空值 false 。它已经在转换过程中被忽略掉了。另外,在 cnet/ctcp 包的信息中我们看到了很多其它的字段。现在我就来看看在Go命令程序中的代码包结构体都有哪些公开的字段。如下表。

表0-7 代码表结构体中的基本字段

字段名称

字段类型

字段描述

Dir

字符串(string)

代码包对应的目录。

ImportPath

字符串(string)

代码包的导入路径。

ImportComment

字符串(string)

代码包声明语句右边的用于自定义导入路径的注释。

Name

字符串(string)

代码包的名称。

Doc

字符串(string)

代码包的文档字符串。

Target

字符串(string)

代码包的安装路径。

Shlib

字符串(string)

包含该代码包的共享库(shared library)的名称。

Goroot

布尔(bool)

该代码包是否在Go语言安装目录下。

Standard

布尔(bool)

该代码包是否属于标准库的一部分。

Stale

布尔(bool)

该代码包的最新代码是否未被安装。

Root

字符串(string)

该代码包所属的工作区或Go安装目录的路径。

表0-8 代码包结构体中与源码文件有关的字段

字段名称

字段类型

字段描述

GoFiles

字符串切片([]string)

Go源码文件的列表。不包含导入了代码包“C”的源码文件和测试源码文件。

CgoFiles

字符串切片([]string)

导入了代码包“C”的源码文件的列表。

IgnoredGoFiles

字符串切片([]string)

忽略编译的源码文件的列表。

CFiles

字符串切片([]string)

名称中有“.c”后缀的源码文件的列表。

CXXFiles

字符串切片([]string)

名称中有“.cc”、“.cxx”或“.cpp”后缀的源码文件的列表。

MFiles

字符串切片([]string)

名称中“.m”后缀的源码文件的列表。

HFiles

字符串切片([]string)

名称中有“.h”后缀的源码文件的列表。

SFiles

字符串切片([]string)

名称中有“.s”后缀的源码文件的列表。

SwigFiles

字符串切片([]string)

名称中有“.swig”后缀的文件的列表。

SwigCXXFiles

字符串切片([]string)

名称中有“.swigcxx”后缀的文件的列表。

SysoFiles

字符串切片([]string)

名称中有“.syso”后缀的文件的列表。这些文件是需要被加入到归档文件中的。

表0-9 代码包结构体中与Cgo指令有关的字段

字段名称

字段类型

字段描述

CgoCFLAGS

字符串切片([]string)

需要传递给C编译器的标记的列表。针对Cgo。

CgoCPPFLAGS

字符串切片([]string)

需要传递给C预处理器的标记的列表。针对Cgo。

CgoCXXFLAGS

字符串切片([]string)

需要传递给C++编译器的标记的列表。针对Cgo。

CgoLDFLAGS

字符串切片([]string)

需要传递给链接器的标记的列表。针对Cgo。

CgoPkgConfig

字符串切片([]string)

pkg-config的名称的列表。针对Cgo。

表0-10 代码包结构体中与依赖信息有关的字段

字段名称

字段类型

字段描述

Imports

字符串切片([]string)

该代码包中的源码文件显式导入的依赖包的导入路径的列表。

Deps

字符串切片([]string)

所有的依赖包(包括间接依赖)的导入路径的列表。

表0-11 代码包结构体中与错误信息有关的字段

字段名称

字段类型

字段描述

Incomplete

布尔(bool)

代码包是否是完整的,也即在载入或分析代码包及其依赖包时是否有错误发生。

Error

*PackageError类型

载入或分析代码包时发生的错误。

DepsErrors

[]*PackageError类型

载入或分析依赖包时发生的错误。

表0-12 代码包结构体中与测试源码文件有关的字段

字段名称

字段类型

字段描述

TestGoFiles

字符串切片([]string)

代码包中的测试源码文件的名称列表。

TestImports

字符串切片([]string)

代码包中的测试源码文件显示导入的依赖包的导入路径的列表。

XTestGoFiles

字符串切片([]string)

代码包中的外部测试源码文件的名称列表。

XTestImports

字符串切片([]string)

代码包中的外部测试源码文件显示导入的依赖包的导入路径的列表。

代码包结构体中定义的字段很多,但有些时候我们只需要查看其中的一些字段。那要怎么做呢?标记 -f 可以满足这个需求。比如这样:

 hc@ubt:~$ go list -f {{.ImportPath}} cnet/ctcp
cnet/ctcp  

实际上, -f 标记的默认值就是 {{.ImportPath}} 。这也正是我们在使用不加任何标记的 go list 命令时依然能看到指定代码包的导入路径的原因了。

标记 -f 的值需要满足标准库的代码包 `text/template 中定义的语法。比如, {{.S}} 代表根结构体的 S 字段的值。在 go list 命令的场景下,这个根结构体就是指定的代码包所对应的结构体。如果 S 字段的值也是一个结构体的话,那么 {{.S.F}} 就代表根结构体的 S 字段的值中的 F 字段的值。如果我们要查看 cnet/ctcp 包中的命令源码文件和库源码文件的列表,可以这样使用 -f 标记:

 hc@ubt:~$ go list -f {{.GoFiles}} cnet/ctcp
[base.go tcp.go]  

如果我们想查看不完整的代码包 cnet 的错误提示信息,还可以这样:

 hc@ubt:~$ go list -e -f {{.Error.Err}} cnet
no buildable Go source files in /home/hc/golang/goc2p/src/cnet  

我们还可以利用代码包 text/template 中定义的强大语法让 go list 命令输出定制化更高的代码包信息。比如:

 hc@ubt:~$ go list -e -f 'The package {{.ImportPath}} is {{if .Incomplete}}incomplete!{{else}}complete.{{end}}' cnet
The package cnet is incomplete!

```bash    
hc@ubt:~$ go list -f 'The imports of package {{.ImportPath}} is [{{join .Imports ", "}}].' cnet/ctcp
The imports of package cnet/ctcp is [bufio, bytes, errors, logging, net, sync, time].  

其中, join 是命令程序在 text/template 包原有语法之上自定义的语法,在底层使用标准库代码包 strings 中的 Join 函数。关于更多的语法规则,请读者查看代码包 text/template 的相关文档。

另外, -tags 标记也可以被 go list 接受。它与我们在讲 go build 命令时提到的 -tags 标记是一致的。读者可以查看代码包`go/build“的文档以了解细节。

go list 命令很有用。它可以为我们提供指定代码包的更深层次的信息。这些信息往往是我们无法从源码文件中直观看到的。

文章来源:智云一二三科技

文章标题:GO 编程:Golang标准命令详解(一)

文章地址:https://www.zhihuclub.com/86570.shtml

关于作者: 智云科技

热门文章

网站地图