Golang Package 与 Module 简介

软件是由代码组成的。为了复用代码,代码的组织出现了不同层次的抽象和实现,如 Module(模块),包(Package),Lib(库),Framwork(框架)等。

通常一个Project(项目),会根据功能拆分很多 module,常用的软件会打包成一个个共享库。在开源社区分享软件包是一件十分 cool 的事儿。这些软件包也有可能引用了其他的开源包,因此开源项目上经常会有软件相互依赖,或依赖某个包,或依赖某个包的某个版本。

现代的语言都有很多好用的包管理工具,如 pip 之于 python,gem 之于 ruby,npm 之于 nodejs。然而 Golang 早期版本却没有官方的包管理器,直到 go1.5 才添加了实验性的 vendor。尽管官方的不作为(保守),还是无法阻止社区的繁荣。社区诞生了许多工具,比较有代表如 govenderglidegopm,以及半官方的 dep 等。

百花齐放的另外一层含义也是工具链混乱的表现。

终于在 go1.12,发布了官方的包管理工具 Go Module。go module 不仅带来了统一的标准,而且对于旧软件包的处理,以及所坚持的包管理哲学都挺有意思。下面将会从 go module 之前的包管理方式和升级到 go module 方式做一个简单的说明。

下面的例子以常见的使用方式做介绍,并没有深入其背后的原理与实现。例如go.sum 以及一些最下化处理原则不会涉及。部分内容闲扯一下 go 的流派观点。

GOPATH

在 go1.12 之前,安装 golang 之后,需要配置两个环境变量----GOROOTGOPATH。前者是 go 安装后的所在的路径,后者是开发中自己配置的,用于存放go 源代码的地方。在 GOPATH 路径内,有三个文件夹,分别是

  • bin: go 编译后的可执行文件所在的文件夹
  • pkg: 编译非 main 包的中间连接文件
  • src: go 项目源代码

开发的程序源码则放在src里,可以在src里创建多个项目。每一个项目同时也是一个文件夹。

go1.12 之后,淡化了 GOPATH,因此也可以忽略这部分内容。

Package

main package

golang 的所有文件都需要指定其所在的包(package),包有两种类型,一种是 main 包,使用 package main 在代码的最前面声明。另外一种就是 非main 包,使用 package + 包名 。main 包的可以有唯一的一个 main 函数,这个函数也是程序的入口。也只有 main 包可以编译成可执行的文件。

下面看一个简单的例子:

➜  golang echo $GOPATH
/Users/master/golang
➜  golang pwd
/Users/master/golang
➜  golang tree
.
├── bin
├── pkg
└── src
    └── demo
        └── main.go

4 directories, 1 file

➜  demo cat main.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello world")
}

➜  demo go run main.go
hello world

从上面的目录结构可以看出,GOPATH为 /Users/master/golang 。在 src 内创建了一个项目 demo。demo 里有一个 main.go 文件。main.go 的第一行声明了这是一个 main 包,因此可以定义一个 main 函数。使用 go run 可以编译并运行 main.go。

虽然 main 包的文件名也是 main.go,其实包名和文件名没有直接关系。

自定义 package

go 使用 package 来管理源文件。package 必须在一个文件夹内,且一个文件夹内也只能有一个package,但是一个文件夹可以有多个文件。下面自定义一个 package。

➜  demo tree
.
├── main.go
└── service
    └── http.go

1 directory, 2 files
➜  demo cat service/http.go
package api

import "fmt"

func HandleReq(){
    fmt.Println("api - http.go Handle Request")
}
➜  demo cat main.go
package main

import (
    "fmt"
    "./service"
)

func main() {
    fmt.Println("hello world")
    api.HandleReq()
}

在 demo 文件内创建一个文件夹 service,在 service 内又创建了 http.go 文件。后者声明为 package api ,即包名为 api。然后在 main.go 文件中,通过 import "./service" 引入了api 包所在的文件夹。最后通过 包名.包函数 调用。

输出结果如下:

➜  demo go run main.go
hello world
api - http.go Handle Request

从上面的例子可以看到,包名为 api,文件夹为 service,文件为 http.go。它们三者的命名都是不一样的。这与很多其他语言相悖。正如前文所言,一个包可以分散在不同的同级文件里,只要都声明为一个包名即可。例如下面再增加一个文件:

➜  demo cat service/rpc.go
package api

import "fmt"

