模块管理

寒江蓑笠翁大约 22 分钟

模块管理

模块的集合通常叫做库library,在大部分情况下对于一些已有的工具和轮子,没有必要自己再去编写一个,一个库通常由很多包组成,通过导入对应的包,就能直接使用该包下提供的任何功能。上述说的库通常分为:

  • 标准库:标准库是由Go官方开发并维护的,提供了许多的功能强大且实用的包,例如在入门示例中就用到了fmt包下的Println函数进行字符串输出。
  • 第三方库:第三方库是由社区开发并维护的,同样也有许多出色的工具,例如著名的web框架gin

而Go Module是官方的一个依赖管理工具,可以前往Go-模块手册open in new window,以了解更多细节。

提示

由于Go Path已经过时,应该被弃用,官方仅仅只是为了兼容性考虑才保留了Go Path,在依赖管理这部分不再讲解关于它的任何内容。


简介

每一个语言对于依赖管理都独有一套解决方案,而Go在远古时期由于谷歌内部项目结构的原因,或许根本就没认真考虑过依赖管理这个问题,导致项目管理起来十分的杂乱,直到1.11版本官方才终于推出了正式的解决方案:Go mod

官方对于模块module的定义为被版本标记的package集合组成的一个单元。通常情况下,大致关系如下:

  • 一个项目可能有一个或多个模块(大多数都是单模块,多模块项目需要使用工作空间)
  • 每个模块拥有一个或多个包
  • 每个包拥有一个或多个源文件

版本规范遵循格式:v(major主版本).(minor小版本).(patch补丁版本),例如v1.0.0。模块管理主要依赖于一个名为go.mod的文件,用于记录项目中的依赖和版本。


env中的GO111MODULE参数值修改为auto或者on以开启模块功能,或者执行以下命令

go env -w GO111MODULE=on

代理

有些依赖仓库是在国外,由于不可明说的原因,无法直接访问,因此也无法下载到本地,默认的官方的模块代理大概率国内是没法访问的,这个时候就需要修改Go代理,下面列出国内几家做的不错的代理:

推荐使用第一个,修改也是十分简单。

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

寻找

对于标准库而言,可以直接前往Go的安装目录下的src文件夹,该目录下即是标准库的源代码,又或者是前往Go-标准库文档open in new window查询包名和具体用法。

对于第三方库而言,可以前往Go 依赖open in new window直接搜索想要寻找的依赖名,例如搜索gin,如下图。

可以得知依赖名为github.com/gin-gonic/gin,就可以使用go get命令来进行下载,同时也可以指定版本,例如github.com/gin-gonic/gin@v1.8.2 。事实上,只要是Github上开源的Go项目可以获取。


下载

下载依赖的命令总共有两个,go getgo install,但两者的使用有点区别。


go get

格式如下

go get [-t] [-u] [-x] [build flags] [packages]

-t:构建测试所需要的模块,这些模块是在命令行指定的packages

-u:更新指定的模块,当这些模块有新的镜像或者发行补丁版本。

-x:打印过程中执行的命令,通常用于调试。

先来看几个例子

go get github.com/gin-gonic/gin@latest

go get golang.org/x/text@master

go get golang.org/x/text@v0.3.2

删除一个依赖

go get github.com/gin-gonic/gin@none

go get命令专门用于调整和修改go.mod文件中的依赖,如果没有特殊需求,用go get就足够了。


go install

格式如下

go install [build flags] [packages]

该命令会下载远程的包并编译成二进制文件放在$GOROOT/bin或者$GOBIN目录下,使用的话分几个情况:

如果指定版本时,会自动忽略目录下的go.mod文件,如果在模块外部执行该命令则必须要指定版本,例如:

go install golang.org/x/tools/gopls@latest

执行后,Go会将远程下载该项目并编译成二进制可执行文件,然后放在$GOBIN对应的目录下。

在模块内部使用该命令可以不用指定版本,但是只能指定模块内已有的包。

使用

笔者建议,先学会怎么使用依赖管理工具,再去研究到底是怎么一回事,这样会更容易去理解,先来看看大致的流程。


创建项目

项目也就是一个文件夹,创建一个文件夹,命名为learn

