本节我们根据实际的例子来演示go module的功能。在开始之前,我希望读者思考如下问题,并带着这些问题阅读下面的内容或者亲自动手实践以便加深认识。

Go module到底是做什么的?

我们在前面的章节已介绍过,但还是想强调一下,Go module实际上只是精准的记录项目的依赖情况,包括每个依赖的精确版本号,仅此而矣。

那么,为什么需要记录这些依赖情况,或者记录这些依赖有什么好处呢?

试想一下,在编译某个项目时,第三方包的版本往往是可以替换的,如果不能精确的控制所使用的第三方包的版本,最终构建出的可执行文件从本质上是不同的,这会给问题诊断带来极大的困扰。

接下来,我们从一个Hello World项目开始,逐步介绍如何初始化module、如何记录依赖的版本信息。
项目托管在GitHub https://github.com/renhongcai/gomodule中,并使用版本号区别使用go module的阶段。

  • v1.0.0 未引用任何第三方包,也未使用go module
  • v1.1.0 未引用任何第三方包,已开始使用go module,但没有任何外部依赖
  • v1.2.0 引用了第三方包,并更新了项目依赖

需要注意的是,下面的例子统一使用go 1.13版本,如果你使用go 1.11 或者go 1.12,运行效果可能略有不同。
本文最后部分我们尽量尝试记录一些版本间的差异,以供参考。

Hello World

在v1.0.0版本时,项目只包含一个main.go文件,只是一个简单的字符串打印:

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

此时,项目还没有引用任何第三方包,也未使用go module。

初始化module

一个项目若要使用Go module,那么其本身需要先成为一个module,也即需要一个module名字。

在Go module机制下,项目的module名字以及其依赖信息记录在一个名为go.mod的文件中,该文件可以手动创建,也可以使用go mod init命令自动生成。推荐自动生成的方法,如下所示:

[root@ecs-d8b6 gomodule]# go mod init github.com/renhongcai/gomodule
go: creating new go.mod: module github.com/renhongcai/gomodule

完整的go mod init命令格式为go mod init [module]:其中[module]为module名字,如果不填,go mod init会尝试从版本控制系统或import的注释中猜测一个。这里推荐指定明确的module名字,因为猜测有时需要一些额外的条件,比如 Go 1.13版本,只有项目位于GOPATH中才可以正确运行,而 Go 1.11版本则没有此要求。

上面的命令会自动创建一个go.mod文件,其中包括module名字,以及我们所使用的Go 版本:

[root@ecs-d8b6 gomodule]# cat go.mod 
module github.com/renhongcai/gomodule

go 1.13

go.mod文件中的版本号go 1.13是在Go 1.12引入的,意思是开发此项目的Go语言版本,并不是编译该项目所限制的Go语言版本,但是如果项目中使用了Go 1.13的新特性,而你使用Go 1.11编译的话,编译失败时,编译器会提示你版本不匹配。

由于我们的项目还没有使用任何第三方包,所以go.mod中并没有记录依赖包的任何信息。我们把自动生成的go.mod提交,然后我们尝试引用一个第三方包。

管理依赖

现在我们准备引用一个第三方包github.com/google/uuid来生成一个UUID,这样就会产生一个依赖,代码如下:

package main

import (
    "fmt"

    "github.com/google/uuid"
)

func main() {
    id := uuid.New().String()
    fmt.Println("UUID: ", id)
}

在开始编译以前,我们先使用go get来分析依赖情况,并会自动下载依赖:

[root@ecs-d8b6 gomodule]# go get 
go: finding github.com/google/uuid v1.1.1
go: downloading github.com/google/uuid v1.1.1
go: extracting github.com/google/uuid v1.1.1

从输出内容来看,go get帮我们定位到可以使用github.com/google/uuid的v1.1.1版本,并下载再解压它们。

注意:go get总是获取依赖的最新版本,如果github.com/google/uuid发布了新的版本,输出的版本信息会相应的变化。关于Go Module机制中版本选择我们将在后续的章节详细介绍。

go get命令会自动修改go.mod文件:

[root@ecs-d8b6 gomodule]# cat go.mod 
module github.com/renhongcai/gomodule

go 1.13

require github.com/google/uuid v1.1.1

可以看到,现在go.mod中增加了require github.com/google/uuid v1.1.1内容,表示当前项目依赖github.com/google/uuidv1.1.1版本,这就是我们所说的go.mod记录的依赖信息。

由于这是当前项目第一次引用外部依赖,go get命令还会生成一个go.sum文件,记录依赖包的hash值:

[root@ecs-d8b6 gomodule]# cat go.sum 
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

该文件通过记录每个依赖包的hash值,来确保依赖包没有被篡改。关于此部分内容我们在此暂不展开介绍,留待后面的章节详细介绍。

go get修改的go.mod和创建的go.sum都需要提交到代码库,这样别人获取到项目代码,编译时就会使用项目所要求的依赖版本。

至此,项目已经有一个依赖包,并且可以编译执行了,每次运行都会生成一个独一无二的UUID:

[root@ecs-d8b6 gomodule]# go run main.go
UUID:  20047f5a-1a2a-4c00-bfcd-66af6c67bdfb

注:如果你没有使用go get在执行之前下载依赖,而是直接使用go build main.go运行项目的话,依赖包也会被自动下载。但是在v1.13.4中有个bug,即此时生成的go.mod中显示的依赖信息则会是require github.com/google/uuid v1.1.1 // indirect,注意行末的indirect表示间接依赖,这明显是错误的,因为我们直接import的。

版本间差异

由于Go module在Go v1.11初次引入,历经Go v1.12、v1.13的发展,其实现细节上已有了一些变化,按照之前的规划Go module将会在v1.14定型,推荐尽可能使用最新版本,否则可能会产生一些困扰。

比如,在v1.11中使用go mod init初始化项目时,不填写module名称是没有问题,但在v1.13中,如果项目不在GOPATH目录中,则必须填写module名称。

后记

本节,我们通过简单的示例介绍了如何初始化module以及如何添加新的依赖,还有更多的内容没有展开。比如go.mod文件中除了 modulerequire指令外还有replaceexclude指令,再比如go get下载的依赖包如何存储的,以及go.sum如何保证依赖包未被篡改的,这些内容我们留待后面的章节一一介绍。

赠人玫瑰手留余香,如果觉得不错请给个赞~

本篇文章已归档到GitHub项目,求星~ 点我即达

文档更新时间: 2020-08-08 22:03   作者:kuteng