func HandleResp(){
    fmt.Println("api - rpc.go Handle Request")
}

在 main 函数里同样使用 包名.包函数 ---- api.HandleResp() 调用。其实很好理解,即同样的一个包,文件内容太多,拆分成多个文件而已。文件名跟包名没有直接关系。如果只有一个文件,通常可以写成包名。但是导入的时候,必须导入包所在的文件夹的路径。其实可以这样理解,import 的是 path(路径),那么go就去那个路径下搜索,搜索当然是查找包名。只不过通常习惯是 文件名包名一致。

这包名和文件夹名不一致,是反模式。主要是为了直观的表示包名和文件夹名没有直接关系。

内嵌 package

在 service 内再创建一个文件夹 api,此时再命名一个 api.go 文件。即 文件夹名 和 包名 一致。其内容如下:

➜  demo cat service/api/api.go
package api

import "fmt"

func HandleError(){
    fmt.Println("api api.go Handle Error")
}

import package 就是导入包所在的文件夹,因此 main.go 如下:

import (
    "fmt"
    "./service"
    "./service/api"
)

但是这样编译会报错:

➜  demo go run main.go
# command-line-arguments
./main.go:6:9: api redeclared as imported package name
    previous declaration at ./main.go:5:2
./main.go:11:2: undefined: "_/Users/master/golang/src/demo/service/api".HandleReq
./main.go:12:9: undefined: "_/Users/master/golang/src/demo/service/api".HandleResp

显然,在 service 文件内的 http.go 和 rpc.go 声明了 package api。在 service/api 文件夹里的 api.go 也声明为 api。这是同名的两个包。

但是在 main 函数里,都是使用包名引用函数。由于名字冲突,无法确定到底使用那个 api 包,进而报错。解决方法也很简单,给包名增加一个别名即可。

import (
    "fmt"
    "./service"
    apiNew "./service/api"
)

func main() {
    fmt.Println("hello world")
    api.HandleReq()
    api.HandleResp()
    apiNew.HandleError()
}

这样 api 就是 service 空间下的,apiNew 就是 service/api 空间下,两者相互隔离。

导入规则

前面 import 语句内,通过相对路径导入了包。对于*nix系统,相对路径 . 通常是可以省略的。下面就省略试试:

import (
        "fmt"
        "service"
        apiNew "service/api"
)

此时会发现报错:

➜  demo go run main.go
main.go:5:2: cannot find package "service" in any of:
    /usr/local/go/src/service (from $GOROOT)
    /Users/master/golang/src/service (from $GOPATH)
main.go:6:9: cannot find package "service/api" in any of:
    /usr/local/go/src/service/api (from $GOROOT)
    /Users/master/golang/src/service/api (from $GOPATH)

报错信息也简单明了。即 go 先从 $GOROOT/src 搜索 service 包,找不到然后从 $GOPATH/src 里搜索。也找不到,进而报错。

这里就涉及到 go 的包搜索方式。通常 standard pkg(标准库)都在$GOROOT/src 里,因此会从这里搜索。而用户自定义的包,或者三方包,go是统一从 $GOPATH/src 里搜索的。当不使用 . 显式表明相对导入,那么 go 就会相对于 $GOPATH/src 导入。解决上面的方法就是把 service 包放到 demo 外面

➜  src tree
.
├── demo
│   ├── main.go
│   └── service
│       ├── api
│       │   └── api.go
│       ├── http.go
│       └── rpc.go
└── service
    ├── api
    │   └── api.go
    ├── http.go
    └── rpc.go

5 directories, 7 files

通常,如果 service 是 demo 的一个内部模块,那么就放到 demo 内,使用 . 方式相对导入。如果它是一个可以共享的 package,就可以放到 GOPATH 下。为什么 go会使用 GOPATH 这样的集中文件夹呢?

此外,想要把这个 service 包发布出去,可以使用 github 管理源代码和包版本。

首先需要从 github 创建一个 repo(仓库)。常见的形式是 github.com/username/repo。因此软件包的名也是这个规则。

例子

初始化 service

在 github上创建一个 repo 叫 service, 并打上了 v1.0.0 的版本 tag。那么在mian.go 里引用就是这样:

import (
    "fmt"
    "github.com/rsj217/service"
)