初始化

在项目内,使用命令go mod init learn初始化项目模块,learn也就是模块名,建议不要随便写,最好与项目名相同,这时会项目下会自动生成一个名为go.mod的文件,这个文件的作用先按下不表,后面会讲,此时文件内容如下。

module learn // 模块名

go 1.19 // 使用Go的版本

下载依赖

使用命令go get github.com/gin-gonic/gin@latest,下载依赖,成功后项目内会出现一个名为go.sum的文件,此时go.mod文件内容如下

module learn

go 1.19

require (
   github.com/gin-contrib/sse v0.1.0 // indirect
   github.com/gin-gonic/gin v1.8.2 // indirect
   github.com/go-playground/locales v0.14.0 // indirect
   github.com/go-playground/universal-translator v0.18.0 // indirect
   github.com/go-playground/validator/v10 v10.11.1 // indirect
   github.com/goccy/go-json v0.9.11 // indirect
   github.com/json-iterator/go v1.1.12 // indirect
   github.com/leodido/go-urn v1.2.1 // indirect
   github.com/mattn/go-isatty v0.0.16 // indirect
   github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
   github.com/modern-go/reflect2 v1.0.2 // indirect
   github.com/pelletier/go-toml/v2 v2.0.6 // indirect
   github.com/ugorji/go/codec v1.2.7 // indirect
   golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
   golang.org/x/net v0.4.0 // indirect
   golang.org/x/sys v0.3.0 // indirect
   golang.org/x/text v0.5.0 // indirect
   google.golang.org/protobuf v1.28.1 // indirect
   gopkg.in/yaml.v2 v2.4.0 // indirect
)

go.sum文件的内容就太多了,只列出一部分,后面会讲有什么用。

github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
......

导入并运行

创建main.go文件,导入gin,然后编译并运行。

package main

import "github.com/gin-gonic/gin" // 导入包

func main() {
    // 运行
	gin.Default().Run(":8080")
}

在项目内执行命令

go run learn

看到输出如下说明运行成功

[GIN-debug] Listening and serving HTTP on :8080

以上步骤完成后,大致概括一下就是从远程仓库下载了一个第三方编写的Web框架,然后在本地导入包,并运行了一个监听端口为8080的Http服务器,日后不管下载任何依赖,都是这一个步骤。


go.mod

可以前往Go Modules - go.mod fileopen in new window以了解更多细节。

module learn

go 1.19

require github.com/gin-gonic/gin v1.8.2

require (
   github.com/gin-contrib/sse v0.1.0 // indirect
   github.com/go-playground/locales v0.14.0 // indirect
   ...
)

一个模块的定义通常是由go.mod文件中的module指定,每一行都包含一个指令,该文件被设计成人类能够读懂和编辑的格式,且Go提供了许多子命令来编辑该文件,通常不建议手动编辑。

module

一个module指令定义了一个主模块的路径,一个go.mod文件必须只包含一个module指令,例如

module learn

require

require指令声明了一个模块依赖项所需要的最小版本。Go命令有时候会自动的加上// indirect注释,即表示间接依赖,这里是因为gin直接依赖了这些包,主模块依赖了gin,所以对于主模块而言,这些模块就是间接依赖,例如

require github.com/gin-gonic/gin v1.8.2

也可以

require golang.org/x/net v1.2.3

require (
    golang.org/x/crypto v1.4.5 // indirect
    golang.org/x/text v1.6.7
)

exclude

exclude指令可以防止Go命令加载该版本的依赖,可能会导致类似go get这样的命令向go.mod文件中添加更高版本的require,并且该指令仅在主模块中应用,在子模块中会被忽略,例子

exclude golang.org/x/net v1.2.3

exclude (
    golang.org/x/crypto v1.4.5
    golang.org/x/text v1.6.7
)

replace

replace将会替换掉指定版本的依赖,可以使用模块路径和版本替换又或者是其他平台指定的文件路径,例子

replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5

replace (
    golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
    golang.org/x/net => example.com/fork/net v1.4.5
    golang.org/x/net v1.2.3 => ./fork/net
    golang.org/x/net => ./fork/net
)

