本章主要内容
我们在第2章概览了Go语言的语法和语言结构。本章会进一步介绍如何把代码组织成包,以及如何操作这些包。在Go语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能。这样做,能够更好地复用代码,并对每个包内的数据的使用有更好的控制。
在进入具体细节之前,假设读者已经熟悉命令行提示符,或者操作系统的shell,而且应该已经在本书前言的帮助下,安装了Go。如果上面这些都准备好了,就让我们开始进入细节,了解什么是包,以及包为什么对Go语言的生态非常重要。
所有Go语言的程序都会组织成若干组文件,每组文件被称为一个包。这样每个包的代码都可以作为很小的复用单元,被其他项目引用。让我们看看标准库中的http包是怎么利用包的特性组织功能的:
net/http/
cgi/
cookiejar/
testdata/
fcgi/
httptest/
httputil/
pprof/
testdata/这些目录包括一系列以.go为扩展名的相关文件。这些目录将实现HTTP服务器、客户端、测试工具和性能调试工具的相关代码拆分成功能清晰的、小的代码单元。以cookiejar包为例,这个包里包含与存储和获取网页会话上的cookie相关的代码。每个包都可以单独导入和使用,以便开发者可以根据自己的需要导入特定功能。例如,如果要实现HTTP客户端,只需要导入http包就可以。
所有的.go文件,除了空行和注释,都应该在第一行声明自己所属的包。每个包都在一个单独的目录里。不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。这意味着,同一个目录下的所有.go文件必须声明同一个包名。
给包命名的惯例是使用包所在目录的名字。这让用户在导入包的时候,就能清晰地知道包名。我们继续以net/http包为例,在http目录下的所有文件都属于http包。给包及其目录命名时,应该使用简洁、清晰且全小写的名字,这有利于开发时频繁输入包名。例如,net/http包下面的包,如cgi、httputil和pprof,名字都很简洁。
记住,并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。这个特性在需要导入不同目录的同名包时很有用。3.2节会展示如何修改导入的包名。
main包在Go语言里,命名为main的包具有特殊的含义。Go语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用Go语言编译的可执行程序都必须有一个名叫main的包。
当编译器发现某个包的名字为main时,它一定也会发现名为main()的函数,否则不会创建可执行文件。main()函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。程序编译时,会使用声明main包的代码所在的目录的目录名作为二进制可执行文件的文件名。
命令和包 Go文档里经常使用命令(command)这个词来指代可执行程序,如命令行应用程序。这会让新手在阅读文档时产生困惑。记住,在Go语言里,命令是指任何可执行程序。作为对比,包更常用来指语义上可导入的功能单元。
让我们来实际体验一下。首先,在$GOPATH/src/hello/目录里创建一个叫hello.go的文件,并输入代码清单3-1里的内容。这是个经典的“Hello World!”程序,不过,注意一下包的声明以及import语句。
代码清单3-1 经典的“Hello World!”程序
01 package main
02
03 import "fmt" ●――――fmt包提供了完成格式化输出的功能。
04
05 func main() {
06 fmt.Println("Hello World!")
07 }
获取包的文档 别忘了,可以访问http://golang.org/pkg/fmt/或者在终端输入
godoc fmt来了解更多关于fmt包的细节。
保存了文件后,可以在$GOPATH/src/hello/目录里执行命令go build。这条命令执行完后,会生成一个二进制文件。在UNIX、Linux和Mac OS X系统上,这个文件会命名为hello,而在Windows系统上会命名为hello.exe。可以执行这个程序,并在控制台上显示“Hello World!”。
如果把这个包名改为main之外的某个名字,如hello,编译器就认为这只是一个包,而不是命令,如代码清单3-2所示。
代码清单3-2 包含main函数的无效的Go程序
01 package hello
02
03 import "fmt"
04
05 func main(){
06 fmt.Println("Hello, World!")
07 }我们已经了解如何把代码组织到包里,现在让我们来看看如何导入这些包,以便可以访问包内的代码。import语句告诉编译器到磁盘的哪里去找想要导入的包。导入包需要使用关键字import,它会告诉编译器你想引用该位置的包内的代码。如果需要导入多个包,习惯上是将import语句包装在一个导入块中,代码清单3-3展示了一个例子。
代码清单3-3 import声明块
import (
"fmt"
"strings" ●――――strings包提供了很多关于字符串的操作,如查找、替换或者变换。可以通过访问http://golang.org/pkg/strings/或者在终端运行godoc strings来了解更多关于strings包的细节。
)
编译器会使用Go环境变量设置的路径,通过引入的相对路径来查找磁盘上的包。标准库中的包会在安装Go的位置找到。Go开发者创建的包会在GOPATH环境变量指定的目录里查找。GOPATH指定的这些目录就是开发者的个人工作空间。
举个例子。如果Go安装在/usr/local/go,并且环境变量GOPATH设置为/home/myproject:/home/ mylibraries,编译器就会按照下面的顺序查找net/http包:
/usr/local/go/src/pkg/net/http ●――――这就是标准库源代码所在的位置。
/home/myproject/src/net/http
/home/mylibraries/src/net/http
一旦编译器找到一个满足import语句的包,就停止进一步查找。有一件重要的事需要记住,编译器会首先查找Go的安装目录,然后才会按顺序查找GOPATH变量里列出的目录。
如果编译器查遍GOPATH也没有找到要导入的包,那么在试图对程序执行run或者build的时候就会出错。本章后面会介绍如何通过go get命令来修正这种错误。
目前的大势所趋是,使用分布式版本控制系统(Distributed Version Control Systems,DVCS)来分享代码,如GitHub、Launchpad还有Bitbucket。Go语言的工具链本身就支持从这些网站及类似网站获取源代码。Go工具链会使用导入路径确定需要获取的代码在网络的什么地方。
例如:
import "github.com/spf13/viper"用导入路径编译程序时,go build命令会使用GOPATH的设置,在磁盘上搜索这个包。事实上,这个导入路径代表一个URL,指向GitHub上的代码库。如果路径包含URL,可以使用Go工具链从DVCS获取包,并把包的源代码保存在GOPATH指向的路径里与URL匹配的目录里。这个获取过程使用go get命令完成。go get将获取任意指定的URL的包,或者一个已经导入的包所依赖的其他包。由于go get的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包。
如果要导入的多个包具有相同的名字,会发生什么?例如,既需要network/convert包来转换从网络读取的数据,又需要file/convert包来转换从文本文件读取的数据时,就会同时导入两个名叫convert的包。这种情况下,重名的包可以通过命名导入来导入。命名导入是指,在import语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。
例如,若用户已经使用了标准库里的fmt包,现在要导入自己项目里名叫fmt的包,就可以通过代码清单3-4所示的命名导入方式,在导入时重新命名自己的包。
代码清单3-4 重命名导入
01 package main
02
03 import (
04 "fmt"
05 myfmt "mylib/fmt"
06 )
07
08 func main() {
09 fmt.Println("Standard Library")
10 myfmt.Println("mylib/fmt")
11 }当你导入了一个不在代码里使用的包时,Go编译器会编译失败,并输出一个错误。Go开发团队认为,这个特性可以防止导入了未被使用的包,避免代码变得臃肿。虽然这个特性会让人觉得很烦,但Go开发团队仍然花了很大的力气说服自己,决定加入这个特性,用来避免其他编程语言里常常遇到的一些问题,如得到一个塞满未使用库的超大可执行文件。很多语言在这种情况会使用警告做提示,而Go开发团队认为,与其让编译器告警,不如直接失败更有意义。每个编译过大型C程序的人都知道,在浩如烟海的编译器警告里找到一条有用的信息是多么困难的一件事。这种情况下编译失败会更加明确。
有时,用户可能需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_来重命名这个导入。我们下节会讲到这个特性的用法。
空白标识符 下划线字符(
_)在Go语言里称为空白标识符,有很多用法。这个标识符用来抛弃不想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的你不感兴趣的值。
init每个包可以包含任意多个init函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的init函数都会安排在main函数之前执行。init函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。
以数据库驱动为例,database下的驱动在启动时执行init函数会将自身注册到sql包里,因为sql包在编译时并不知道这些驱动的存在,等启动之后sql才能调用这些驱动。让我们看看这个过程中init函数做了什么,如代码清单3-5所示。
代码清单3-5 `init`函数的用法
01 package postgres
02
03 import (
04 "database/sql"
05 )
06
07 func init() {
08 sql.Register("postgres", new(PostgresDriver)) ●――――创建一个postgres驱动的实例。这里为了展现init的作用,没有展现其定义细节。
09 }
这段示例代码包含在PostgreSQL数据库的驱动里。如果程序导入了这个包,就会调用init函数,促使PostgreSQL的驱动最终注册到Go的sql包里,成为一个可用的驱动。
在使用这个新的数据库驱动写程序时,我们使用空白标识符来导入包,以便新的驱动会包含到sql包。如前所述,不能导入不使用的包,为此使用空白标识符重命名这个导入可以让init函数发现并被调度运行,让编译器不会因为包未被使用而产生错误。
现在我们可以调用sql.Open方法来使用这个驱动,如代码清单3-6所示。
代码清单3-6 导入时使用空白标识符作为包的别名
01 package main
02
03 import (
04 "database/sql"
05
06 _ "github.com/goinaction/code/chapter3/dbdriver/postgres" ●――――使用空白标识符导入包,避免编译错误。
07 )
08
09 func main()
10 sql.Open("postgres", "mydb") ●――――调用sql包提供的Open方法。该方法能工作的关键在于postgres驱动通过自己的init函数将自身注册到了sql包。
11 }
在前几章里,我们已经使用过了go这个工具,但我们还没有探讨这个工具都能做哪些事情。让我们进一步深入了解这个短小的命令,看看都有哪些强大的能力。在命令行提示符下,不带参数直接键入go这个命令:
$ gogo这个工具提供了很多功能,如图3-1所示。

