2.1 Go1的包机制

同⽬录Go源⽂件的集合构成包,而同⽬录下⼦⽬录对应的包的集合构成模块(暂不考虑子模块)。因此,要了解模块之前需要先了解包。

2.1.1 包是最小链接单位

Go语言是一种编译型的语言,链接的最小单位是包,对应go/ast.Package类型,多个包链接为一个可执行程序。在当前的官方实现中,一个包一般对应一个路径目录下的全部的Go语言源文件,而目录的路径包含了表示包的唯一路径名。Go语言的规范中包只是一个抽象的概念,并不要求包一定是以目录的方式展现,在未来包或者是模块也可能以压缩文件的方式展现。

包是Go语言应用编译和链接的基本单位,因此模块最终的目的是为了管理这些包的版本。

2.1.2 包目录布局的演变

在Go语言刚刚开源、还没有GOPATH环境变量之前,Go语言标准库的包全部是放在$(GOROOT)/src目录之下的,比如标准库中的image/png包对应$(GOROOT)/src/image/png目录。而第三方的包也可以放在$(GOROOT)/src目录,这时候Go语言的构建工具是不区分标准库的包和第三方包的。此外第三方或自己的应用对应的包,可以放在$(GOROOT)目录之外的任意目录。

比如可以在任意目录创建一个hello.go文件:

package hello

import "fmt"

func PrintHello() {
	fmt.Printf("Hello, 世界\n")
}

然后在同级的目录创建一个Makefile文件用于管理构建工作:

include $(GOROOT)/src/Make.inc

TARG=github.com/chai2010/go2-book/ch2/hello
GOFILES=./hello.go

include $(GOROOT)/src/Make.pkg

第一个语句包含构建环境,最后的语句表示这是一个包。最重要的TARG变量定义了包的路径,而GOFILES则表示了包由哪些Go文件组成。然后执行make nuke就可以编译生成$(GOROOT)/pkg/github.com/chai2010/go2-book/ch2/hello.a文件。当另一个包要导入hello包时,实际上是从$(GOROOT)/pkg/github.com/chai2010/go2-book/ch2/hello.a文件读取包的信息。

在Go1之前的史前时代,一切包都是手工构建安装的,因此包的版本管理也是手工的方式进行。如果需要使用社区开源的第三方包,需要手工下载代码放到合适的目录正确编译并安装之后才可以使用。

为了将标准库的包和第三方的包分开管理,避免第三方包代码污染$(GOROOT)/src$(GOROOT)/pkg目录,Go语言引入了GOPATH环境变量。GOPATH环境变量对应的目录用于管理非标准库的包,其中src用户存放Go代码,pkg用于存放编译后的.a文件,bin用于存放编译后的可执行程序。此后直到Go1.10,Go语言所有和包版本管理相关的工作都是基于GOPATH特性展开。

2.1.3 GOPATH特性的扩展

扩展GOPATH的目标都是为了更方便管理包。Go语言的包有三种类型:首先是叶子包,此类包最多依赖标准库,不依赖第三方包;其次是main包表示一个应用,它不能被其它包导入(单元测试除外);最后是普通的依赖第三方包的非main包。比较特殊的main包同时也是一个叶子包。

叶子包自身很少会遇到版本管理问题,因为不会遇到因为依赖第三方包产生的各种问题,因此叶子包的开发者很少关注版本管理的问题。稍微复杂一点的是main包,main包是包依赖中的根包。main包不担心被其它包依赖,因此它其实是可以通过一个独占的GOPATH来维护所有依赖的第三方包的。最复杂的是既不是main包,也不是普通的叶子包,因为普通包需要管理其依赖的第三方包,同时一般又不能单独管理GOAPTH。在vendor出现之前的版本管理实践中,普通包的版本比较简陋,很多普通包甚至都没有版本管理,只有master一个最新版本。

GOPATH的扩展主要分为横向和纵向两个方向。横向就是同时并列维护多个GOPATH目录,通过手工方式调整其中某些目录来实现目录中包版本切换的目的。纵向扩展一般在管理main包对应的应用程序中使用,通过在包内部创建临时的GOPATH子目录,在GOPATH子目录中包含全部第三方依赖的拷贝来实现外部依赖包版本的管理。

社区中早期出现的Godeps工具就是通过在当前目录下创建Godeps/_workspace子目录来管理维护依赖的第三方包的版本。Godeps的实践成果最终被吸收到来Go语言中,通过vendor机制实现来main包的依赖管理(不是版本管理)。但是最终vendor机制也带来了各种问题(稍后的章节会讨论),最终官方完全重新设计了模块化的特性。

2.1.4 模块化之后的包目录路径

通过vendor机制来实现版本管理的尝试虽然失败了,但是通过横向扩展GOPATH的来维护同一个包的不同版本的思路却在模块中复活了。模块化通过重新组织目录结构,实现了同时管理同一个包的不同版本需求。

比如之前$(GOPATH)/src/github.com/chai2010/pbgo包的1.0.0版本,在模块化之后将对应$(HOME)/go/pkg/mod/github.com/chai2010/pbgo@1.0.0。模块化通过$(HOME)/go/pkg/mod目录管理第三方的依赖包,同时通过pkg@x.y.z的版本后缀来区分同一个包的不同版本。这其实和多个GOPATH并列存放的思路是类似,不过模块化对多版本支持的更加完美。