«

Go Modules 初探

这篇文章是对 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。

  1. 创建 Module
    Windows 为例(LinuxMac同理),在我的系统中 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 VersioningMinimal 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/testgomodgit 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,有三种更新方式:

对我们要从 v1.0.0 更新到 v1.0.1 来说,以下几种更新方式都可行:

$ go get -u
$ go get -u=patch
$ go get github.com/bingohuang/testgomod@v1.0.1

运行之后,go.modgo.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.modgo.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 机制作为官方正式推出的包依赖管理机制,必定会步入大众的舞台,成为包管理工具的主导工具,并且会越来越完善,在此极力推荐大家学习使用。

最后,总结几条好的工程实践:

# 开启 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

参考资料

  1. Using Go Modules
  2. Introduction to Go Modules
  3. 初窥Go module
分享