初探Go Module


初探Go Module

Go 语言从2007年诞生至今,已经发展和演化十多年了。这十多年来,Go 取得了巨大的成就,先后于2009年和2016年当选 TIOBE 年度最佳编程语言,并在全世界范围内拥有数量庞大的拥趸。当然 Go 语言也不是完美的 —— 这些年来 Go 在“包依赖管理”和“缺少泛型”两个方面就饱受诟病,它们也是 Go 开发者最希望 Go 核心开发团队重点完善的两个方面。
Go modules 刚刚 merge 到 Go trunk 中,问题还会有很多,不过这是 Go team 在解决包依赖管理方面的一次勇敢尝试。无论如何,对 Go 语言来说都是一个好事。merge 后很多 gopher 也提出了诸多问题,可以在这里查看。如果你也遇到了 go modules 方面的问题,可以在 Go 的 GitHub 仓库中提 issue,帮助 Go team 尽快更好地完善 Go 1.11 的 Go modules 机制。

Go提供了两个关键的环境变量,GOROOT指向系统安装路径,GOPATH指向工作路径,这样的好处是我们的工作可以系统文件分离。
例如:

GOROOT = C:\Go
GOPATH = D:\MyWorks

理解GOPATH环境变量变量,就明白启用Go Module前后的变化。先来说说GOPATH环境变量
GOPATH是作为编译后二进制的存放目的地和import包时的搜索路径 (其实也是你的工作目录, 你可以在src下创建你自己的go源文件, 然后开始工作)。如果设置就使用已经设置的值,如果没有设置就使用默认值,默认值使用go env

GOPATH主要包含三个目录:bin、pkg、src,目录结构如下:

  • bin目录主要存放编译后生成的可执行文件
  • pkg存放编译时生成的中间文件(比如:.a)
  • src存放源代码(比如:.go .c .h .s等) 按照golang默认约定,go run,go install等命令的当前工作路径(即在此路径下执行上述命令)。

需要注意的是:在1.11版本中,GOPATH不再用于解析。但仍然用于存储下载的源代码(在GOPATH/pkg/mod中)和编译的命令(在GOPATH/bin中)。

我们知道go get获取的代码会放在$GOPATH/src下面,而go build会在$GOROOT/src和$GOPATH/src下面按照import path去搜索package,由于go get 获取的都是各个package repo的trunk/mainline的代码,因此,Go 1.5之前的Go compiler都是基于目标Go程序依赖包的trunk/mainline代码去编译的。这样的机制带来的问题是显而易见的,至少包括:

  • 因依赖包的trunk的变化,导致不同人获取和编译你的包/程序时得到的结果实质是不同的,即不能实现reproduceable build
  • 因依赖包的trunk的变化,引入不兼容的实现,导致你的包/程序无法通过编译
  • 因依赖包演进而无法通过编译,导致你的包/程序无法通过编译

为了实现reporduceable build,Go 1.5引入了Vendor机制,Go编译器会优先在vendor下搜索依赖的第三方包,这样如果开发者将特定版本的依赖包存放在vendor下面并提交到code repo,那么所有人理论上都会得到同样的编译结果,从而实现reporduceable build。

在Go 1.5发布后的若干年,gopher们把注意力都集中在如何利用vendor解决包依赖问题,从手工添加依赖到vendor、手工更新依赖,到一众包依赖管理工具的诞生:比如: govendor、glide以及号称准官方工具的dep,努力地尝试着按照当今主流思路解决着诸如:“钻石型依赖”等难题。

正当gopher认为dep将“顺理成章”地升级为go toolchain一部分的时候,vgo横空出世,并通过对“Semantic Import Versioning”和”Minimal Version Selected”的设定,在原Go tools上简单快速地实现了Go原生的包依赖管理方案 。vgo就是go module的前身。

go modules定义、experiment开关以及“依赖管理”的工作模式

通常我们会在一个repo(仓库)中创建一组Go package,repo的路径比如:github.com/bigwhite/gocmpp会作为go package的导入路径(import path),Go 1.11给这样的一组在同一repo下面的packages赋予了一个新的抽象概念: module,并启用一个新的文件go.mod记录module的元信息。

不过一个repo对应一个module这种说法其实并不精确也并不正确,一个repo当然可以拥有多个module,很多公司或组织是喜欢用monorepo的,这样势必有在单一的monorepo建立多个module的需求,显然go modules也是支持这种情况的。

GOPATH是Go最初设计的产物,在Go语言快速发展的今天,人们日益发现GOPATH似乎不那么重要了,尤其是在引入vendor以及诸多包管理工具后。并且GOPATH的设置还会让Go语言新手感到些许困惑,提高了入门的门槛。Go core team也一直在寻求“去GOPATH”的方案,当然这一过程是循序渐进的。Go 1.8版本中,如果开发者没有显式设置GOPATH,Go会赋予GOPATH一个默认值(在linux上为$HOME/go)。虽说不用再设置GOPATH,但GOPATH还是事实存在的,它在go toolchain中依旧发挥着至关重要的作用。

