依赖管理

包的导入

1. 单行导入与多行导入

在 Go 语言中,一个包可包含多个 .go 文件(这些文件必须得在同一级文件夹中),只要这些 .go 文件的头部都使用 package 关键字声明了同一个包。

导入包主要可分为两种方式:

  • 单行导入
import "fmt"
import "sync"
  • 多行导入
import(
"fmt"
"sync"
)

Go 语言中 导入的包,必须得用双引号包含。

2. 使用别名

在一些场景下,我们可能需要对导入的包进行重新命名,比如

  • 我们导入了两个具有同一包名的包时产生冲突,此时这里为其中一个包定义别名
import (
"crypto/rand"
mrand "math/rand" // 将名称替换为mrand避免冲突
)
  • 我们导入了一个名字很长的包,为了避免后面都写这么长串的包名,可以这样定义别名
import hw "helloworldtestmodule"
  • 防止导入的包名和本地的变量发生冲突,比如 path 这个很常用的变量名和导入的标准包冲突。
import pathpkg "path"

3. 使用点操作

如里在我们程序内部里频繁使用了一个工具包,比如 fmt,那每次使用它的打印函数打印时,都要 包名+方法名。

对于这种使用高频的包,可以在导入的时,就把它定义会 “自己人”(方法是使用一个 . ),自己人的话,不分彼此,它的方法,就是我们的方法。

从此,我们打印再也不用加 fmt 了。

import . "fmt"

func main() {
Println("hello, world")
}

但这种用法,会有一定的隐患,就是导入的包里可能有函数,会和我们自己的函数发生冲突。

4. 包的初始化

每个包都允许有一个 init 函数,当这个包被导入时,会执行该包的这个 init 函数,做一些初始化任务。

对于 init 函数的执行有几点需要注意

  1. init 函数优先于 main 函数执行
  2. 在一个包引用链中,包的初始化是深度优先的。比如,有这样一个包引用关系:main→A→B→C,那么初始化顺序为 C.init→B.init→A.init→main
  3. 同一个包甚至同一个源文件,可以有多个 init 函数
  4. init 函数不能有入参和返回值
  5. init 函数不能被其他函数调用
  6. 同一个包内的多个 init 顺序是不受保证的
  7. 在 init 之前,其实会先初始化包作用域的常量和变量(常量优先于变量),具体可参考如下代码
package main

import "fmt"

func init() {
fmt.Println("init1:", a)
}

func init() {
fmt.Println("init2:", a)
}

var a = 10
const b = 100

