golang快速入门[1]-go语言导论linux
《庄子》‘逍遥游’有云:且夫水之积也不厚,则其负大舟也无力。覆杯水于坳堂之上,则芥为之舟;置杯焉则胶,水浅而舟大也。
咱们今天要介绍的就是北冥神功—go module绝技,以吸取他人内力为己所用。并且别人的内力愈强 吸力愈大。极天下代码于一身,好不快活。
在上文中,咱们介绍了gopath
的含义、功能、优劣、以及如何经过GOPATH来组织项目
在本文中,咱们将介绍go module
的原理和用法以试图可以回答下面的几个问题
go module 是什么?
go module为何须要?
go module的基本使用方法是什么?
go module如何管理版本与依赖?
go module如何解决依赖的冲突问题?
go module 环境变量的配置与使用方式?
如何搭建私有 module镜像?
在go module
以前,有一些问题长期困扰go语言的开发人员
可否将go工程代码脱离gopath
以外
可否处理版本依赖问题而且自动选择最兼容的依赖版本
可否使用go工具本地管理依赖项,自定义依赖项
go1.11开始支持,go1.13全面支持的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
。
对于任何版本控制(VCS)工具,咱们都能在任何代码提交点打上"tag"标记,以下所示:
使用VCS工具,开发人员能够经过引用特定标签将软件包的任何特定版本克隆到本地。
当咱们引用一个第三方包时,可能并不老是但愿应用项目最新的代码,而是某一个特定与当前项目兼容的代码。对于某一个项目来讲,可能并无意识到有人在使用他们的代码,或者某种缘由进行了巨大的不兼容更新。
咱们但愿可以指明须要使用的第三方包的版本,而且go工具可以方便下载、管理
更棘手的是,一个第三方包A可能引用了其余的第三方包B,所以还必须把第三方包A的所有依赖下载
如何查找并把全部的依赖包下载下来?
某一个包下载失败应该怎么办?
全部项目之间如何进行依赖的传导?
如何选择一个最兼容的包?
如何解决包的冲突?
若是但愿在项目中同时引用第三方包的二个不一样版本,须要如何处理?
所以,只经过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 gnatsd@v1.4.1
dr-x------ 15 jackson staff 480 2 17 22:22 go-nats-streaming@v0.4.0
dr-x------ 26 jackson staff 832 2 19 22:05 go-nats@v1.7.0
dr-x------ 26 jackson staff 832 1 17 10:27 go-nats@v1.7.2
...
为了拥有一个干净的工做环境,咱们能够用以下代码清空缓存区。可是请注意,在正常的工做流程中,是不须要执行以下代码的。
$ go clean -modcache
咱们从GOPATH
外开始一个新的项目讲解,新建一个新建夹以及一个main
文件
$ cd $HOME
$ mkdir mathlib
$ cd mathlib jackson@192
$ touch main.go
接着在当前目录中,执行以下指令初始化moudle。
~/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
。其代码以下图所示:
以下图所示,在goland中咱们能够看到导入的package 是红色的,由于此时在go module的缓存并不能找到此package。
为了可以将此package下载到本地,咱们可使用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
一切又都空空如也~
每一个依赖管理解决方案都必须解决选择依赖版本的问题,当今存在的许多版本选择算法都试图识别任何依赖的“最新最大”版本。若是您认为语义版本控制被正确应用而且将遵照约定,那么这是有道理的。在这些状况下,依赖项的“最新最大”版本应该是最稳定和安全的版本,而且应与较早版本具备向后兼容性。
Go决定采用其余方法,Russ Cox花费了大量时间和精力撰写和谈论 Go团队的版本选择方法,即最小版本选择(Minimal Version Selection,MVS)。从本质上讲,Go团队相信MVS能够为Go程序提供最佳的机会,以实现兼容性和可重复性。我建议阅读这篇文章,以了解Go团队为何相信这一点。
go最小版本选择指的是选择项目中最合适的最小版本。并非说MVS不能选择最新的版本,而是若是项目中任何依赖不须要最新的版本,则不须要它。
举一个简单的例子,假设如今项目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
若是咱们的当前项目只依赖A,这个时候go module
会如何选择呢?像dep这样的依赖工具将选择v1.0.3,即最新的语义版本控制。可是在go module
中,最小版本选择原理将遵循A项目声明的版本,即v1.0.1
若是随后当前项目又引入了模块B的新代码怎么办?将模块B导入项目后,Go会将项目的模块D版本从v1.0.1升到v1.0.2。为模块D的全部依赖项(模块A和B)选择模块D的“最小”版本,该版本当前处于所需版本集(v1.0.1和v.1.0.2)中
最后,若是删除刚刚为模块B添加的代码,会发生什么?Go会将项目锁定到模块D的版本v1.0.2中。降级到版本v1.0.1将是一个更大的更改,而Go知道版本v1.0.1能够正常运行而且稳定,所以版本v1.0.2仍然是“最新版本”。
为了验证最小版本选择原理,做者呕心沥血设计了一个简单的示例。
以项目github.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,同时依赖了模块B
当前代码以下:
package 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
所以咱们将验证,是否和咱们所料,当前项目选择了模块D v1.0.2 呢?
验证方式有两种:第一种为直接运行,查看项目采用了哪个版本的代码
$ go run main.go
v1.0.2 b can't = 0 v1.0.2 b can't = 0
如上所示,输出的结果所有是咱们在模块D v1.0.2中定义的代码!
第二种方式是使用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
提供详细输出
./…
在整个源代码树中执行这些操做,而且仅更新所需的依赖项
注意,除非你了解项目的全部细节,不然慎用所有的最大最新版本的更新
若是go get中使用-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 <module name>
$ go mod tidy
Go模块引入了一种新的导入路径语法,即语义导入版本控制。每一个语义版本均采用vMAJOR.MINOR.PATCH的形式。
MAJOR 主版本号,若是有大的版本更新,致使 API 和以前版本不兼容。咱们遇到的就是这个问题。
MINOR 次版本号,当你作了向下兼容的新 feature。
PATCH 修订版本号,当你作了向下兼容的修复 bug fix。
v 全部版本号都是 v 开头。
若是两个版本具备相同的主编号,则预期更高版本(若是您愿意,更大版本)将与较早版本(较小版本)向后兼容。可是,若是两个版本的主要编号不一样,则它们之间没有预期的兼容性关系。
所以咱们在上面的实例中能够看到,go预料到v1.0.3与v1.0.1是兼容的,由于他们有相同的主版本号1
。可是通常咱们将版本升级到了v2.0.0
,即被认为是出现了重大的更新。
如上图实例显示了go对于版本更新的处理。my/thing/v2
标识特定模块的语义主版本2
。版本1是my/thing
,模块路径中没有明确的版本。可是,当您引入主要版本2或更大版本时,必须在模块名称后添加版本,以区别于版本1和其余主要版本,所以版本2为my/thing/v2,版本3为my/thing/v3,依此类推。
假设模块A引入了模块B和模块C,模块B引入了模块Dv1.0.0,模块C引入了模块Dv2.0.0。则看起来就像是
A --> 模块B --> 模块D v1.0.0
A --> 模块C --> 模块D v2.0.0
因为v1 和v2 模块的路径不相同,所以他们之间会是互不干扰的两个模块。
下面咱们用实例来验证
首先咱们给mydiv打一个v2.0.0的tag,其代码以下,简单修改了错误文字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
}
同时须要修改v2模块路径名为:
module github.com/dreamerjackson/mydiv/v2
接着在mathlib中,代码以下:
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数据库有关的行为
GOPROXY:一组指向模块镜像的URL,用于获取模块。若是您但愿Go工具仅直接从VCS地址获取模块,则能够将其设置为direct
。若是将此设置为off
,则将不会下载模块
GOSUMDB:用于验证给定模块/版本的代码的checksum数据库地址。此地址用于造成一个适当的URL,该URL告诉Go工具在哪里执行这些checksum校验和查找。该URL能够指向Google拥有的checksum数据库,也能够指向支持对checksum数据库进行缓存或代理的本地模块镜像。若是您不但愿Go工具验证添加到go.sum文件中的给定模块/版本的哈希码,也能够将其设置为off,仅在将任何新module添加到go.sum文件之时,才查询checksum数据库
GONOPROXY:一组基于URL的模块路径,这些模块不会使用模块镜像来获取,而应直接在VCS地址上获取。
GONOSUMDB:一组基于URL的模块路径,这些模块的哈希码不会在checksum数据库中查找。
GOPRIVATE:一个方便变量,用于将GONOPROXY和GONOSUMDB设置为相同的默认值
咱们能够经过go env
来查看到这些默认值
$ go env
GONOPROXY=""
GONOSUMDB=""
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOSUMDB="sum.golang.org"
这些默认值告诉Go工具使用Google模块镜像和Google checksum数据库。若是这些Google服务能够访问您所需的全部代码,则建议使用此配置。若是Google模块镜像刚好以410(消失)或404(未找到)响应,则使用direct(这是GOPROXY配置的一部分)将容许Go工具更改路线并直接获取模块/版本VCS位置。
例如,若是咱们须要访问全部代理服务器,例如须要权限的位于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
接着咱们修改GOPROXY参数,指向本地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
详细信息,查看参考资料中Athens的官方网站
提供脱离gopath
管理go代码的优点
提供了代码捆绑、版本控制、依赖管理的功能
供全球开发人员使用、构建,下载,受权、验证,获取,缓存和重用模块(能够经过搭建本身的代理服务器来实现这些功能)
能够验证模块(对于任何给定的版本)始终包含彻底相同的代码,而无论它被构建了多少次,从何处获取以及由谁获取
在本文中,使用详细的实例讲解了go module
是什么,为何须要,其最佳实践以及其实现原理。但愿读者在这篇文章以后,可以回答咱们在开头提出的问题
go module
是什么?
go module
为何须要?
go module
的基本使用方法是什么?
go module
如何管理版本与依赖?
go module
如何解决依赖的冲突问题?
go module
环境变量的配置与使用方式?
如何搭建私有module
镜像?
see you~
项目连接
做者知乎
blog
Athens
talk youtube
Modules Part 03: Minimal Version Selection
How to Write Go Code
Go module 如何发布 v2 及以上版本?
The Principles of Versioning in Go