Go module的引入在Go 1.8版本上更进了一步,它引入了一种新的依赖管理mode:“module-aware mode”。在该mode下,某源码树(通常是一个repo)的顶层目录下会放置一个go.mod文件,每个go.mod文件定义了一个module,而放置go.mod文件的目录被称为module root目录(通常对应一个repo的root目录,但不是必须的)。module root目录以及其子目录下的所有Go package均归属于该module,除了那些自身包含go.mod文件的子目录。

在“module-aware mode”下,go编译器将不再在GOPATH下面以及vendor下面搜索目标程序依赖的第三方Go packages。

Go modules机制在go 1.11中是experiment feature,按照Go的惯例,在新的experiment feature首次加入时,都会有一个特性开关,go modules也不例外,GO111MODULE这个临时的环境变量就是go module特性的experiment开关。GO111MODULE有三个值:auto、on和off,默认值为auto。GO111MODULE的值会直接影响Go compiler的“依赖管理”模式的选择(是GOPATH mode还是module-aware mode),我们详细来看一下:

  • 当GO111MODULE的值为off时,go modules experiment feature关闭,go compiler显然会始终使用GOPATH mode,即无论要构建的源码目录是否在GOPATH路径下,go compiler都会在传统的GOPATH和vendor目录(仅支持在gopath目录下的package)下搜索目标程序依赖的go package;
  • 当GO111MODULE的值为on时(export GO111MODULE=on),go modules experiment feature始终开启,与off相反,go compiler会始终使用module-aware mode,即无论要构建的源码目录是否在GOPATH路径下,go compiler都不会在传统的GOPATH和vendor目录下搜索目标程序依赖的go package,而是在go mod命令的缓存目录($GOPATH/pkg/mod)下搜索对应版本的依赖package;
  • 当GO111MODULE的值为auto时(不显式设置即为auto),也就是我们在上面的例子中所展现的那样:使用GOPATH mode还是module-aware mode,取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下,且包含go.mod文件(两个条件缺一不可),那么使用module-aware mode;否则使用传统的GOPATH mode。

实践

介绍完理论知识,接下来我们进行实践新建一个项目,假设我的项目名为ponycool
设置GO111MODULE环境变量

创建项目

ps:当GO111MODULE的值为auto时,构建的源码目录不在以GOPATH/src为根的目录体系下,且包含go.mod文件(两个条件缺一不可)

在使用Go Module之前,我们先使用go help mod 看下Go Module有哪些命令

进入项目根目录,初始化

系统在项目根目录下生成了一个go.mod的文件

module ponycool

创建文件main.go并写入

import "github.com/micro/go-micro"

func main()  {
service := micro.NewService(
    micro.Name("go.micro.api.example"),
)

service.Init()
}

执行go build,进行构建

我们看到go compiler并没有去使用之前已经下载到GOPATH下的包,而是主动下载了这个包并成功编译。我们看看执行go build后go.mod文件的内容:

同时还生成go.sum文件,go命令和go.mod一起维护一个名为go.sum的文件。

其中包含特定模块版本内容的预期加密校验和。每次使用依赖项时,如果缺少,则将其校验和添加到go.sum,或者需要匹配go.sum中的现有条目。

go命令维护下载包的缓存,并在下载时计算并记录每个包的加密校验和。在正常操作中,go命令会针对主模块的go.sum文件检查这些预先计算的校验和,而不是在每次命令调用时重新计算它们。'go mod verify'命令检查模块下载的缓存副本是否仍然匹配记录的校验和和go.sum中的条目。

同时在GOPATH\pkg目录下会有一个mod目录,我们看到mod下的结构是经过精心设计的。cache/download下面存储了每个module的“元信息”以及每个module不同version的zip包。

在国内的小伙伴因为某些客观存在原因,并不是所有包都能下载到。这时我们就需要使用go module 的replace功能了,
replace顾名思义,就是用新的package去替换另一个package,他们可以是不同的package,也可以是同一个package的不同版本。看一下基本的语法:

go mod edit -replace=old[@v]=new[@v]

old是要被替换的package,new就是用于替换的package。
这里有几点要注意:

  • replace应该在引入新的依赖后立即执行,以免go tools自动更新mod文件时使用了old package导致可能的失败
  • package后面的version不可省略。(edit所有操作都需要版本tag)
  • version不能是master或者latest,这两者go get可用,但是go mod edit不可识别,会报错。(不知道是不是bug,虽然文档里表示可以这么用,希望go1.12能做点完善措施)

下面来演示下如果通过replace替换无法下载的包

可以看到golang.org/x/crypto/ed25519这个包被墙无法获取
先通过go get 获取它的镜像

我们可以看到它的版本号为:v0.0.0-20190103213133-ff983b9c42bc,有版本号我们就能replace了

接下来查看go.mod,发现多了一行代码

現在我們只要go mod tidy或者go build,我們的代碼就可以使用new-package了

声明:初心|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 初探Go Module


愿你勿忘初心,并从一而终