func main() {
fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10

5. 包的匿名导入

当我们导入一个包时,如果这个包没有被使用到,在编译时,是会报错的。

但是有些情况下,我们导入一个包,只想执行包里的 init 函数,来运行一些初始化任务,此时怎么办呢?

可以使用匿名导入,用法如下,其中下划线为空白标识符,并不能被访问

// 注册一个PNG decoder
import _ "image/png"

由于导入时,会执行 init 函数,所以编译时,仍然会将这个包编译到可执行文件中。

6. 导入的是路径还是包?

当我们使用 import 导入 testmodule/foo 时,初学者,经常会问,这个 foo 到底是一个包呢,还是只是包所在目录名?

import "testmodule/foo"

结论是:

  • 导入时,是按照目录导入。导入目录后,可以使用这个目录下的所有包。
  • 出于习惯,包名和目录名通常会设置成一样,所以会让你有一种你导入的是包的错觉。

7. 相对导入和绝对导入

据我了解在 Go 1.10 之前,好像是不支持相对导入的,在 Go 1.10 之后才可以。

绝对导入:从 $GOPATH/src$GOROOT 或者 $GOPATH/pkg/mod 目录下搜索包并导入

相对导入:从当前目录中搜索包并开始导入。就像下面这样

import (
"./module1"
"../module2"
"../../module3"
"../module4/module5"
)

使用相对导入的方式,项目可读性会大打折扣,不利用开发者理清整个引用关系。

所以一般更推荐使用绝对引用的方式。

Go Modules

在以前,Go 语言的的包依赖管理一直都被大家所诟病,Go官方也在一直在努力为开发者提供更方便易用的包管理方案,从最初的 GOPATH 到 GO VENDOR,再到最新的 GO Modules,虽然走了不少的弯路,但最终还是拿出了 Go Modules 这样像样的解决方案。

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。

在v1.14中足够成熟,可以用于生产环境,Gomod可以让项目目录不再约束在$GOPATH/src下,此时不同的项目可以使用同一个第三方库的不同版本。在推出gomod之后,基本没必要再使用老的GOPATH模式进行开发。

1. 配置环境变量

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:offonauto,默认值是auto

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。

  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖。

  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

  • 开启:go env -w GO111MODULE=on
  • 查看:go env GO111MODULE

注意:

  1. 没使用go mod时依赖安装在 %GOPATH/src/github.com
  2. 使用go mod后依赖安装在 %GOPATH/pkg/mod 不管是第三方还是Google相关的包

注意:在使用GoLand工具时,不要配置Project GOPATH为当前工程目录,最好不要配置Project GOPATH,而是配置Module GOPATH

  1. 安装govendor : go get -u -v github.com/kardianos/govendor:

$GOPATH/src/vendor/vendor.json, 是从远端库添加依赖包

2. go mod相关命令

常用的go mod命令如下

 1. go mod init [module]
初始化gomod,会在项目目录下生成 go.mod 文件,如: go mod init github/duduniao/application.
如果是v2以上版本,golang建议在项目末尾加上大版本,如: github/duduniao/application/v3

2. go mod tidy [-e] [-v]
更新go.mod文件,移除不必要的模块,添加新的模块。
-e: 在遇到错误的时候尽量继续
-v: 显示移除的模块

3. go mod download [-json] [-x] [modules]
用于下载模块到gomod缓存目录下。
-jons: 以json对象打印下载的模块信息
-x: 默认donwload不会打印标准输出,只打印标准错误,使用-x打印执行过程到标准错误
modules: 指定要下载的模块,可以是模块名@version,如果不指定modules,则下载所有go.mod中依赖

4. go mod edit [flags] [-fmt|-print|-json] [go.mod]
编辑go.mod文件,如添加、修改、删除go.mod内容。
flags: flags 分为以下几种:
• -module: 用于修改模块路径
• -go=version: 修改golang版本
• -require=path@version: 添加require的模块
• -droprequire=path: 移除require的模块
• -replace=old[@v]=new[@v]: 替换模块,当new省略version,仅用于new为相对路径,不能为本项目目录之外的代码库
• -dropreplace=old[@v]: 移除replace中的模块
• -exclude=path@version, -dropexclude=path@version: 添加或移除模块
-fmt: 格式化go.mod文件
-print: 打印go.mod文件,以文本的形式
-josn: 打印go.mod文件,以json格式

5. go mod verify
校验模块的摘要信息

6. go mod why [-m] [-vendor] packages...
打印为什么需要引入这个包

7. go mod vendor [-e] [-v]
将go.mod中用的模块从缓存目录迁移到项目根目录下的 vendor 目录下,这个目录可能会很大。
大部分情况下这个命令是没必要执行的,部分场景中由于CI过程中拉取代码存在问题才会考虑从vendor目录下获取依赖进行编译
当启用了 vendor 目录,编译和运行时指定 -mod=vendor 表示从vendor目录读取依赖而不是缓存中

8. go get [依赖名称]
获取单个依赖
在项目中执行`go get`命令可以下载依赖包,并且还可以指定下载的版本。

运行`go get -u`将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
运行`go get -u=patch`将会升级到最新的修订版本

运行`go get package@version`将会升级到指定的版本号version

如果下载所有依赖可以使用`go mod download`命令。

3. go mod文件

go.mod文件记录了项目所有的依赖信息,其结构大致如下:

module gitee.com/lemon/learn-go

go 1.16

require (
gitee.com/linux_duduniao/apiserver v0.0.0
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/containerd/containerd v1.5.2 // indirect
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f // indirect
google.golang.org/grpc v1.39.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.21.2 // indirect
k8s.io/apimachinery v0.21.2
k8s.io/client-go v0.21.2
k8s.io/klog/v2 v2.9.0 // indirect
k8s.io/utils v0.0.0-20210709001253-0e1f9d693477 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
)

replace (
gitee.com/linux_duduniao/apiserver => ./src/apiserver
)

excule (
k8s.io/client-go v0.21.0
)

其中:

以上的 go.mod 文件中,line 1 表示当前工程项目的模块名称; line 3 表示当前使用的 go mod init 时使用的go语言版本。require 代码块表示当前项目依赖的模块。

  • module用来定义包名

  • require用来定义依赖包及版本
    版本号格式为: v{major}.{minor}.{patch} ,一般而言,当 major 为0或者 patch 带有 pre, beta 时,表示版本不稳定。上述的 line 11、line 14 是伪版本号,会增加时间戳和修订标识符。

  • indirect表示间接引用
    ndirect 表示间接依赖,当项目A引用B,B引用C1和C2时:如果B没有启用go.mod,则C1,C2作为间接引用出现在A的go.mod文件中。当B启用了go.mod,但是C2不在其go.mod中时,C2作为间接引用出现在 A 的go.mod中。使用 go mod why -m pkg 可以显示为啥依赖该模块。

  • replace替换成本地库(如自己的库,或者修改源代码的库)或者github对应的库(如翻墙失败)
    replace 用于替换require中的依赖,在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。

    一般有以下三种情况会使用到:

    • 替换无法下载的包,如使用 github 中的包替换其它 URL 地址或版本的包
    • 替换本地自己的包,如 line 33
    • 替换fork的包,部分场景中需要对第三方包做修改且作为公共包对外时,会考虑fork一个到自己的仓库
  • exclude
    用于排除指定版本的包,在实际的工程环境中很少使用!

4. go sum文件

go.sum 用于记录各个依赖包版本的摘要,如果该依赖有 go.sum 还会记录 go.sum 的摘要。目的是用于防止依赖包被篡改,因为第三方模块的tag可能会被篡改,导致项目出现风险。go.sum 记录的依赖版本可能会比go.sum中使用到的多得多,这个并不会影响项目本地。在提交代码时,需要把 go.sum 和 go.mod 同时提交。当go.sum 合并分支发生冲突时,一般将两个go.sum文件内容合并后保存。

参考感谢
01-环境准备 · 语雀 (yuque.com)
3.2 依赖管理:超详细解读 Go Modules 应用 — Go编程时光 1.0.0 documentation (iswbm.com)
go mod · 语雀 (yuque.com)