Go Modules 初探
转载:https://bingohuang.com/go-modules-explore/
这篇文章是对 Go 官方依赖管理工具 Go Modules
机制的一次初探,教会你如何使用 Go modules
。
一、准备
1. Go Modules 定义
这里引用 TonyBai老师 初窥Go module 一文的总结:
通常我们会在一个repo(仓库)中创建一组Go package,repo的路径比如:github.com/bigwhite/gocmpp
, 会作为go package的导入路径(import path),Go 1.11给这样的一组在同一repo下面的packages赋予了一个新的抽象概念: module,并启用一个新的文件go.mod记录module的元信息。
并且一个repo可以拥有多个module,如下:
图:single repo,single module
图:single monorepo,multiple modules
2. 准备 Go 语言环境
需要安装 Go 1.11 以上的版本,当前最新版 1.12.5
。
3. Go Modules 配置
Go modules 机制作为 1.11
才正式引入的特性,当前还有一个特性开关:GO111MODULE
,对应的值有三个:auto/on/off
,这会影响 Go 所有命令依赖管理
的模式选择(即 GOPATH mode/module-aware mode
),这三个值的作用简要介绍如下:
1. off: GOPATH mode,和之前版本一样,默认查找vendor和GOPATH目录
2. on:module-aware mode,使用 go modules,将会忽略GOPATH目录,使用是go mod命令的缓存目录($GOPATH/pkg/mod)
3. auto:如果当前目录不在 $GOPATH 并且 当前目录(或者父目录)下有go.mod文件,则使用 GO111MODULE, 否则仍旧使用 GOPATH mode。
注意:当前版本的默认值是 auto
,从 Go 1.13
以后,将会默认设置为 on
,即 module-aware mode
。
二、实战
学习最好的方式就是实战!这里采用默认设置(GO111MODULE=auto
),将在 GOPATH
目录外创建一个新的工程(single Module
),使用 Go modules 机制,一步步教会你如何使用 Go modules。
创建 Module
以Windows
为例(Linux
和Mac
同理),在我的系统中GOPATH=D:\go
,接下来就在GOPATH
之外的目录(比如D:\gotest
)创建工程:
$ mkdir testgomod $ cd testgomod
继而在当前目录写一个简单的 Go 程序 hello.go
:
package testgomod import "fmt" // Say hello func Hello(who string) string { return fmt.Sprintf("Hello, %s", who) }
程序创建完成,但这还不是一个 module
,需要使用 go mod
命令初始化成一个 module
:
$ go mod init github.com/bingohuang/testgomod go: creating new go.mod: module github.com/bingohuang/testgomod
在该目录下,会生成了一个新的文件 go.mod
:
module github.com/bingohuang/testgomod go 1.12
到此,我们的工程就是一个 go module
了,区别就在于是否有这个 go.mod
文件。
接下来可以将工程推送到远程仓库:
$ git init $ git add . $ git commit -m "init commit" $ git push -u origin master
到这步,其他人就可以使用这个 go package
,比如使用最常用的 go get
方式: go get -v github.com/bingohuang/testgomod
这会拉取 master
分支的最新代码,但实际上这么做是有隐患的,因为我们不知道代码作者是否会修改代码,造成使用上的不兼容,而且也不能很好的管理代码版本。
2. 使用 Module
有了 Go modules 机制,就可以很方便的解决以上该问题。
Go modules 的主要设计理念包括:Semantic Import Versioning、Minimal Version Selection等,所以我们最好也对我们的 module
打上版本。
使用 git tag
的方式打版本,发布 1.0.0
:
$ git tag v1.0.0 $ git push --tags
这个将会在当前 commit 上,创建一个 tag v1.0.0
,并推送至远程仓库。
接下来,我们创建一个 Go 程序,使用上面我们推送的新包:github.com/bingohuang/testgomod
。
$ mkdir usegomod $ cd usegomod
package main import ( "fmt" "github.com/bingohuang/testgomod" ) func main() { fmt.Println(testgomod.Hello("bingohuang")) }
如果是以前,你通常需要通过 go get github.com/bingohuang/testgomod
来下载依赖包,不过有了 go modules
,将无需这么做,而且更加方便。
首先,同样使用 go mod
命令初始化代码工程,使其成为一个 Go Module:
go mod init github.com/bingohuang/usegomod
这样就生成一个我们熟知的文件 go.mod
,内容如下:
module github.com/bingohuang/usegomod go 1.12
接下来就是见证奇迹的时刻,使用原生 go build
命令编译该工程:
$ go build go: finding github.com/bingohuang/testgomod v1.0.0 go: downloading github.com/bingohuang/testgomod v1.0.0 go: extracting github.com/bingohuang/testgomod v1.0.0
可以看到,go 命令会自动的获取和下载远程三方包,此时再看 go.mod
文件,发现有了新的变化:
module github.com/bingohuang/usegomod go 1.12 require github.com/bingohuang/testgomod v1.0.0
并且还生成了一个新的文件:go.sum
,包含了所引用包的 hash
值,保证我们获取的是正确的版本和文件。
github.com/bingohuang/testgomod v1.0.0 h1:JdNLPaJoAvogFRBWAyyr5jrLAsKFv7axKYDOOeFUbOo= github.com/bingohuang/testgomod v1.0.0/go.mod h1:dSAc0893lV3VXfM/YX6n2s+lW3CxIXytDP//OgpmKFo=
同时,还可以使用 go list -m all
命令来列出当前的 module 信息和它的依赖包:
$ go list -m all github.com/bingohuang/usegomod github.com/bingohuang/testgomod v1.0.0
3. 更新 Module
A、小更新
假设我们需要修复点小 bug,更新这个库:github.com/bingohuang/testgomod
,git diff
如下:
+// Say hello func Hello(who string) string { - return fmt.Sprintf("Hello, %s", who) + return fmt.Sprintf("Hello, %s!", who) }
并发布新版 v1.0.1
:
$ git commit -m "update package" hello.go $ git tag v1.0.1 $ git push --tags origin master
接下来,我们该如何在使用该包的地方(github.com/bingohuang/usegomod
)做更新呢?
在此之前,先补充一个知识点:默认情况下,Go modules 是不会自动更新的,需要我们主动更新包依赖,同样还是使用 go get
,有三种更新方式:
go get -u
:更新到最新的 minor 或者 patch 版本,比如我们从 1.0.0 来做更新,将更新到 1.0.1,如果有更高版本 1.1.0,将会优先获取 1.1.0,但不会更新到 2.0.0 这种 major 版本go get -u=patch
:只更新最新的 patch 版本,也就是更新到 1.0.1,即使有 1.1.0,也不会去更新go get package@version
:更新到指定版本,比如:github.com/bingohuang/testgomod@v1.0.1,这个最直观
对我们要从 v1.0.0
更新到 v1.0.1
来说,以下几种更新方式都可行:
$ go get -u $ go get -u=patch $ go get github.com/bingohuang/testgomod@v1.0.1
运行之后,go.mod
和 go.sum
都会更新,其中 go.mod
关键变动如下:
require github.com/bingohuang/testgomod v1.0.1
B、大变动
再回到我们引用的包:github.com/bingohuang/testgomod
,此时,我们需要做一个大的变更,甚至会影响到函数签名,比如给函数添加参数和返回值:
// Say hello func Hello(who, lang string) (string, error) { switch lang { case "en": return fmt.Sprintf("Hello, %s!", who), nil cse "cn": return fmt.Sprintf("你好, %s!", who), nil default: return "", errors.New("unknown language") } }
这样的改动,将导致新的 API
不兼容之前版本,所以包的版本需要升级到 v2.0.0
。
为了给使用我们 package
的程序做好兼容,避免直接出现不兼容错误,需要在我们的 module
名称上加上一个版本路径,比如 V2
:
github.com/bingohuang/testgomod/v2
接下来还是和之前一样,提交、打tag 并 push:
$ git commit hello.go -m"change func Hello" $ git commit go.mod -m "Bump version to v2" $ git tag v2.0.0 $ git push --tags origin master
此时,对于其它使用该包的程序,比如:github.com/bingohuang/usegomod
,还是能正常运行,因为我们一直会使用 v1.0.1
,即使使用 go get -u
也不会更新到 v2.0.0
。
如果,我们就是想将 gotestmod
库升级到最新的 v2.0.0
版本,该如何做呢?
很简单,修改引入的 package
和函数
即可:
package main import ( "fmt" "github.com/bingohuang/testgomod/v2" ) func main() { hi, err := testgomod.Hello("bingohuang", "cn") if err != nil { panic(err) } fmt.Println(hi) }
注意:github.com/bingohuang/testgomod/v2
虽然是以 v2 结尾,但是 Go 引用的包名还是 testgomod
,非常方便。
这时,我们再运行 go build 或者 go run . ,将会自动为我们拉去 v2.0.0
版本:
$ go run . go: downloading github.com/bingohuang/testgomod/v2 v2.0.0 go: extracting github.com/bingohuang/testgomod/v2 v2.0.0 你好, bingohuang!
并且 go.mod
和 go.sum
都有所更新,其中 go.mod
关键变动如下:
require ( github.com/bingohuang/testgomod v1.0.1 github.com/bingohuang/testgomod/v2 v2.0.0 )
甚至,我们还可以同时使用这两个不兼容版本,如下:
package main import ( "fmt" "github.com/bingohuang/testgomod" testgomodV2 "github.com/bingohuang/testgomod/v2" ) func main() { fmt.Println(testgomod.Hello("bingohuang")) hi, err := testgomodV2.Hello("bingohuang", "cn") if err != nil { panic(err) } fmt.Println(hi) }
这个方式可以用来做升级过渡或调试。
4. 清理 Module
回到上文,我们修改了引用 package
路径,仅使用 v2.0.0
版本的情况,其中 go.mod 的关键变动如下:
require ( github.com/bingohuang/testgomod v1.0.1 github.com/bingohuang/testgomod/v2 v2.0.0 )
可以看到,这里未被使用的 v1.0.1
版本并未自动移除,Go 给我们提供了一个命令来主动移除不用的依赖,如下:
$ go mod tidy
5. Vender 机制
Go modules 机制会忽略 vendor 目录,甚至在最初的设计中,Go team
有想彻底废除 vendor
,但 vendor
毕竟使用多年,影响很大,在社区的反馈下,当前得以保留,并且 Go modules 支持将该 module 下所有的依赖都 copy 一份到 module 根目录
的 vendor目录
下,命令同样很简单:
$ go mod vendor
这样,如果你不在 module-aware 模式下,就可以使用 vendor 目录来编译:
$ go build
即使在 module-aware
模式下,也可以通过如下命令来使用 vendor 目录构建:
$ go build -mod vendor
3. 总结:
Go Modules 机制作为官方正式推出的包依赖管理机制,必定会步入大众的舞台,成为包管理工具的主导工具,并且会越来越完善,在此极力推荐大家学习使用。
最后,总结几条好的工程实践:
通常我们在
GOPATH
目录下开发居多,在当前版本下想使用 module-aware 模式,只需打开特性开关:export GO111MODULE=auto
配合
GOPROXY
,可以达到更好的效果,比如无需配置个人代理了,就可以方便下载任何 Go 第三方代码。比如可以采用开源工具 athens 搭建了一个Go Proxy 服务
,简要使用方法如下:
# 开启 Go Modules 机制 $ export GO111MODULE=on # 配置 Go Proxy $ export GOPROXY=http://go_proxy_server # 进入工程目录 $ cd 工程目录 # 初始化 Go Module $ go mod init 你的module名 # 使用 go 相关命令即可,包括 go build/go run/go test/go get 等 $ go build
JetBrain 推出的 Go IDE
Goland
也有对 Go modules 的集成支持,可以参考这篇文章:Working with Go Modules,可能会获得一些有趣的tips
。
参考资料
- 点赞
- 收藏
- 关注作者
评论(0)