7

Go语言重新开始,Go Modules 的前世今生与基本使用

 2 years ago
source link: https://my.oschina.net/qcloudcommunity/blog/5301196
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go语言重新开始,Go Modules 的前世今生与基本使用

导语 | 随着Go语言发展与场景变化, GOPATH 引起的编译依赖、内部自签发证书、安全审计等问题相继出现,随着官方推出的Go Modules逐渐完善,用户有了新的选择。本文将会带领大家从0开始,认识并使用Go Modules。

本文作者:李保坤,腾讯云高级开发工程师、InfluxDB/OpenCensus Contributor、Golang 代码贡献者。

1620

2020 年腾讯内部的一份开发者报告显示,Go 语言已经成为腾讯内部第二大后端开发语言,在腾讯每天有大量的 Go 开发者在做业务和平台开发,大量的团队和项目使用也暴露出一些问题,随着 Go Modules 的出现,类似于内部自签发证书、安全审计等这些问题也逐渐得到解决。

笔者在腾讯当前负责腾讯云在 Go 编程语言使用上的一些问题, 2021年初开始负责内部 goproxy 服务并推广Go Modules使用,这些技术支撑了腾讯云、微信、腾讯视频、腾讯游戏、腾讯音乐、腾讯会议等明星产品,并与公司内部软件源团队、工蜂团队、TRPC 团队以及各个 CI 团队达成密切的合作。在本系列文章中,笔者就将由浅入深帮助大家开始学习和了解 Go Modules。

Golang 开发的模式演进

从 Go 出生开始,用户就一直在使用 GOPATH 这个环境变量,随着 Go 语言的快速发展和不断壮大,由 GOPATH 引起的编译依赖问题也开始逐渐出现。终于在2019 年, Golang 迎来 10 周年之际,Google Go 团队终于开始把目光投向了这一伴随了 Golang 十年的环境变量。

GOPATH的使用

目前在 Go 中存在两种开发模式,GOPATH mode 和 Go modules mode。

在 Go modules 之前,Go 开发中的依赖管理使用 GOPATH 开发模式。在 GOPATH 开发模式中,Go 命令使用 GOPATH 环境变量来实现以下几个功能:

  1. go install 命令安装二进制库到 GOBIN,其默认路径为GOBIN,其默认路径为GOPATH/bin。
  2. go install 命令安装编译好的包到 GOPATH/pkg/中,例如将example.com/y/z安装到GOPATH/pkg/中,例如将example.com/y/z安装到GOPATH/pkg/example.com/y/z.a。
  3. go get 命令下载源码包到 GOPATH/src/中,例如将example.com/y/z下载到GOPATH/src/中,例如将example.com/y/z下载到GOPATH/src/example。

Go modules的发展历程

GOPATH mode 开发模式是终将要被淘汰的,Go 官方在整个 Go 开发生态系统中添加 package version 这一概念,进而引入 Go modules 开发模式。从 GOPATH mode 开发模式变换到 Go modules 是一个漫长的过程,它已经经历了数个 Go 的发行版本:

  • Go 1.11 (2018 年 8 月) 引入了 GO111MODULE 环境变量,其默认值为 auto。如果设置该变量 GO111MODULE=off, 那么 go 命令将始终使用 GOPATH mode 开发模式。如果设置该变量 GO111MODULE=on,go 命令将始终使用 Go modules 开发模式。如果设置该变量 GO111MODULE=auto (或者不设置),go 命令行会根据当前工作目录来决定使用哪种模式,如果当前目录在 GOPATH/src以外,并且在根目录下存在go.mod文件,那么go命令会启用Gomodule模式,否则使用GOPATH开发模式。这个规则保证了所有在GOPATH/src以外,并且在根目录下存在go.mod文件,那么go命令会启用Gomodule模式,否则使用GOPATH开发模式。这个规则保证了所有在GOPATH/src 中使用 auto 值时原有编译不受影响,并且还可以在其他目录中来体验最新的 Go module 开发模式。
  • Go 1.13 (2019 年 8 月) 调整了 GO111MODULE=auto 模式中对 GOPATH/src的限制,如果一个代码库在GOPATH/src的限制,如果一个代码库在GOPATH/src 中,并且有 go.mod 文件的存在, go 命令会启用 module 开发模式。这允许用户继续在基于导入的层次结构中组织他们的检出代码,但使用模块进行个别仓库的导入。
  • Go 1.16 (2021 年 2 月) 会将 GO111MODULE=on 做为默认值,默认启用 go module 开发模式,也就是说,默认情况下 GOPATH 开发模式将被彻底关闭。如果用户需要使用 GOPATH 开发模式可以指定环境变量 GO111MODULE=auto 或者 GO111MODULE=off。
  • Go 1.NN (???) 将会废弃 GO111MODULE 环境变量和 GOPATH 开发模式,默认完全使用 module 开发模式。

