gopath
的含义、功能、优劣、以及如何通过GOPATH来组织项目go module
的原理和用法以试图能够回答下面的几个问题
在go module
之前,有一些问题长期困扰go语言的开发人员
gopath
之外go modules
正是为了解决上面的问题诞生的,下面我们详细介绍go module
企图解决的问题gopath
时,我们介绍了如果导入为import "github.com/gobuffalo/buffalo"
实际引用的是$GOPATH/src/github.com/gobuffalo/buffalo
文件中的代码。
gopath
中 ,导入路径与项目在文件系统中的目录结构和名称必须是匹配的。import
路径为github.com/gobuffalo/buffalo
,但是项目实际的路径却是在另一个任意的文件目录中?(例如/users/gobuffalo/buffalo
).答案是肯定的,go module
通过在一个特殊的叫做go.mod
的文件中指定模块名来解决这一问题。## go.mod
01 module github.com/gobuffalo/buffalo
02
...
06
在go.mod文件的第一行指定了模块名,模块名表示开发人员可以用此来引用当前代码仓库中任何package
的路径名,以此来替代$gopath
的路径。从而,代码仓库在任何位置都已经没有关系,因为Go工具可以使用模块文件的位置和模块名来解析代码仓库中的任何内部import
。
gopath
维护单一的master包的方式是远远不够的,因为依赖包的最新代码不一定与项目兼容。尽管go社区已经针对以上问题提供了一些解决方案(例如dep,godep,glide等)但是go官方的go moudle
提供了一种集成解决方案,通过在文件中维护直接和间接依赖项的版本列表来解决这一问题。通过将一个特定版本的依赖项看做是捆绑的不可变的依赖项,就叫做一个模块(moudle)为了加快构建程序的速度并快速切换、获取项目中依赖项的更新,Go维护了下载到本地计算机上的所有模块的缓存,缓存目前默认位于$GOPATH/pkg
目录中。有go的提议希望能够自定义缓存的位置。
所在位置看上去如下所示:
go/
├── bin
├── pkg
├── darwin_amd64
└── mod
└── src
在mod目录下,我们能够看到模块名路径中的第一部分用作了模块缓存中的顶级文件夹
~/go/pkg/mod » ls -l jackson@192
drwxr-xr-x 6 jackson staff 192 1 15 20:50 cache
drwxr-xr-x 7 jackson staff 224 2 20 17:50 cloud.google.com
drwxr-xr-x 3 jackson staff 96 2 18 12:03 git.apache.org
drwxr-xr-x 327 jackson staff 10464 2 28 00:02 github.com
drwxr-xr-x 8 jackson staff 256 2 20 17:27 gitlab.followme.com
drwxr-xr-x 6 jackson staff 192 2 19 22:05 go.etcd.io
...
当我们打开一个实际的模块,例如github.com/nats-io
,我们会看到与nats库有关许多模块
~/go/pkg/mod » ls -l github.com/nats-io jackson@192
total 0
dr-x------ 24 jackson staff 768 1 17 10:27 [email protected]
dr-x------ 15 jackson staff 480 2 17 22:22 [email protected]
dr-x------ 26 jackson staff 832 2 19 22:05 [email protected]
dr-x------ 26 jackson staff 832 1 17 10:27 [email protected]
...
为了拥有一个干净的工作环境,我们可以用如下代码清空缓存区。但是请注意,在正常的工作流程中,是不需要执行如下代码的。
$ go clean -modcache
GOPATH
外开始一个新的项目讲解,新建一个新建夹以及一个main
文件$ cd $HOME
$ mkdir mathlib
$ cd mathlib jackson@192
$ touch main.go
~/mathlib » go mod init github.com/dreamerjackson/mathlib
go mod init
指令的功能很简单,自动生成一个go.mod
文件 后面紧跟的路径即是自定义的模块名。习惯上以托管代码仓库的URL为模块名(代码将会放置在https://github.com/dreamerjackson/mathlib
下)go.mod
文件 位于项目的根目录下,内容如下所示,第一行即为模块名。module github.com/ardanlabs/service
#### 引入第三方模块
go 1.13
package main
import "github.com/dreamerjackson/mydiv"
func main(){
}
go moudle
而特地的引入的packagegithub.com/dreamerjackson/mydiv
,其进行简单的除法操作,同时又引入了另一个包github.com/pkg/errors
。其代码如下图所示:go mod tidy
指令$ go mod tidy
go: finding github.com/dreamerjackson/mydiv latest
go: downloading github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161
go: extracting github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161
同时我们在go.mod
中能够看到新增加了一行用于表示我们引用的依赖关系
module github.com/dreamerjackson/mathlib
go 1.13
require github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161
github.com/dreamerjackson/mydiv
依赖的github.com/pkg/errors
)并没有也没有必要在go.mod
文件展示出来,而是出现在了一个自动生成的新的文件go.sum
中.## go.sum
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161 h1:QR1fJ05yjzJ0qv1gcUS+gAe5Q3UU5Y0le6TIb2pcJpQ=
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161/go.mod h1:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
package main
import (
"fmt"
"github.com/dreamerjackson/mydiv"
)
func main(){
res,_ :=mydiv.Div(4,2)
fmt.Println(res)
}
go run
命令后,即会为我们输出除法结果2
go.mod
文件中修改版本号为:require github.com/dreamerjackson/mydiv latest
或者
require github.com/dreamerjackson/mydiv master
获取复制commitId 到最后
require github.com/dreamerjackson/mydiv c9a7ffa8112626ba6c85619d7fd98122dd49f850
还有一种办法是在终端当前项目中,运行go get
go get github.com/dreamerjackson/mydiv
go mod tidy
即会进行更新此时如果我们再次打开go.sum
文件会发现,go.sum
中不仅仅存储了直接和间接的依赖,还会存储过去的版本信息。
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161 h1:QR1fJ05yjzJ0qv1gcUS+gAe5Q3UU5Y0le6TIb2pcJpQ=
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161/go.mod h1:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ=
github.com/dreamerjackson/mydiv v0.0.0-20200305090126-c9a7ffa81126/go.mod h1:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
当我们不想在使用此第三方包时,可以直接在代码中删除无用的代码,接着执行
$ go mod tidy
会发现go.mod
与go.sum
一切又都空空如也~
github.com/dreamerjackson/mydiv
的最新版本为v1.0.2
,可通过下面指令查看所有> go list -m -versions github.com/dreamerjackson/mydiv
github.com/dreamerjackson/mydiv v1.0.0 v1.0.1 v1.0.2 v1.0.3
假设现在有两个模块A、B,都依赖模块D。其中
A -> D v1.0.1,
B -> D v1.0.2
go module
会如何选择呢?像dep这样的依赖工具将选择v1.0.3,即最新的语义版本控制。但是在go module
中,最小版本选择原理将遵循A项目声明的版本,即v1.0.1github.com/dreamerjackson/mydiv
为例,读者可以将其看做上节中的模块D
,其v1.0.1
与v1.0.2
版本的代码如下,只是简单的改变了错误返回的字符串。## v1.0.1
package mydiv
import "github.com/pkg/errors"
func Div(a int,b int) (int,error){
if b==0{
return 0,errors.Errorf("new error b can't = 0")
}
return a/b,nil
}
## v1.0.2
package mydiv
import "github.com/pkg/errors"
func Div(a int,b int) (int,error){
if b==0{
return 0,errors.Errorf("new error b can't = 0")
}
return a/b,nil
}
模块B
即github.com/dreamerjackson/minidiv
引用了模块D即github.com/dreamerjackson/mydiv
v1.0.1版本## 模块B
package div
import (
"github.com/dreamerjackson/mydiv"
)
func Div(a int,b int) (int,error){
return mydiv.Div(a,b)
}
模块Now
直接依赖了模块D v1.0.2,同时依赖了模块Bpackage main
import (
"fmt"
div "github.com/dreamerjackson/minidiv"
"github.com/dreamerjackson/mydiv"
)
func main(){
_,err1:= mydiv.Div(4,0)
_,err2 := div.Div(4,0)
fmt.Println(err1,err2)
}
当前的依赖关系如下:
当前模块 --> 模块D v1.0.2
当前模块 --> 模块B --> 模块D v1.0.1
$ go run main.go
v1.0.2 b can't = 0 v1.0.2 b can't = 0
go list
指令~/mathlib » go list -m all | grep mydiv
github.com/dreamerjackson/mydiv v1.0.2
我们还可以通过使用go mod mhy
z指令,查看在哪里引用了包github.com/dreamerjackson/mydiv
~/mathlib » go mod why github.com/dreamerjackson/mydiv
# github.com/dreamerjackson/mydiv
github.com/dreamerjackson/mathlib
github.com/dreamerjackson/mydiv
go list -m -u all
指令查看直接和间接模块的当前和最新版本~/mathlib » go list -m -u all | column -t jackson@192
go: finding github.com/dreamerjackson/minidiv latest
github.com/dreamerjackson/mathlib
github.com/dreamerjackson/minidiv v0.0.0-20200305104752-fcd15cf402bb
github.com/dreamerjackson/mydiv v1.0.2 [v1.0.3]
github.com/pkg/errors v0.9.1
github.com/dreamerjackson/mydiv
的当前版本为v1.0.2
,但是最新的版本为v1.0.3
go get
指令。其中有不少的参数。最小版本原则
更新所有的直接和间接模块go get -t -d -v ./...
-t
考虑构建测试所需的模块-d
下载每个模块的源代码-v
提供详细输出./…
在整个源代码树中执行这些操作,并且仅更新所需的依赖项-u
参数会用最大最新版本
原则更新所有的直接和间接模块~/mathlib » go get -u -t -d -v ./... jackson@192
go: finding github.com/dreamerjackson/minidiv latest
go: downloading github.com/dreamerjackson/mydiv v1.0.3
go: extracting github.com/dreamerjackson/mydiv v1.0.3
github.com/dreamerjackson/mydiv
已经强制更新到了最新的v1.0.3~/mathlib » go list -m all | grep mydiv jackson@192
github.com/dreamerjackson/mydiv v1.0.3
go.mod go.sum
模块文件并再次运行go mod tidy来重置。当项目还不太成熟时这是一种选择。$ rm go.*
$ go mod init
$ go mod tidy
1
。但是一般我们将版本升级到了v2.0.0
,即被认为是出现了重大的更新。my/thing/v2
标识特定模块的语义主版本2
。版本1是my/thing
,模块路径中没有明确的版本。但是,当您引入主要版本2或更大版本时,必须在模块名称后添加版本,以区别于版本1和其他主要版本,因此版本2为my/thing/v2,版本3为my/thing/v3,依此类推。A --> 模块B --> 模块D v1.0.0
A --> 模块C --> 模块D v2.0.0
v2.0.0 b can't = 0
package mydiv
import "github.com/pkg/errors"
func Div(a int,b int) (int,error){
if b==0{
return 0,errors.Errorf("v2.0.0 b can't = 0")
}
return a/b,nil
}
module github.com/dreamerjackson/mydiv/v2
package main
import (
"fmt"
div "github.com/dreamerjackson/minidiv"
mydiv "github.com/dreamerjackson/mydiv/v2"
)
func main(){
_,err1:= mydiv.Div(4,0)
_,err2 := div.Div(4,0)
fmt.Println(err1,err2)
}
mathlib --> 直接引用mydiv v2
mathlib --> 直接引用minidiv --> 间接引用mydiv v1
当我们运行代码之后,会发现两段代码是共存的
v2.0.0 b can't = 0 :: v1.0.1 b can't = 0
接着执行go list
,模块共存,验证成功~
~/mathlib(master*) » go list -m all | grep mydiv
github.com/dreamerjackson/mydiv v1.0.1
github.com/dreamerjackson/mydiv/v2 v2.0.1
模块镜像于2019年八月推出,是go官方1.13版本的默认系统。模块镜像是一个代理服务器,以帮助加快构建本地应用程序所需的模块的获取。代理服务器实现了基于REST的API,并根据Go工具的需求进行了设计。
模块镜像将会缓存已请求的模块及其特定版本,从而可以更快地检索将来的请求。一旦代码被获取并缓存在模块镜像中,就可以将其快速提供给世界各地的用户。
checksum数据库也于2019八月推出,是可以用来防止模块完整性、有效性的手段。它验证特定版本的任何给定模块代码的正确性,而不管何人何时何地以及是如何获取的。Google拥有唯一的校验和数据库,但是可以通过私有模块镜像对其进行缓存。
有几个环境变量可以控制与模块镜像和checksum数据库有关的行为
direct
。如果将此设置为off
,则将不会下载模块go env
来查看到这些默认值$ go env
GONOPROXY=""
GONOSUMDB=""
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOSUMDB="sum.golang.org"
gitlab
等地的代码,我们可以使用export GOPRIVATE=gitlab.XXX.com,gitlab.XXX-XX.com,XXX.io
多个域名用逗号分隔。Athens是一个私有模块镜像,可以用于搭建私有模块镜像。使用私有模块镜像的一个原因是允许缓存公共模块镜像无法访问的私有模块。Athens项目提供了一个在Docker Hub
上发布的Docker
容器,因此不需要特殊的安装。
docker run -p '3000:3000' gomods/athens:latest
接下来,启动一个新的终端会话以运行Athens,为大家演示其用法。启动Athens服务并通过额外的参数调试日志(请确保系统已经安装并启动了docker)并有科学*上网的环境
$ docker run -p '3000:3000' -e ATHENS_LOG_LEVEL=debug -e GO_ENV=development gomods/athens:latest
INFO[7:11AM]: Exporter not specified. Traces won't be exported
2020-03-06 07:11:30.671249 I | Starting application at port :3000
3000
端口,初始化我们之前的项目,再次执行go mod tidy
$ export GOPROXY="http://localhost:3000,direct"
$ rm go.*
$ go mod init github.com/dreamerjackson/mathlib
$ go mod tidy
在Athens日志中即可查看对应信息
INFO[7:39AM]: incoming request http-method=GET http-path=/github.com/dreamerjackson/mydiv/@v/list http-status=200
INFO[7:39AM]: incoming request http-method=GET http-path=/github.com/dreamerjackson/minidiv/@v/list http-status=200
INFO[7:39AM]: incoming request http-method=GET http-path=/github.com/dreamerjackson/minidiv/@latest http-status=200
gopath
管理go代码的优势在本文中,使用详细的实例讲解了go module
是什么,为什么需要,其最佳实践以及其实现原理。希望读者在这篇文章之后,能够回答我们在开头提出的问题
go module
是什么?go module
为什么需要?go module
的基本使用方法是什么?go module
如何管理版本与依赖?go module
如何解决依赖的冲突问题?go module
环境变量的配置与使用方式?module
镜像?