图3-1 go命令输出的帮助文本
通过输出的列表可以看到,这个命令包含一个编译器,这个编译器可以通过build命令启动。正如预料的那样,build和clean命令会执行编译和清理的工作。现在使用代码清单3-2里的源代码,尝试执行这些命令:
go build hello.go当用户将代码签入源码库里的时候,开发人员可能并不想签入编译生成的文件。可以用clean命令解决这个问题:
go clean hello.go调用clean后会删除编译生成的可执行文件。让我们看看go工具的其他一些特性,以及使用这些命令时可以节省时间的方法。接下来的例子中,我们会使用代码清单3-7中的样例代码。
代码清单3-7 使用io包的工作
01 package main
02
03 import (
04 "fmt"
05 "io/ioutil"
06 "os"
07
08 "github.com/goinaction/code/chapter3/words"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 filename := os.Args[1]
14
15 contents, err := ioutil.ReadFile(filename)
16 if err != nil {
17 fmt.Println(err)
18 return
19 }
20
21 text := string(contents)
22
23 count := words.CountWords(text)
24 fmt.Printf("There are %d words in your text.\n", count)
25 }如果已经下载了本书的源代码,应该可以在$GOPATH/src/github.com/goinaction/code/chapter3/words找到这个包。确保已经有了这段代码再进行后面的内容。
大部分Go工具的命令都会接受一个包名作为参数。回顾一下已经用过的命令,会想起build命令可以简写。在不包含文件名时,go工具会默认使用当前目录来编译。
go build因为构建包是很常用的动作,所以也可以直接指定包:
go build github.com/goinaction/code/chapter3/wordcount也可以在指定包的时候使用通配符。3个点表示匹配所有的字符串。例如,下面的命令会编译chapter3目录下的所有包:
go build github.com/goinaction/code/chapter3/...除了指定包,大部分Go命令使用短路径作为参数。例如,下面两条命令的效果相同:
go build wordcount.go
go build .要执行程序,需要首先编译,然后执行编译创建的wordcount或者wordcount.exe程序。不过这里有一个命令可以在一次调用中完成这两个操作:
go run wordcount.gogo run命令会先构建wordcount.go里包含的程序,然后执行构建后的程序。这样可以节省好多录入工作量。
做开发会经常使用go build和go run命令。让我们看另外几个可用的命令,以及这些命令可以做什么。
我们已经学到如何用go这个通用工具进行编译和执行。但这个好用的工具还有很多其他没有介绍的诀窍。
go vet这个命令不会帮开发人员写代码,但如果开发人员已经写了一些代码,vet命令会帮开发人员检测代码的常见错误。让我们看看vet捕获哪些类型的错误。
Printf类函数调用时,类型匹配错误的参数。让我们看看许多Go开发新手经常犯的一个错误。fmt.Printf函数常用来产生格式化输出,不过这个函数要求开发人员记住所有不同的格式化说明符。代码清单3-8中给出的就是一个例子。
代码清单3-8 使用go vet
01 package main
02
03 import "fmt"
04
05 func main() {
06 fmt.Printf("The quick brown fox jumped over lazy dogs", 3.14)
07 }这个程序要输出一个浮点数3.14,但是在格式化字符串里并没有对应的格式化参数。如果对这段代码执行go vet,会得到如下消息:
go vet main.go
main.go:6: no formatting directive in Printf callgo vet工具不能让开发者避免严重的逻辑错误,或者避免编写充满小错的代码。不过,正像刚才的实例中展示的那样,这个工具可以很好地捕获一部分常见错误。每次对代码先执行go vet再将其签入源代码库是一个很好的习惯。
fmt是Go语言社区很喜欢的一个命令。fmt工具会将开发人员的代码布局成和Go源代码类似的风格,不用再为了大括号是不是要放到行尾,或者用tab(制表符)还是空格来做缩进而争论不休。使用go fmt后面跟文件名或者包名,就可以调用这个代码格式化工具。fmt命令会自动格式化开发人员指定的源代码文件并保存。下面是一个代码执行go fmt前和执行go fmt后几行代码的对比:
if err != nil { return err }在对这段代码执行go fmt后,会得到:
if err != nil {
return err
}很多Go开发人员会配置他们的开发环境,在保存文件或者提交到代码库前执行go fmt。如果读者喜欢这个命令,也可以这样做。
还有另外一个工具能让Go开发过程变简单。Go语言有两种方法为开发者生成文档。如果开发人员使用命令行提示符工作,可以在终端上直接使用go doc命令来打印文档。无需离开终端,即可快速浏览命令或者包的帮助。不过,如果开发人员认为一个浏览器界面会更有效率,可以使用godoc程序来启动一个Web服务器,通过点击的方式来查看Go语言的包的文档。Web服务器godoc能让开发人员以网页的方式浏览自己的系统里的所有Go语言源代码的文档。
对那种总会打开一个终端和一个文本编辑器(或者在终端内打开文本编辑器)的开发人员来说,go doc是很好的选择。假设要用Go语言第一次开发读取UNIX tar文件的应用程序,想要看看archive/tar包的相关文档,就可以输入:
go doc tar执行这个命令会直接在终端产生如下输出:
PACKAGE DOCUMENTATION
package tar // import "archive/tar"
Package tar implements access to tar archives.It aims to cover most of the
variations, including those produced by GNU and BSD tars.
References:
http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5
http://www.gnu.org/software/tar/manual/html_node/Standard.html
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html
var ErrWriteTooLong = errors.New("archive/tar: write too long") ...
var ErrHeader = errors.New("archive/tar: invalid tar header")
func FileInfoHeader(fi os.FileInfo, link string) (*Header, error)
func NewReader(r io.Reader) *Reader
func NewWriter(w io.Writer) *Writer
type Header struct { ...}
type Reader struct { ...}
type Writer struct { ...}
开发人员无需离开终端即可直接翻看文档,找到自己需要的部分。
Go语言的文档也提供了浏览器版本。有时候,通过跳转到文档,查阅相关的细节,能更容易理解整个包或者某个函数。在这种情况下,会想使用godoc作为Web服务器。如果想通过Web浏览器查看可以点击跳转的文档,下面就是得到这种文档的好方式。
开发人员启动自己的文档服务器,只需要在终端会话中输入如下命令:
godoc -http=:6060这个命令通知godoc在端口6060启动Web服务器。如果浏览器已经打开,导航到http://localhost:6060可以看到一个页面,包含所有Go标准库和你的GOPATH下的Go源代码的文档。
如果图3-2显示的文档对开发人员来说很熟悉,并不奇怪,因为Go官网就是通过一个略微修改过的godoc来提供文档服务的。要进入某个特定包的文档,只需要点击页面顶端的Packages。