GOPATH 与Go modules的相爱想杀

针对大家关心的几个问题,笔者对此作出如下回答:

Q1:GOPATH 变量会被移除吗?

A:不会,GOPATH 变量不会被移除。未来废弃 GOPATH 开发模式并不是指删除 GOPATH 环境变量,它会继续保留,主要作用如下:

  • go install 命令安装二进制到 GOBIN目录,其默认位置为GOBIN目录,其默认位置为GOPATH/bin。
  • go get 命令缓存下载的 modules 到 GOMODCACHE目录,默认位置为GOMODCACHE目录,默认位置为GOPATH/pkg/mod。
  • go get 命令缓存下载的 checksum 数据到 $GOPATH/pkg/sumdb 目录。

Q2:我还可以继续在 `GOPATH/src/import/path` 中创建代码库吗?

A:可以,很多开发者以这样的文件结构来组织自己的仓库,你只需要在自己创建的仓库中添加 go.mod 文件。

Q3: 如果我想测试修改一个我需要的依赖库,我改怎么做?

A:如果你编译自己的项目时依赖了一些未发布的变更,你可以使用 go.mod 的 replace来实现你的需求。

举个例子,如果你已经将 golang.org/x/website 和 golang.org/x/tools 下载到 GOPATH/src/目录下,那么你可以在GOPATH/src/目录下,那么你可以在GOPATH/src/golang.org/x/website/go.mod 中添加下面的指令来完成替换:

replace golang.org/x/tools => $GOPATH/src/golang.org/x/tools

当然,replace 指令是不感知 GOPATH 的,将代码下载到其他目录也一样可以。

从0开始使用 Go Modules

1. 创建一个新的 Go module

首先创建一个新目录 /home/gopher/hello,然后进入到这个目录中,接着创建一个新文件, hello.go:

package hello

func Hello() string {

return "Hello, world."

}

然后再写个对应的测试文件 hello_test.go:

package hello

import "testing"

func TestHello(t *testing.T) {

want := "Hello, world."

if got := Hello(); got != want {

t.Errorf("Hello() = %q, want %q", got, want)

}

}

现在我们拥有了一个 package,但它还不是一个 module,因为还没有创建 go.mod 文件。如果在 /home/gopher/hello 目录中执行 go test,则可以看到:

$ go test

go: go.mod file not found in current directory or any parent directory; see 'go help modules'

可以看到 Go 命令行提示没有找到 go.mod 文件,可以参考 go help modules。这样的话可以使用 Go mod init 来初始化一下,然后再执行 Go test:

$ go mod init example.com/hello

go: creating new go.mod: module example.com/hello

go: to add module requirements and sums:

go mod tidy

$go mod tidygo

$ go test

PASS

ok example.com/hello 0.020s

$

这样的话,module 测试就完成了。

然后执行的 go mod init 命令创建了一个 go.mod 文件:

$ cat go.mod

module example.com/hello

go 1.17

2. 给 module 添加依赖

Go modules 的主要亮点在于在编程时使用别人写的代码,即引入一个依赖库时能有非常好的体验。首先更新一下 hello.go,引入 rsc.io/quote 来实现一些新的功能。

package hello

import "rsc.io/quote"

func Hello() string {

return quote.Hello()

}

然后,再测试一下:

$ go test

hello.go:3:8: no required module provides package rsc.io/quote; to add it:

