关于Go Modules,看这一篇文章就够了

Go 1.13版本之后新的包管理器Modules趋于成熟,目前越来越多的开源项目已经支持Go Modules,典型的如etcd。Go具有相当长的包管理工具变迁史,各种包管理工具层出不穷,究其原因,还是官方没有实现足够好用包管理工具。本文不对部分基础知识做详解,主要重点是Go Modules,本文来源:[https://roberto.selbach.ca/in...]()

GOPATH的缺陷

几乎所有的包管理工具在Go 1.11版本之前都绕不开GOPATH这个环境变量。GOPATH主要用来放置项目依赖包的源代码,GOPATH不区分项目,代码中任何import的路径均从GOPATH为根目录开始;但现在GOPATH已经不够用了。

不区分依赖项版本

当有多个项目时,不同项目对于依赖库的版本需求不一致时,无法在一个GOPATH下面放置不同版本的依赖项。典型的例子:当有多项目时候,A项目依赖C 1.0.0,B项目依赖C 2.0.0,由于没有依赖项版本的概念,C 1.0.0和C 2.0.0无法同时在GOPATH下共存,解决办法是分别为A项目和B项目设置GOPATH,将不同版本的C源代码放在两个GOPATH中,彼此独立(编译时切换),或者C 1.0.0和C 2.0.0两个版本更改包名。无论哪种解决方法,都需要人工判断更正,不具备便利性。

依赖项列表无法数据化

在Go Modules之前,没有任何语义化的数据可以知道当前项目的所有依赖项,需要手动找出所有依赖。对项目而言,需要将所有的依赖项全部放入源代码控制中。如果剔除某个依赖,需要在源代码中手工确认某个依赖是否剔除。

为了解决GOPATH的缺陷,Go官方和社区推出许多解决方案,比如godep、govendor、glide等,这些工具要么未彻底解决GOPATH存在的问题要么使用起来繁冗,这才催生了Go Modules的出现。

如何使用Go Modules

简单来说,Go Modules是语义化版本管理的依赖项的包管理工具;它解决了GOPATH存在的缺陷,最重要的是,它是Go官方出品。(以下内容将会涉及到标准依赖项管理和构建)

创建Modules

首先创建一个可以供其他项目使用的项目:testmod,内容如下:

package testmod

import (
    "fmt"
)

func Hi(name string) string {
    return fmt.Sprintf("Hi, %s", name)
}

使用如下方式创建modules:
go mod init github.com/robteix/testmod
该命令会在目录下生成go.mod文件,内容如下:

module github.com/robteix/testmod

go 1.13

在将代码推送至Github之后,其他人可以使用如下命令下载到testmod:

go get github.com/robteix/testmod

默认情况下,go get将会下载到testmodmaster分支(在没有tags的情况下),即代码主分支。上面我们说过,Go Modules具有语义化版本管理功能的,所以可以使用go get下载特定版本的包:

go get github.com/robteix/testmod@vX.X.X

模块版本

Go Modules是版本化的,并且某些版本具有某些特殊性。Go默认语义化版本控制,语义化版本详见:[https://semver.org/lang/zh-CN/]()。

Go在查找包版本时使用仓库tags,在一些情况下,某些版本和其他版本不同:版本2和更高版本的导入路径应该与版本0和版本1不。默认情况下,Go获取仓库中可用的最新标记版本。

发布第一个版本

我们可以通过使用版本标签来发布1.0.0版本:

git tag v1.0.0
git push --tags

此时将会在Github的仓库上创建名为v1.0.0的标签。推荐的做法是创建新的代码分支,这样可以直接在分支上修改v1.0.0的问题,而不影响主分支的开发进度。

git checkout -b v1
git push -u origin v1

使用已发布的版本

现在创建一个使用testmod包的项目:

package main

import (
    "fmt"
    "github.com/robteix/testmod"
)

func main() {
    fmt.Printf(testmod.Hi("roberto"))
}

执行go mod init mymod,将会生成go.mod文件,然后go build,此时会输出:

$ go build
go: finding github.com/robteix/testmod v1.0.0
go: downloading github.com/robteix/testmod v1.0.0

go get命令自动执行尝试在Github上下载最新版本标签的testmod,完成之后go.mod文件中新增了:

require (
    require github.com/robteix/testmod v1.0.0
)

意即当前项目依赖testmod v1.0.0版本。

发布修复版本

假设testmod v1.0.0需要进行问题修复:

// Hi returns a friendly greeting
func Hi(name string) string {
-   return fmt.Sprintf("Hi, %s", name)
+   return fmt.Sprintf("Hi, %s!", name)
}

我们在v1分支中进行此修复:

$ git commit -m "Emphasize our friendliness" testmod.go
$ git tag v1.0.1
$ git push --tags origin v1

更新模块

默认情况下,出于构建中的可预测性和稳定性考虑,Go不会自动更新模块,需要手动更新依赖。可以使用如下方式更新依赖包:

  1. 使用go get -u,更新到修订版本或次要版本,即从v1.0.0更新到v1.0.1,如果v1.1.0可用,则更新到v1.1.0。
  2. 使用go get -u=path,更新到修订版本,即从v1.0.0更新到v1.0.1。
  3. 使用go get package-path@vX.X.X更新到特定版本。

使用上述任一方式更新之后,go.mod中的依赖记录被更新如下:

require github.com/robteix/testmod v1.0.1

主要版本

根据语义化版本控制,主要版本与次要版本不同,主要版本可能会破坏向后兼容性。从Go模块角度来看,主要版本是完全不同的软件包:两个不兼容的库版本是两个不同的仓库。

package testmod

import (
     "errors"
     "fmt" 
) 

// Hi returns a friendly greeting in language lang
func Hi(name, lang string) (string, error) {
     switch lang {
     case "en":
         return fmt.Sprintf("Hi, %s!", name), nil
     case "pt":
         return fmt.Sprintf("Oi, %s!", name), nil
     case "es":
         return fmt.Sprintf("¡Hola, %s!", name), nil
     case "fr":
         return fmt.Sprintf("Bonjour, %s!", name), nil
     default:
         return "", errors.New("unknown language")
     }
}

现在testmodHi函数已经和v1.0.x不兼容了,是时候发布v2.x.x版本了。最佳的做法是:v2.x.x以及更高的版本更改导入路径。修改go.mod文件中的模块导入路径(也可以使用echo命令修改):

module github.com/robteix/testmod/v2

然后创建v2分支并将版本打上v2.0.0的发布标签:

$ git commit testmod.go -m "Change Hi to allow multilang"
$ git checkout -b v2 # optional but recommended
$ echo "module github.com/robteix/testmod/v2" > go.mod
$ git commit go.mod -m "Bump version to v2"
$ git tag v2.0.0
$ git push --tags origin v2 # or master if we don't have a branch

更新到主要版本

现在修改mymode项目引入testmod v2.0.0版本,Go在编译时可以根据import路径自动下载依赖包:

package main

import (
     "fmt"
     "github.com/robteix/testmod/v2" 
)

func main() {
     g, err := testmod.Hi("Roberto", "pt")
     if err != nil {
         panic(err)
     }
     fmt.Println(g)
}

当运行go build的时候,会自动下载testmod v2.0.0版本。由于testmod v1和v2的导入路径完全不一样,在mymod项目里可以同时存在:

package main
import (
     "fmt"
     "github.com/robteix/testmod"
     testmodML "github.com/robteix/testmod/v2"
)

func main() {
     fmt.Println(testmod.Hi("Roberto"))
     g, err := testmodML.Hi("Roberto", "pt")
     if err != nil {
         panic(err)
     }
     fmt.Println(g)
}

这里消除了依赖性管理的一个常见问题:依赖于同一库的不同版本。

依赖本地包

有一种可能出现的情况:项目的某个依赖包并不在Github或者其他代码托管网站上,而是在本地,此时需要修改go.mod文件引入本地依赖包。

require (
    3rd/module/testmod v0.0.0
)

replace 3rd/module/testmod => /usr/local/go/testmod

代码中以3rd/module/testmod作为导入路径,编译时会根据replace找到真实代码目录。

CI

有一种现实情况是:内部构建系统是无网络环境的,也就是说所有的依赖项都需要纳入内部版本控制中,Go Modules提供了此功能:

go mod vendor

该命令会将在当前项目根目录下创建vendor目录,然后将项目所有依赖项缓存此目录中,而此目录可以直接进入内部版本控制。在默认情况下go build将忽略vendor目录,如果要从vendor目录开始构建:

go build -mod vendor

这样做的好处可以不用依赖网络上游的版本,在内部自由使用稳定可控制的版本进行构建,go mod vendor应该会成为非开源项目的主要构建方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值