上述的 import 语句,无非就是增加了包 servic e的文件夹层级 github.com/rsj217 而已。这样做的主要目的是为了让 go get 工具从网络上的 url 地址拉取包。go get 会自动从 github.com/rsj217/service 拉取源码,并 copy 到 $GOPATH/src 下。运行go get 或者 go run main.go ,go 都会从 import 语句中解析并下载软件包。

➜  demo go get github.com/rsj217/service
➜  demo tree ../
../
├── demo
│   └── main.go
└── github.com
    └── rsj217
        └── service
            ├── LICENSE
            ├── README.md
            ├── api
            │   └── api.go
            ├── http.go
            └── rpc.go

5 directories, 6 files

从前面包的搜索方式就很容易理解。import "github.com/rsj217/service" go 就从 $GOPATH/src 下搜索,正好能搜索到 github.com/rsj217/service

进入到 service文件夹内,可以看到就是完整的 git repo 的clone。修改提交后,即使删掉这个包。再使用 go get 也能再次下载。

Why GOPATH

前面我们抛了一个问题: 为什么 go 会使用 GOPATH 这样的集中文件夹呢?go 不像 python 有一个中央仓库 PyPI 用来管理存储软件包,以提供给包管理工具 pip 下载。而 python 也不像 go 那样有个本地的 GOPATH 用来搜索软件包。

对于 python 开发者而言,源码怎么管理都无所谓。但是只要想分发软件包,就可以打包然后传到 pypi 即可。想要使用软件包,从 pypi 搜索即可(或从github.com)。

但是go是另外一种开发方式。从一些朋友了解到Google的开发方式。他们本身就有一个中央集中式中心用来管理软件包。使用,构建都从那里获取。因为外人无法访问google的中央,但是这种引用中央的开发模式被保留下来,因此就抽象成本地的GOPATH。

至于软件的分发,就从源代码拉取即可。有名的源代码版本管理无非就是 github 之类。google的做法就是让用户自己维护一个在本地类似 pypi 的中央库。

自己怎么维护呢?就是通过 go get 从 github 或者其他类似的网站上获取,然后打包,处理依赖。这样的一个好处就是自己把分散式的 github 开源包进行了本地的集中式处理。处理依赖的时候会比较方便,毕竟本地什么东西都有了。

但是从上面的 case 可以看出,go get 就类似 git clone。即使每次 go get 拉取仓库都是最新的版本。如何处理好版本依赖呢?

在go module之前也有不少解决方案,但是都或多或少有优劣。下面就介绍一下 go module 的使用方式。

go module

go1.12 引入了 go Module,即 go 的软件包都可以声明为 module。同时淡化了GOPATH,之所以说淡化,是因为即使使用1.12版本,用户不用配置GOPATH环境变量,但是它还是以另外一种形式存在。

go1.12 会在用户的 $home 目录下创建一个 go 文件夹作为默认的 GOPATH。

➜  ~ echo $GOPATH

➜  ~ go env | grep GOPATH
GOPATH="/Users/master/go"

虽然引入了 module,但是并没有废弃 go get,go get 还是基于 GOPATH 运作的,并且 go 的开发方式也没有放弃。只是减少了用户的心智负担,但也提升了用户体验。go get 会将远程的软件包 download 在 $home/go/pkg/mod 目录里。

没有了显示的 GOPATH,开发者的项目就不用像之前一样放到 GOPATH/src 下面了。用户可以在任何一个文件创建自己的项目。下面是一个例子:

随意创建一个 demo 文件夹作为项目。使用 mod 命令 声明一个 module。然后编写 main.go

➜  demo go mod init demo
go: creating new go.mod: module demo
➜  demo ls
go.mod
➜  demo echo $GOPATH

➜  demo cat go.mod
module demo

go 1.12

➜  demo tree
.
├── go.mod
└── main.go

0 directories, 2 files
➜  demo less main.go

package main

import (
        "fmt"
        // "github.com/rsj217/service"
)

func main() {
        fmt.Println("hello world")
        // api.HandleReq()
        // api.HandleResp()
}

go mod init 会在当前文件夹下创建一个 go.mod 文件,里面声明了当前 module 的名字。以后也可以把相关依赖写在这个文件里,就像 python 的 requirements.txt 文件一样。

编译运行 main.go,会发现项目即使没有 $GOPATH/src 下,也能正常运行。下面把上面的main.go 的注释去掉。再次编译运行