go get rsc.io/quote

$ go get rsc.io/quote

go: downloading rsc.io/quote v1.5.2

go: downloading rsc.io/sampler v1.3.0

go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

go: added golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

go: added rsc.io/quote v1.5.2

go: added rsc.io/sampler v1.3.0

$ go test

PASS

ok example.com/hello 1.401s

从 Go 1.7开始,Go modules 开始使用 lazyloading 加载机制,依赖库的更新需要根据提示手动进行相应的更新。

Go 命令会根据 go.mod 的文件来解析拉取指定的依赖版本。如果在 go.mod 中没有找到指定的版本,会提示相应的命令引导用户添加,然后 Go 命令会去解析最新的稳定版本(Latest),并且添加到 go.mod 文件中。 在这个例子中可以看到,第一次执行的 Go test 运行需要 rsc.io/quote 这个依赖,但是在 go.mod 文件中并没有找到,于是引导用户去获取 latest 版本,用户通过 go get rsc.io/quote 获取了最新版本 v1.5.2,而且还另外下载了另外两个 rsc.io/quote 需要的依赖:rsc.io/sampler 和 golang.org/x/text。间接依赖引用也会记录在 go.mod 文件中,使用 indirect 注释进行标记。

$ cat go.mod

module example.com/hello

go 1.17

require (

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect

rsc.io/quote v1.5.2 // indirect

rsc.io/sampler v1.3.0 // indirect

)

再一次运行 go test 命令不会重复上面的工作,因为 go.mod 已经是最新的,并且所需的依赖包已经下载到本机中了(在 $GOPATH/pkg/mod 中):

$ go test

PASS

ok example.com/hello 0.020s

注意,虽然 Go 命令可以快速轻松地添加新的依赖项,但并非没有代价。

如上面所提到,在项目中添加一个直接依赖可能会引入其他间接依赖。go list -m all 命令可以列出当前项目所依赖的所有依赖:

$ go list -m all

example.com/hello

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

rsc.io/quote v1.5.2

rsc.io/sampler v1.3.0

在 go list 的输出结果中,可以看到当前的 module,也被称为 main module 会展示在第一行,然后其他的会按照 module path 进行排序。其中依赖 golang.org/x/text 的版本号 v0.0.0-20170915032832-14c0d48ead0c 是一个伪版本号, 它是 Go 版本的一种,指向了一个没有打 tag 的 commit 上。

另外除了 go.mod 文件,go 命令还维护了一个叫做 go.sum 的文件,这个文件包含了每个版本对应的加密哈希值。

$ cat go.sum

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...

rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...

rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...

rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...

rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...

Go 命令使用 go.sum 来保证每次下载的依赖库代码和第一次都是一致的,进而来保证项目不会出现一些异常情况,所以 go.mod 和 go.sum 都应该上传到 git 等版本控制系统中。

3. 更新依赖

从上面 go list -m all 命令的输出中,可以看到在库 golang.org/x/text 中使用了一个伪版本号。首先把这个版本好更新到最新的稳定版本:

$ go get golang.org/x/text

go: downloading golang.org/x/text v0.3.7

go: upgraded golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c => v0.3.7

$ go test

PASS

ok example.com/hello 0.013s

测试仍然可以通过,然后再执行一次 go list -m all:

$ go list -m all

example.com/hello

golang.org/x/text v0.3.7

rsc.io/quote v1.5.2

rsc.io/sampler v1.3.0

$ cat go.mod

module example.com/hello

go 1.17

require (

golang.org/x/text v0.3.7 // indirect

rsc.io/quote v1.5.2 // indirect

rsc.io/sampler v1.3.0 // indirect

)

结语

Go 通过 Go modules 的依赖管理统一了 Go 生态中众多的第三方的依赖管理,并且高度集成在 Go 命令行中,无需开发者们额外安装使用,目前在当前维护的 Go 版本中都已经支持了 Go modules。还没有切换到 Go modules 的用户,强烈建议大家开始使用它,这无论是从团队开发体验、性能还是安全等方面,都能提供诸多的优质特性和保障。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK