# 2.1 Go1的包机制


# 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`文件：

```go
package hello

import "fmt"

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

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

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