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并列存放的思路是类似,不过模块化对多版本支持的更加完美。