=>左边的版本被替换,其他版本的同一个依赖照样可以正常访问,无论是使用本地路径还是模块路径指定替换,如果替换模块具有 go.mod 文件,则其module指令必须与所替换的模块路径匹配。

retract

retract指令表示,不应该依赖go.mod文件所指定依赖的版本或版本范围。例如在一个新的版本发布后发现了一个重大问题,这个时候就可以使用retract指令。

撤回一些版本

retract (
    v1.0.0 // Published accidentally.
    v1.0.1 // Contains retractions only.
)

撤回版本范围

retract v1.0.0
retract [v1.0.0, v1.9.9]
retract (
    v1.0.0
    [v1.0.0, v1.9.9]
)

go.sum

go.sum文件的存在是为了解决一致性构建的问题。依赖下载到本地后,会缓存到本地,以方便下次构建,下载的依赖和缓存的依赖都会有被纂改的可能,go.sum文件会记录每一个依赖的哈希值,如果go.sum文件中的哈希值与本地依赖的哈希值不同,则会拒绝构建,并且一般在下载依赖完成后还会请求环境变量GOSUMDB所指定的服务器查询一个可信的公共哈希值,如果哈希值不同的话也不会继续执行。可以前往Go Modules - go.sum fileopen in new window以查看更多细节。总的来说,该文件存在的意义就是确保下载的依赖是安全的与远程仓库是内容一致且未被修改的。

提示

大多数情况下,不应该去手动修改go.sum文件。

常用命令

下列命令是模块模块管理会经常用到的

命令说明
go mod download下载当前项目的依赖包
go mod edit编辑go.mod文件
go mod graph输出模块依赖图
go mod init在当前目录初始化go mod
go mod tidy清理项目模块
go mod verify验证项目的依赖合法性
go mod why解释项目哪些地方用到了依赖
go clean -modcache用于删除项目模块依赖缓存
go list -m列出模块

工作区

工作区(workspace),是Go在1.18引入的关于多模块管理的一个新的解决方案。在以往的时候,如果想要在本地依赖其他模块但又没有上传到远程仓库,一般都需要使用replace指令,文件结构如下。

learn
	-main
	|--	main.go
	|--	go.mod
	-tool
	|--	util.go
	|--	go.mod

假如main.go想要导入tool模块下一个函数,则需要将maingo.mod文件修改如下。

module main

go 1.19

require (
   tool v0.0.0
)
replace (
   tool => "../utils" // 使用replace指令指向本地模块
)
package main

import (
   "fmt"
   "tool"
)

func main() {
   fmt.Println(tool.StringMsg())
}

另一种解决办法是将tool模块上传至远程仓库然后发布tag,然后main模块使用go get -u进行更新,而工作区就是为了解决这样的问题而生的。在目录learn下使用命令go work init main tool,就会多出一个名为go.work的文件,内容如下。

go 1.19

use (
   ./main
   ./tool
)

如下操作后,main模块下的go.mod文件便可以不用replace也可以访问tool模块下的函数。


go.work文件有三种指令。

go

go.work文件中必须要有一个有效的Go版本,其指定了要使用的Go工具链版本。

go 1.19

use

use指令用于将本地的模块加入到主模块集合中,它的参数是包含go.mod文件目录的相对路径,但不会添加包含在参数中的子模块。

use ./mymod  // example.com/mymod

use (
    ../othermod
    ./subdir/thirdmod
)

replace

就如同go.mod文件中的replace指令一样,用于替换指定版本的依赖,区别在于,它会覆盖use指令中相同的replace指令,也就是说replace指令的优先级以go.work文件为准,其次才是go.mod文件。

replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5

replace (
    golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
    golang.org/x/net => example.com/fork/net v1.4.5
    golang.org/x/net v1.2.3 => ./fork/net
    golang.org/x/net => ./fork/net
)

顺便提下,go work命令有几个子命令

    edit        编辑go.work文件
    init        初始化工作区
    sync        将工作区构建列表同步至模块
    use         添加模块到工作区中

若想要禁用工作区模式,可以通过 -workfile=off 指令来指定。

go run -workfile=off main.go