Golang 依赖管理
概述
Golang发展至今,其依赖管理方式有了几次变革。本文对Golang的不同依赖管理方式进行总结。
- GOPATH (最早的依赖管理方式)
- Go vendor (v1.5)
- Go Modules (v1.11至今,默认开启,适用于生产环境)
GOPATH (了解即可)
最早Go通过GOPATH环境变量进行依赖管理。GOPATH是一个环境变量,存储着Go的工作目录(项目目录)。在该目录下,通常包含src
、pkg
和bin
三个子目录,其中src存放项目源代码,pkg存放编译后的包对象文件,bin存放编译后生成的可执行文件。
Go 编译器在编译项目时,会在 GOPATH 下的src目录及其子目录中查找所需的依赖包。当多个 Go 项目需要使用相同的代码包时,可以将这些代码包统一放在 GOPATH 下的src目录中,实现代码的共享和复用。GOPATH模式下,执行go get
命令下载依赖也是将依赖保存在GOPATH/src
下。
如果GOPATH存在多个路径地址,go get会将依赖下载到第一个路径的src目录中
所以GOPATH模式下的依赖管理的缺点很明显:
go get
会将依赖下载到src
目录下,本地代码与第三方代码混合管理,使项目文件混乱- 当GOPATH使用多个路径地址管理不同项目,即使依赖相同的包也得分开保存在各自的
src
目录中,造成磁盘浪费
Go vendor (Gov1.5-v1.10,了解即可)
在Go v1.5时,Golang引入了vendor模式,即将项目的所有外部依赖包保存在项目的vendor
目录中。Go v1.5-v1.10,即还没GOMODULE模式时,通常会使用第三方工具 govendor 来管理 vendor 目录。如:
1 | 初始化项目根目录下的vendor目录 |
想启用 vendor 模式,需要设置 GO15VENDOREXPERIMENT 环境变量为 1(较新版本的Go中已经不存在该变量)。然后使用正常的 go build 命令进行构建,此时 Go 编译器会优先从 vendor 目录中查找依赖包而不是 GOPATH/src 中。
新版本Go中在Go Modules模式下,打包程序时想加入vendor目录中的依赖,只需要使用命令
go build -mod=vendor
即可
Go Module
在Gov1.11时,Golang引入了Go Module模式:
- 将
go get
下载的依赖统一保存在GOPATH/pkg/mod
目录下 - 在项目的根目录使用
go.mod
文件管理项目的依赖以及其版本 - 在项目的根目录使用
go.sum
文件保存每个依赖包的哈希值(包括依赖包的直接依赖和间接依赖),用于验证从远程仓库下载的依赖包是否与之前使用的是同一版本
Go Modules是目前Go使用的最广泛的依赖管理方式。作用有:
- 版本控制与依赖管理
- 精确版本控制:Go Module 可以明确指定项目所依赖的各个模块的版本号,确保项目在不同环境下使用的是相同版本的依赖
- 依赖自动解析:在构建项目时,Go Module 会自动解析项目的依赖关系,并下载所需的依赖模块及其所有间接依赖
- 避免依赖冲突:当多个依赖模块依赖于同一个模块的不同版本时,Go Module 会尝试解决版本冲突,选择一个能够满足所有依赖的版本。
- 项目隔离与可重复性
- 环境隔离:Go Module 为每个项目提供了独立的依赖环境,使得项目的构建不依赖于全局的GOPATH设置。
- 可重复性构建:由于 Go Module 精确地记录了项目的依赖版本,只要go.mod文件和相关的依赖文件不变,项目在任何环境下都可以进行相同的构建,保证了项目构建的可重复性
- 模块发布与共享
- Go Module 简化了模块的发布过程。开发者可以将自己编写的模块发布到公共的模块仓库(如proxy.golang.org),供其他开发者使用。发布时,只需要按照 Go Module 的规范设置好模块的版本号和依赖关系,就可以轻松地将模块分享给其他人。
Go Module开启设置
Golang通过环境变量GO111MODULE
来控制Go Module模式的开启(默认开启),总共有三个可选的值:
- auto: go自主根据当前项目目录进行判断是否使用Go Module模式。当目录满足以下条件时使用Go Module模式:
- 不在
GOPATH/src
下 - 当前或上一级目录中存在
go.mod
文件
- 不在
- on:开启Go Module
- off:使用GOPATH模式
go mod 命令
Golang提供了一系列go mod
命令进行依赖管理,常用的有:
命令 | 作用 |
---|---|
go mod init <module_name> | 在当前目录下生成go.mod文件,并设置包名为 |
go mod tidy | 会扫描当前项目所有源文件中导入语句,更新go.mod和go.sum:添加未声明的依赖,移除未使用的依赖 |
go mod download | 根据项目的 go.mod 文件中列出的依赖信息,将所需的依赖包及其相应版本下载到本地的模块缓存(GOPATH/pkg/mod)中 |
go mod verify | 校验模块是否被篡改过 |
go mod vendor | 将依赖的模块保存在项目的vendor目录下 |
go mod why <module_name> | 输出项目中引入某个包的原因 |
go mod graph | 输出项目中的依赖列表 |
go.mod格式
go.mod文件的格式如下:
1 | // 双斜杠表示注释 |
外部包引用规则
引用前需要使用go get
命令将外部包保存在本地缓存中。
无论是哪种模式下,直接import "包名"
就可以实现外部包的引用。如:
import "github.com/example/package"
本地包引用规则
- Golang 不允许在同一个目录下有两个不同的包(子目录除外)
- Golang 建议包名与包所在的目录名一致
如:1
2
3
4
5
6
7
8
9.
|-demo
| |-foo
| | |-foo.go // package foo
| |-bar.go //package demo
| |-baz.go //package demo
|-main.go
|-go.mod // module test
...
上述目录结构是没有错误的;如果baz.go的包名改成demo2,就会产生编译冲突,因为 Go 编译器会将一个目录下的所有 .go 文件作为一个整体进行编译和链接操作,bar.go和baz.go的包名不同就会造成编译冲突。
在不同依赖管理模式下,包的引用方式也略有不同:
- GOPATH模式下,可以通过
GOPATH/src
的相对路径导入本地包(了解即可) - Go Module模式下,通过模块名+目录路径导入本地包(重点)
以Go Module模式为例,在main.go
中可以通过import "test/demo"
引用demo
包(bar.go
和baz.go
),通过import "test/demo/foo
来引用foo
包(foo.go
)
以下是一个正确的本地包引入例子:
1 | // demo/bar.go |
Golang 允许包名与目录名不一致,但可能会导致代码混淆,因为导入语句使用到的是目录名,而引用该包时使用的是包名,以下是一个正确的例子:
1 | // demo/bar.go |
有趣的是,笔者在写这篇博客时发现,即使包名与目录名不一致,也可以使用别名导入的方式避免混淆(当然保持包名与目录名一致仍是最佳实践),以下是一个正确的例子:
1 | // demo/bar.go |