➜  demo go run main.go
go: finding github.com/rsj217/service v1.0.0
go: downloading github.com/rsj217/service v1.0.0
go: extracting github.com/rsj217/service v1.0.0
hello world
api - http.go Handle Request
api -- rpc.go Handle Response
➜  demo cat go.mod
module demo

go 1.12

require github.com/rsj217/service v1.0.0 // indirect

➜  demo go list -m all
demo
github.com/rsj217/service v1.0.0

go 会检测 main.go 里的 import 语句。并尝试根据 go.mod 的依赖引用关系导入三方包。如果发现本地cache没有,就会从远程拉取。就像是 go get。当 go module下载了远程包后,同时会自动更新 go.mod 。

如上面就追加了 require github.com/rsj217/service v1.0.0 // indirect 这样一句。require 指令后跟软件包名版本号

对于 demo,其包名为 module demo 里声明的 demo,对于 service,当前不是一个 module,因此其包名是 github.com/rsj217/service。版本号就是 repo 的 tag。

软件包被缓存在 $home/go/pkg/mod 目录里, mod 目录就像之前的 src 一样。里面也有 github.com 目录。最终可以看到在 github.com/rsj217/ 文件下有 service@v1.0.0文件夹,里面正好就是之前 service 的代码

➜  service@v1.0.0 pwd
/Users/master/go/pkg/mod/github.com/rsj217/service@v1.0.0
➜  service@v1.0.0 tree
.
├── LICENSE
├── README.md
├── api
│   └── api.go
├── http.go
└── rpc.go

1 directory, 5 files

service@v1.0.0 与之前的 go get clone 的service 项目不一样。前者只下载了版本号为 1.0.0 的仓库(如果没有打tag,就是指定的某个commit),而后者是整个 repo 的clone。由此可见,go module 是可以精确的拉取目标版本。

语义化版本

go module 同时引入了 semver (Semantic Versioning)。它定义的版本号格式是:

vMAJOR.MINOR.PATCH

  • v:表示版本
  • MAJOR:主版本,通常是大版本升级,导致向前不兼容
  • MINOR:次版本,通常是向下兼容的 feture
  • PATCH:修订版本,如一些bugfix。

下面针对刚才的 service@v1.0.0 升级到1.1.0 版本。通过 git 修改 http.go,然后push一个v1.1.0 的 tag。

v1.0.0

然后使用 go get -u 升级版本

➜  demo go get -u
go: finding github.com/rsj217/service v1.1.0
go: downloading github.com/rsj217/service v1.1.0
go: extracting github.com/rsj217/service v1.1.0

要升级或降级到更具体的版本,go get 允许通过在 包参数中添加 @version 后缀搜索。如 go get service@v1.0.0,go get service@dsa1ewr,或者 go get service@<v1.3.2。
使用 go get -u 会把所依赖的次级包也跟着升级,原因参考golang modules问题的理解与踩坑记
),不是特别友好。比较好的方式是指定版本的具体包或者修改 go.mod 文件。这里没有次级依赖,为了方便,下面的更新也使用了 go get -u

可以看到,mod 里多了一个 service@v1.2.0 的文件夹包,go.mod 也引用了 v1.2.0的软件。运行main.go ,也输出了 1.1.0 升级后的内容。

大版本升级

升级 MINOR 和 PATCH 都比较简单,升级 MAJOR 稍微复杂一点点。再次修改service 仓库,然后打一个 V2.1.0 的tag。推送到远程分支。再使用go get -u 升级

➜  demo go get -u
go: downloading github.com/rsj217/service v2.1.0+incompatible
go: extracting github.com/rsj217/service v2.1.0+incompatible
➜  demo go list -m all
demo
github.com/rsj217/service v2.1.0+incompatible
➜  demo cat go.mod
module demo

go 1.12

require github.com/rsj217/service v2.1.0+incompatible

➜  demo go run main.go
hello world
api - http.go Handle Request
v2.1.0
api -- rpc.go Handle Response

可以看到,在下载软件包的时候,版本号跟着有一个 incompatible 标记 。incompatible 表示是大版本升级,与之前的版本可能不兼容。但是运行main.go的时候,依然可以正确的找到 2.1.0的版本。并且 $home/go 里的软件包也多了一个 service@v2.1.0+incompatible

旧包升级到 go module