图3-2 本地Go文档
Go文档工具最棒的地方在于,它也支持开发人员自己写的代码。如果开发人员遵从一个简单的规则来写代码,这些代码就会自动包含在godoc生成的文档里。
为了在godoc生成的文档里包含自己的代码文档,开发人员需要用下面的规则来写代码和注释。我们不会在本章介绍所有的规则,只会提一些重要的规则。
用户需要在标识符之前,把自己想要的文档作为注释加入到代码中。这个规则对包、函数、类型和全局变量都适用。注释可以以双斜线开头,也可以用斜线和星号风格。
// Retrieve连接到配置库,收集各种链接设置、用户名和密码。这个函数在成功时
// 返回config结构,否则返回一个错误。
func Retrieve() (config, error) {
// ...省略
}在这个例子里,我们展示了在Go语言里为一个函数写文档的惯用方法。函数的文档直接写在函数声明之前,使用人类可读的句子编写。如果想给包写一段文字量比较大的文档,可以在工程里包含一个叫作doc.go的文件,使用同样的包名,并把包的介绍使用注释加在包名声明之前。
/*
包usb提供了用于调用USB设备的类型和函数。想要与USB设备创建一个新链接,使用NewConnection
...
*/
package usb这段关于包的文档会显示在所有类型和函数文档之前。这个例子也展示了如何使用斜线和星号做注释。可以在Google上搜索golang documentation来查找更多关于如何给代码创建一个好文档的内容。
现代开发者不会一个人单打独斗,而Go工具也认可这个趋势,并为合作提供了支持。多亏了go工具链,包的概念没有被限制在本地开发环境中,而是做了扩展,从而支持现代合作方式。让我们看看在分布式开发环境里,想要良好合作,需要遵守的一些惯例。
开发人员一旦写了些非常棒的Go代码,就会很想把这些代码与Go社区的其他人分享。这其实很容易,只需要执行下面的步骤就可以。
使用go get的时候,开发人员指定了要导入包的全路径。这意味着在创建想要分享的代码库的时候,包名应该就是代码库的名字,而且包的源代码应该位于代码库目录结构的根目录。
Go语言新手常犯的一个错误是,在公用代码库里创建一个名为code或者src的目录。如果这么做,会让导入公用库的语句变得很长。为了避免过长的语句,只需要把包的源文件放在公用代码库的根目录就好。
与其他语言相比,Go语言的包一般相对较小。不要在意包只支持几个API,或者只完成一项任务。在Go语言里,这样的包很常见,而且很受欢迎。
go fmt和其他开源代码库一样,人们在试用代码前会通过源代码来判断代码的质量。开发人员需要在签入代码前执行go fmt,这样能让自己的代码可读性更好,而且不会由于一些字符的干扰(如制表符),在不同人的计算机上代码显示的效果不一样。
Go开发者用godoc来阅读文档,并且会用http://godoc.org这个网站来阅读开源包的文档。如果按照go doc的最佳实践来给代码写文档,包的文档在本地和线上都会很好看,更容易被别人发现。
从Go1.0发布那天起,社区做了很多努力,提供各种Go工具,以便开发人员的工作更轻松。有很多工具专注在如何管理包的依赖关系。现在最流行的依赖管理工具是Keith Rarik写的godep、Daniel Theophanes写的vender和Gustavo Niemeyer开发的gopkg.in工具。gopkg.in能帮助开发人员发布自己的包的多个版本。
作为对社区的回应,Go语言在1.5版本开始试验性提供一组新的构建选项和功能,来为依赖管理提供更好的工具支持。尽管我们还需要等一段时间才能确认这些新特性是否能达成目的,但毕竟现在已经有一些工具以可重复使用的方式提供了管理、构建和测试Go代码的能力。
像godep和vender这种社区工具已经使用第三方(verdoring)导入路径重写这种特性解决了依赖问题。其思想是把所有的依赖包复制到工程代码库中的目录里,然后使用工程内部的依赖包所在目录来重写所有的导入路径。
代码清单3-9展示的是使用godep来管理工程里第三方依赖时的一个典型的源代码树。
代码清单3-9 使用godep的工程
$GOPATH/src/github.com/ardanstudios/myproject
|--Godeps
| |--Godeps.json
| |-- Readme
| |-- _workspace
| |-- src
| |-- bitbucket.org
| |-- ww
| | |-- goautoneg
| | |-- Makefile
| | |-- README.txt
| | |-- autoneg.go
| | |-- autoneg_test.go
| |-- github.com
| |-- beorn7
| |-- perks
| |-- README.md
| |-- quantile
| |-- bench_test.go
| |-- example_test.go
| |-- exampledata.txt
| |-- stream.go
|
|-- examples
|-- model
|-- README.md
|-- main.go可以看到godep创建了一个叫作Godeps的目录。由这个工具管理的依赖的源代码被放在一个叫作_workspace/src的目录里。
接下来,如果看一下在main.go里声明这些依赖的import语句(如代码清单3-9和代码清单3-10所示),就能发现需要改动的地方。
代码清单3-10 在路径重写之前
01 package main
02
03 import (
04 "bitbucket.org/ww/goautoneg"
05 "github.com/beorn7/perks"
06 )代码清单3-11 在路径重写之后
01 package main
02
03 import (
04 "github.ardanstudios.com/myproject/Godeps/_workspace/src/
bitbucket.org/ww/goautoneg"
05 "github.ardanstudios.com/myproject/Godeps/_workspace/src/
github.com/beorn7/perks"
06 )在路径重写之前,import语句使用的是包的正常路径。包对应的代码存放在GOPATH所指定的磁盘目录里。在依赖管理之后,导入路径需要重写成工程内部依赖包的路径。可以看到这些导入路径非常长,不易于使用。
引入依赖管理将所有构建时依赖的源代码都导入到一个单独的工程代码库里,可以更容易地重新构建工程。使用导入路径重写管理依赖包的另外一个好处是这个工程依旧支持通过go get获取代码库。当获取这个工程的代码库时,go get可以找到每个包,并将其保存到工程里正确的目录中。
gb是一个由Go社区成员开发的全新的构建工具。gb意识到,不一定要包装Go本身的工具,也可以使用其他方法来解决可重复构建的问题。
gb背后的原理源自理解到Go语言的import语句并没有提供可重复构建的能力。import语句可以驱动go get,但是import本身并没有包含足够的信息来决定到底要获取包的哪个修改的版本。go get无法定位待获取代码的问题,导致Go工具在解决重复构建时,不得不使用复杂且难看的方法。我们已经看到过使用godep时超长的导入路径是多么难看。
gb的创建源于上述理解。gb既不包装Go工具链,也不使用GOPATH。gb基于工程将Go工具链工作空间的元信息做替换。这种依赖管理的方法不需要重写工程内代码的导入路径。而且导入路径依旧通过go get和GOPATH工作空间来管理。
让我们看看上一节的工程如何转换为gb工程,如代码清单3-12所示。
代码清单3-12 gb工程的例子
/home/bill/devel/myproject ($PROJECT)
|-- src
| |-- cmd
| | |-- myproject
| | | |-- main.go
| |-- examples
| |-- model
| |-- README.md
|-- vendor
|-- src
|-- bitbucket.org
| |-- ww
| |-- goautoneg
| |-- Makefile
| |-- README.txt
| |-- autoneg.go
| |-- autoneg_test.go
|-- github.com
|-- beorn7
|-- perks
|-- README.md
|-- quantile
|-- bench_test.go
|-- example_test.go
|-- exampledata.txt
|-- stream.go一个gb工程就是磁盘上一个包含src/子目录的目录。符号$PROJECT导入了工程的根目录中,其下有一个src/的子目录中。这个符号只是一个简写,用来描述工程在磁盘上的位置。$PROJECT不是必须设置的环境变量。事实上,gb根本不需要设置任何环境变量。
gb工程会区分开发人员写的代码和开发人员需要依赖的代码。开发人员的代码所依赖的代码被称作第三方代码(vendored code)。gb工程会明确区分开发人员的代码和第三方代码,如代码清单3-13和代码清单3-14所示。
代码清单3-13 工程中存放开发人员写的代码的位置
$PROJECT/src/代码清单3-14 存放第三方代码的位置
$PROJECT/vendor/src/gb 一个最好的特点是,不需要重写导入路径。可以看看这个工程里的main.go文件的import语句——没有任何需要为导入第三方库而做的修改,如代码清单3-15所示。
代码清单3-15 gb工程的导入路径
01 package main
02
03 import (
04 "bitbucket.org/ww/goautoneg"
05 "github.com/beorn7/perks"
06 )gb工具首先会在$PROJECT/src/目录中查找代码,如果找不到,会在$PROJECT/vender/src/目录里查找。与工程相关的整个源代码都会在同一个代码库里。自己写的代码在工程目录的src/目录中,第三方依赖代码在工程目录的vender/src子目录中。这样,不需要配合重写导入路径也可以完成整个构建过程,同时可以把整个工程放到磁盘的任意位置。这些特点,让gb成为社区里解决可重复构建的流行工具。
还需要提一点:gb工程与Go官方工具链(包括go get)并不兼容。因为gb不需要设置GOPATH,而Go工具链无法理解gb工程的目录结构,所以无法用Go工具链构建、测试或者获取代码。构建(如代码清单3-16所示)和测试gb工程需要先进入$PROJECT目录,并使用gb工具。
代码清单3-16 构建gb工程
gb build all很多Go工具支持的特性,gb都提供对应的特性。gb还提供了插件系统,可以让社区扩展支持的功能。其中一个插件叫作vender。这个插件可以方便地管理$PROJECT/vender/src/目录里的依赖关系,而这个功能Go工具链至今没有提供。想了解更多gb的特性,可以访问这个网站:getgb.io。
GOPATH决定了Go源代码在磁盘上被保存、编译和安装的位置。GOPATH,以保持源代码和依赖的隔离。go工具是在命令行上工作的最好工具。go get来获取别人的包并将其安装到自己的GOPATH指定的目录。