本地的项目 demo 已经使用了 go module 声明,对于其依赖 service,也可以改造成 go module,只需要增加一个 go.mod 文件即可。然后打上一个 v2.2.0的 tag。当然 修改 http.go ,以便测试是 v2.2.0 的内容。

然后更新下载依赖

➜  demo go get github.com/rsj217/service@v2.2.0
go: finding github.com/rsj217/service v2.2.0
go: downloading github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e
go: extracting github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e

从上面的输出可以看到,因为已经有了v2的版本,就应该在声明 module 的时候加上v2。如果不加,那么go貌似认为 tag 没有打上。不管怎么样,代码库还是下载了。

查看本地的包会发现有之前的好几个版本,其中 v2.2.0 的被命名为 service@v0.0.0-20190508051156-b9ee113ae63e 。因为刚初始化的 module 认为没有打上 tag,对于没有打 tag 的,go.mod 的格式是 pseudo-version。它的含义是v0.0.0-yyyymmddhhmmss-abcdefabcdef

查看所下载的软包
➜  rsj217 ls
service@v0.0.0-20190508051156-b9ee113ae63e service@v1.1.0
service@v1.0.0                             service@v2.1.0+incompatible

➜  demo cat go.mod
module demo

go 1.12

require github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e

➜  demo go run main.go
hello world
api - http.go Handle Request
v2.2.0
api -- rpc.go Handle Response

从go.mod 和运行效果来看,确实升级到 v2.2.0版本的代码库了。只是go module对于大于 v1 的版本库,其声明的 module 也要加上vMAJOR

go module 多版本共存

上述的 v2.2.0 没有被 go module 识别,是因为 go.mod 的 module github.com/rsj217/service 没有带上 MAJOR 版本号。将 go.mod 改成 module github.com/rsj217/service/v3,即新的 go.mod,然后再打上一个v3.0.0 的版本再更新。

由于(假设) v3.0.0 与 之前的版本都不兼容,但是demo项目又同时需要两个版本的包,此时就是 go module 比之前 go get 方式优越了。

正如 v3.0.0 的 go.mod 加上了版本号,因此对于使用者而言,其实可以理解为是不同命名空间包。就像之前 service 下的 api 和 service/api 下的 api 一样。需要用别名区别。

修改 go.mod 增加两个版本的包。v2.2.0 和 v3.0.0

➜  demo cat go.mod
module demo

go 1.12

require (
    github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e  
    github.com/rsj217/service/v3 v3.0.0
)

➜  demo cat main.go
package main

import (
    "fmt"
    "github.com/rsj217/service"                 // 使用 v2.2.0 的包
    apiv3 "github.com/rsj217/service/v3"  // 使用 v3.0.0 的包
)

func main() {
    fmt.Println("hello world")
    api.HandleReq()
    api.HandleResp()
    apiv3.HandleReq()
}


➜  demo go run main.go
go: downloading github.com/rsj217/service/v3 v3.0.0
go: extracting github.com/rsj217/service/v3 v3.0.0
hello world
api - http.go Handle Request
v2.2.0
api -- rpc.go Handle Response
api - http.go Handle Request
v3.0.0

可以看到 main.go 里的 import 语句,显示的区分了 v2 版本和 v3 版本。查看 home/go 下面的目录,会发现多了一个 service 文件夹,里面放了一个 v3 版本的包。

不管怎样,go module 通过 vMAJOR 让用户可以同时使用多个版本的软件包。但是需要修改使用者的代码,还是略麻烦。不过既然进行了向前不兼容,又得保证旧代码正常work,想来也没有更好的办法。

本地包

开源社区的最大好处就是共享智慧。可是实际开发中,开源的包也未必都十分可靠。通常可以提 PR 提 ISSUE 参与改进。但是原作者要是不维护就得需要自己 fork 修改。

还有一些涉及到商业方向的代码包,总不能扔到 github 上开源。因此本地构建也是一个需求。例如上面的 v3.0.0 有类似的 bug。可以在本地修改一个版本,然后通过 go module 的 replace 指令替换到 github 上的包。

➜  demo go mod vendor
➜  demo ls
go.mod  go.sum  main.go vendor
➜  demo cd vendor
➜  vendor tree
.
├── github.com
│   └── rsj217
│       └── service
│           ├── LICENSE
│           ├── README.md
│           ├── go.mod
│           ├── http.go
│           ├── rpc.go
│           └── v3
│               ├── LICENSE
│               ├── README.md
│               ├── go.mod
│               ├── http.go
│               └── rpc.go
└── modules.txt

4 directories, 11 files

使用 go vendor,可以将依赖包copy到项目的 vendor文件下。如上面的例子 vendor下的结构和之前的 GOPATH/src home/go 类似。修改 v3 下的 http.go

➜  v3 cat http.go
package api

import "fmt"

func HandleReq(){

    fmt.Println("api - http.go Handle Request")
    fmt.Println(" vendor v3.0.0")
}

然后修改 go.mod ,添加 replace指令

➜  demo cat go.mod
module demo

go 1.12

require (
    github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e
    github.com/rsj217/service/v3 v3.0.0
)

replace github.com/rsj217/service/v3 v3.0.0 => ./vendor/github.com/rsj217/service/v3

➜  demo go run main.go
hello world
api - http.go Handle Request
v2.2.0
api -- rpc.go Handle Response
api - http.go Handle Request
 vendor v3.0.0
➜  demo

从运行结果可以看到,go module找到了本地的 vendor里的版本。如果一开始 service项目就不想开源。声明其module 就可以不加 github.com 。可以直接声明成一个 service module。最好是使用git init 初始化为本地的 repo。然后使用 replace 指令,将 service 包指定到本地某个目录。

除了针对大版本声明 module 需要 vMARJOR,其他时候,module 的名字是否加 github.com/username 并不是硬性要求,毕竟有 replace 大法。

依赖关系

通过上面从一个普通的 go package 的改造成 go module 的例子。可以看到 go 在包管理工具上的努力。尤其是对不兼容版本的支持,还是有一定防范。但是需要改源文件,还是略显不够优雅。另外,go mod还提供了一些了命令。如 go list 可以看出当前 module 的依赖

➜  demo go list -m -json all
{
    "Path": "demo",
    "Main": true,
    "Dir": "/Users/master/demo",
    "GoMod": "/Users/master/demo/go.mod",
    "GoVersion": "1.12"
}
{
    "Path": "github.com/rsj217/service",
    "Version": "v0.0.0-20190508051156-b9ee113ae63e",
    "Time": "2019-05-08T05:11:56Z",
    "Dir": "/Users/master/go/pkg/mod/github.com/rsj217/service@v0.0.0-20190508051156-b9ee113ae63e",
    "GoMod": "/Users/master/go/pkg/mod/cache/download/github.com/rsj217/service/@v/v0.0.0-20190508051156-b9ee113ae63e.mod",
    "GoVersion": "1.12"
}
{
    "Path": "github.com/rsj217/service/v3",
    "Version": "v3.0.0",
    "Replace": {
        "Path": "./vendor/github.com/rsj217/service/v3",
        "Dir": "/Users/master/demo/vendor/github.com/rsj217/service/v3",
        "GoVersion": "1.12"
    },
    "Dir": "/Users/master/demo/vendor/github.com/rsj217/service/v3",
    "GoMod": "/Users/master/demo/vendor/github.com/rsj217/service/v3/go.mod",
    "GoVersion": "1.12"
}

从上面的 json 可以看出,demo 项目依赖 github.com/rsj217/service 的 "v0.0.0-20190508051156-b9ee113ae63e" 版本,即 git 的(v2.2.0)。还依赖 github.com/rsj217/service/v3 的 v3.0.0 也是 git 的(v3.0.0)

总结

golang 1.12 正式发布了 go module 特性。go module 作为官方的包管理解决方案。既然针对旧软件包能work,也可以将旧软件包升级为 go module。

go module 淡化了 GOPATH 的概念。用户不设置 GOPATH,golang会自动在 home/go 添加到 GOPATH 中。go module 所下载的软件包存储在home/go/pkg/mod 中,并且可以存储多个版本的包。

为了管理包依赖,go module 引入了 go.mod文件, 通过 module 指令声明一个module,通过 require 指定依赖,通过 replace 替换依赖。

同时 软件包的版本提供了一个 版本格式 ,并且针对 大于 v1 的软件包,声明的module 也需要加上vMarjor。

总而言之,go module在不对原有的包管理带来破坏的情况下,引入了新的workflow,只要遵循这些约定,还是可以轻松的解决日常开发的包管理问题。

相关连接:

Introduction to Go Modules
Anatomy of Modules in Go
Go module 如何发布 v2 及以上版本?
golang modules问题的理解与踩坑记