参考
- Go语言基础之包 | 李文周的博客
- Go mod的使用、发布、升级 | wei
- Go Module如何发布v2及以上版本
- 1.2.7. go mod命令 --- 新溪-gordon V1.7.9 文档
- golang go 包管理工具 go mod的详细介绍-腾讯云开发者社区-腾讯云
- Go Mod 常见错误的原因 | walker的博客
项目案例
- oceanweave/testgomod
- 用于发布 go 包,v1 v2 版本等
- oceanweave/testhello: test the repo testgomod
- 用于拉取 testgomod 项目的 go包
- 具体操作详情,可以查看两个项目的 commit 信息
总结
1. 项目初始化
-
go mod init {项目名}
,不推荐随便命名项目名,这样不便于以后发布到 github,若本地使用的话可以随意命名项目名 -
推荐方式:
- 首先在 github 创建个 repo
- 然后初始化
go mod init github.com/{your-username}/{repo-name}1
-
go mod init {项目名}
-
本地创建的项目不上传 github 的话,项目名随便指定
-
若该项目需要上传 github,并需要后期 go get 下载的话,go mod init 一定要与 github 上存储仓库路径一致
-
如本地创建一个 hello 项目,同时 github 上创建一个 hello 仓库(repo),那么该 repo 路径对应的 url 就是(github.com/{你的用户名}/{你的仓库名称})
- 就是确保可以通过此路径找到你的项目
- 若你在你的 repo 下创建一个二级目录,如 hello repo 下创建个 world 目录,那么如果你想把你的项目传到此目录下
- 你的 go mod 就要如下设置 go mod init github.com/{你的用户名}/hello/world
-
你的包如果存放在github上 你的包的go.mod module后面一定是github.com/{username}/xxx不能直接写成 xxx 这样的话 go mod 无法获得包 错误是
parsing go.mod: unexpected module path "xxx" go: error loading module requirements
-
也就是说 go.mod 的module 要跟go get xx/xxx 保持一致
-
sh
# 举例子
# 1. 首先在 github 上创建个 repo,如 testgomod repo
# 2. 本地创建 go 项目,并发布到 github 上
# - 首先本地创建自己项目目录 testgomod --> 对应 github repo 名
# 不要将 test 放到最后,若放在最后,其中的 go 文件,将会视此目录为 测试目录,可能编译的时候会有些问题
mkdir testgomod && cd testgomod
# - 与 github repo 进行同步,github 创建 repo 时就有这些提示
echo "# testgomod" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
# 此处我的用户名是 oceanweave
git remote add origin git@github.com:oceanweave/testgomod.git
git push -u origin main
echo "# gomodtest" >> README.md
# 3. 在上面创建的 hello 目录中 go mod 初始化
go mod init github.com/oceanweave/testgomod
# 4. 创建 pkg 目录用于编写你的函数逻辑,创建 main.go 作为程序主入口
$ tree testgomod
testgomod
├── README.md
└── go.mod
# go.mod 内容
# github.com/oceanweave/hello 就是项目名
module github.com/oceanweave/testgomod
go 1.21.4
2. 程序编写及函数引用
2.1 先把目录结构创建出来
sh
$ tree
.
├── README.md
├── go.mod
├── main.go # 函数总逻辑入口
└── pkg
└── demo1 # 一般来说包名命名和子目录名相同,便于查看
└── hello1.go # 编写函数逻辑
2.2 pkg 内创建工作函数(包名和目录一致)
- 注意:包名要和所在目录名保持一致,否则引用 import 时会混乱
go
// - 此处 package 关键字就是指定包名
// - 子目录内的函数文件,可以随意命名,如 hello1.go,自己识别就好
// - 注意:子目录内可以创建多个函数文件,但同一个子目录的所有文件只能属于同一个包名,就是必须设置相同的 package
// - 如 demo1 子目录,后续创建的文件,必须添加 package demo1
package demo1
import "fmt"
func Hello1() {
fmt.Println("this is Hello-1 from demo1")
}
2.3 main函数引用子目录下的函数(import引用及函数调用)
- 注意:import 引用的是包的路径而不是包名
- 若开启了 go mod,可以用
项目名(module名)+ 包的相对路径表示
(如github.com/oceanweave/testgomod
是项目名,可以理解为是项目的根路径,pkg/demo1
是相对路径,用来指向包),来引用包
- 若开启了 go mod,可以用
- 函数调用的时候,用的是包名,而不是 import 路径名
- 但是为什么一致呢,因为在包的定义是,我们定义 package 名保持和目录名一致,也就是上一步操作
- 保持一致的好处就是,当 demo1 包函数发生错误是,可以通过相同名称在 import 找到对应的项目路径,然后排查并解决错误
go
package main
import (
// 可以理解为是 项目名(go module 名) + 包的相对路径(从项目根目录到包的路径)
"github.com/oceanweave/testgomod/pkg/demo1"
)
func main() {
// 此处 demo1 是包名,而不是包的路径名
// 但因为包所在目录和包名一致,所以可能会误认为,此处是上面 import 路径的末尾
// 这样保持一致的好处就是,当 demo1 包函数发生错误是,可以通过相同名称在 import 找到对应的项目路径,然后排查并解决错误
demo1.Hello1()
}
2.4 包名与所在目录不一致的情况
go
// 包名和所在目录不一致情况,如 /pkg/demo1 目录定义的包名为 demox
package demox
import "fmt"
func Hello1() {
fmt.Println("this is Hello-1 from demo1")
}
// -------------------------------
// 在 main 包应用时的不同,引用方法1
package main
import (
// 包的路径
"github.com/oceanweave/testgomod/pkg/demo1"
)
func main() {
// 包名调用函数
// 可以看出来,当此处发生问题时,当 import 包过多时,无法定位该包对应上面哪个 import 路径
demox.Hello1()
}
// -------------------------------
// 在 main 包应用时的不同,引用方法2,利用 import 重命名
package main
import (
// 有效利用,包的重命名,避免歧义
// 可以看出来,当此处发生问题时,当 import 包过多时,可以利用 import 重命名的名称,定位该包对应上面哪个 import 路径
// 但是这种方法仍不如,包名和所在目录保持一致的情况
demox "github.com/oceanweave/testgomod/pkg/demo1"
)
func main() {
demox.Hello1()
}
2.5 版本发布
- 版本发布机下载
sh
# 1. 首先将本地项目开发完成
cd testgomod
# 2. 然后上传到 github
git push
# 此时是未发布版本情况,但上传到了 github
# 此时创建其他项目,比如另创建个项目 testhello,可以如下形式下载此 testgomod 包,然后引用其中的函数
# - 会拉取最新的 commit-id,就是最新的包
go get -u github.com/oceanweave/testgomod
# - 指定某个 commit-id 进行拉取
go get -u github.com/oceanweave/testgomod@commit-id
# 3. 版本发布
git tag -a 版本号 commit-id -m "注释信息"
git push origin 版本号
# 如
git tag -a v1.0.0 87fdf824 -m "Release v1.0.0"
# 之后发布到 github
git push origin v1.0.0
# 此时是发布版本情况
# 此时创建其他项目,比如另创建个项目 testhello,可以如下形式下载此 testgomod 包,然后引用其中的函数
git get -u github.com/oceanweave/testgomod@版本号
git get -u github.com/oceanweave/testgomod@v1.0.0
-
版本形式要求
- 发布版本,版本形式要求 https://go.dev/ref/mod#versions
- 形式建议 v1.2.3
- 1 主版本号:发布了不兼容的版本迭代时递增(breaking changes)。
- 2 次版本号:发布了功能性更新时递增。
- 3 修订号:发布了bug修复类更新时递增
- 发布版本,版本形式要求 https://go.dev/ref/mod#versions
-
go get
sh
- go get 有三种拉取方法
- 版本号形式拉取: go get github.com/oceanweave/hello/demo1@v1.0.1
- 分支形式拉取: go get github.com/oceanweave/hello/demo1@git-tag-test 会拉取分支最新的 commit
- commit-id形式拉取: go get github.com/oceanweave/hello/demo1@0c74f12d
- 包的引用
- 注意 go get 是把另一个项目的所有 go 包都下载下来了
- 但引用时要指定 go 包的详细路径
go
// hello 项目
package main
import (
// go get -u github.com/oceanweave/testgomod 下载了 testgomod 项目所有的包
// 但引用时,要注意写明 要引用包的详细路径
"github.com/oceanweave/testgomod/pkg/demo1"
// 若写成下面这种,程序就不知道该引用哪个包
// "github.com/oceanweave/testgomod/pkg"
)
func main() {
demo1.Hello1()
}
2.6 不兼容版本发布
-
关键点:
-
v2 moudle 名称要和 v1 区分
sh- 因为 v2 进行了重大重构,v1 与 v2 不兼容,所以更改此处 module 进行重命名,避免用户拉取后的不兼容导致失败 - v1 版本的 module 名称 `module github.com/oceanweave/testgomod` - v2 版本的 module 名称 `module github.com/oceanweave/testgomod/v2`
-
同时拉取和使用方式也会有所不同
sh- 更改module名称后,现在拉取方式为,注意多个 v2 路径 - v1 版本拉取 go get -u github.com/oceanweave/testgomod@v1.0.1 - v2 版本拉取 go get -u github.com/oceanweave/testgomod/v2@v2.0.0 # v2.0.0 是我们自己手动打的 tag,和上面 v1.0.1 操作方法一致 - import 使用时,注意多个 v2 路径 - v1 版本 import "github.com/oceanweave/testgomod/tools" - v2 版本 import "github.com/oceanweave/testgomod/v2/tools"
-
-
方法1:为旧版本在 git 上新建个分支 branch
- 比如 testgomod 项目
- main 或 master 分支执行
- git checkout -b v1
- git push
-
方法2:新建一个子目录 v2 保存 v2 代码
- git push 就好,便于维护
sh
# 仍以 testgomod 项目举例
# 原来 v1 版本 go.mod 文件如下
module github.com/oceanweave/testgomod
go 1.21.4
# 在该项目下,新建一个 v2 目录,将 v2 的所有代码和 go.mod 文件放入其中
# 现在 v2 版本进行了重大重构,很多函数都发生了改变
# 若 v1 版本的老用户误引下载新版本,将会导致其程序无法运行,引发事故
# 所以做了如下改动,避免用户误引用,更改了 go.mod,在后面加了 v2
module github.com/oceanweave/testgomod/v2
go 1.21.4
# 这样拉取包的时候就有所区分了,
# 不过要注意拉取 和 import 方式的不同,注意多个 v2 路径
- 不兼容,更改module名称后,现在拉取方式为,注意多个 v2 路径
- v1 版本拉取 go get -u github.com/oceanweave/testgomod@v1.0.1
- v2 版本拉取 go get -u github.com/oceanweave/testgomod/v2@v2.0.0 # v2.0.0 是我们自己手动打的 tag,和上面 v1.0.1 操作方法一致
- import 使用时,注意多个 v2 路径
- v1 版本 import "github.com/oceanweave/testgomod/tools"
- v2 版本 import "github.com/oceanweave/testgomod/v2/tools"
2.7 废弃已发布版本
个人测试未生效,没找到原因
如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用retract
声明废弃的版本。例如我们在hello/go.mod
文件中按如下方式声明即可对外废弃v1.0.1
版本。
go
module github.com/oceanweave/testgomod
go 1.21.4
retract v1.0.1
用户使用go get下载v1.0.1
版本时就会收到提示,催促其升级到其他版本。
这个特性是在Go1.16版本中引入,用来声明该第三方模块的某些发行版本不能被其他模块使用;
使用场景:发生严重问题或者无意发布某些版本后,模块的维护者可以撤回该版本,支持撤回单个或多个版本;
这种场景以前的解决办法:
维护者删除有问题版本的tag,重新打一个新版本的tag;
使用者发现有问题的版本tag丢失,手动介入升级,并且不明真因;
引入retract后,维护者可以使用retract在go.mod中添加有问题的版本:
javascript
// 严重bug...
retract (
v0.1.0
v0.2.0
)
重新发布新版本后,在引用该依赖库的使用执行go list可以看到 版本和"严重bug..."的提醒;
该特性的主要目的是将问题更直观的反馈到开发者的手中;
版本升级
Go模块中规范了一个重要原则
If an old package and a new package have the same import path, the new package must be backwards compatible with the old package. 3
如果旧包和新包具有相同的导入路径,新包必须向后兼容旧包。
v0->v1
假设我们自定义的模块已经稳定了,那么开始要对外发布v1.0.0版本了
- 拉取新的分支v1.0.0_branch
- git tag v1.0.0 并且推送到远程仓库
v1->v2
v1->v2的升级属于主版本的升级(v2不向后兼容), 这里有两种方式:
- 创建新的版本目录v2
- 继续使用v0->v1的升级方式
方法1 创建新的版本目录v2(新建目录)
按照go模块规范的原则,我们不能继续使用github.com/youdw/hello
这个包路径了。当前v1版本目录如下;
sh
.
├── go.mod
├── go.sum
├── hello.go
└── hello_test.go
为了开始开发v2,在hello目录下创建v2目录,然后把.go 和 mod 文件复制到v24 ,然后将v2目录下的mod路径修改为 github.com/youdw/hello/v2
sh
mkdir v2
cp *.go v2/
cp go.mod v2/go.mod
go mod edit -module github.com/youdw/hello/v2 v2/go.mod
完成之后的项目结构如下:
sh
.
├── go.mod
├── go.sum
├── hello.go
├── hello_test.go
└── v2
├── go.mod
├── hello.go
└── hello_test.go
1 directory, 7 files
发布v2
go
git tag v2.0.0
git push origin v2.0.0
方法2 继续使用v0->v1的升级方式(新建分支)
拉取新的分支,修改go.mod路径为github.com/youdw/hello/v2
,基于master分支打tag v2.0.0
go
go mod edit -module github.com/youdw/hello/v2
git push
git tag v2.0.0
git push origin v2.0.0
推荐方式
以上两种方式,第一种是go模块推荐的方式,但是维护和开发的开销较大
go module
在Go语言的早期版本中,我们编写Go项目代码时所依赖的所有第三方包都需要保存在GOPATH这个目录下面。这样的依赖管理方式存在一个致命的缺陷,那就是不支持版本管理,同一个依赖包只能存在一个版本的代码。可是我们本地的多个项目完全可能分别依赖同一个第三方包的不同版本。
go module介绍
Go module 是 Go1.11 版本发布的依赖管理方案,从 Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。Go module 提供了以下命令供我们使用:
go module相关命令
命令 | 介绍 |
---|---|
go mod init | 初始化项目依赖,生成go.mod文件 |
go mod download | 根据go.mod文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑go.mod文件 |
go mod vendor | 将项目的所有依赖导出至vendor目录 |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
Go语言在 go module 的过渡阶段提供了 GO111MODULE
这个环境变量来作为是否启用 go module 功能的开关,考虑到 Go1.16 之后 go module 已经默认开启,所以本书不再介绍该配置,对于刚接触Go语言的读者而言完全没有必要了解这个历史包袱。
GOPROXY
这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式,直接通过镜像站点来快速拉取。
GOPROXY 的默认值是:https://proxy.golang.org,direct
,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个https://goproxy.cn
和https://goproxy.io
,当然如果你的公司有提供GOPROXY地址那么就直接使用。设置GOPAROXY的命令如下:
bash
go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 "," 分隔。最后的 "direct" 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 "direct" 时触发回源,也就是回到源地址去抓取。
GOPRIVATE
设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。
GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 "," 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:
bash
$ go env -w GOPRIVATE="git.mycompany.com"
这样在拉取以git.mycompany.com
为路径前缀的依赖包时就能正常拉取了。
此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none
,允许通内部代理拉取私有仓库的包。
使用go module引入包
接下来我们将通过一个示例来演示如何在开发项目时使用 go module 拉取和管理项目依赖。
初始化项目 我们在本地新建一个名为holiday
项目,按如下方式创建一个名为holiday
的文件夹并切换到该目录下:
bash
$ mkdir holiday
$ cd holiday
目前我们位于holiday
文件夹下,接下来执行下面的命令初始化项目。
bash
$ go mod init holiday
go: creating new go.mod: module holiday
该命令会自动在项目目录下创建一个go.mod
文件,其内容如下。
go
module holiday
go 1.16
其中:
- module holiday:定义当前项目的导入路径
- go 1.16:标识当前项目使用的 Go 版本
go.mod
文件会记录项目使用的第三方依赖包信息,包括包名和版本,由于我们的holiday
项目目前还没有使用到第三方依赖包,所以go.mod
文件暂时还没有记录任何依赖包信息,只有当前项目的一些信息。
接下来,我们在项目目录下新建一个main.go
文件,其内容如下:
go
// holiday/main.go
package main
import "fmt"
func main() {
fmt.Println("现在是假期时间...")
}
然后,我们的holiday
项目现在需要引入一个第三方包github.com/q1mi/hello
来实现一些必要的功能。类似这样的场景在我们的日常开发中是很常见的。我们需要先将依赖包下载到本地同时在go.mod
中记录依赖信息,然后才能在我们的代码中引入并使用这个包。下载依赖包主要有两种方法。
第一种方法是在项目目录下执行go get
命令手动下载依赖的包:
bash
holiday $ go get -u github.com/q1mi/hello
go get: added github.com/q1mi/hello v0.1.1
这样默认会下载最新的发布版本,你也可以指定想要下载指定的版本号的。
bash
holiday $ go get -u github.com/q1mi/hello@v0.1.0
go: downloading github.com/q1mi/hello v0.1.0
go get: downgraded github.com/q1mi/hello v0.1.1 => v0.1.0
如果依赖包没有发布任何版本则会拉取最新的提交,最终go.mod
中的依赖信息会变成类似下面这种由默认v0.0.0的版本号和最新一次commit的时间和hash组成的版本格式:
go
require github.com/q1mi/hello v0.0.0-20210218074646-139b0bcd549d
如果想指定下载某个commit对应的代码,可以直接指定commit hash,不过没有必要写出完整的commit hash,一般前7位即可。例如:
bash
holiday $ go get github.com/q1mi/hello@2ccfadd
go: downloading github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
go get: added github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
此时,我们打开go.mod
文件就可以看到下载的依赖包及版本信息都已经被记录下来了。
go
module holiday
go 1.16
require github.com/q1mi/hello v0.1.0 // indirect
行尾的indirect
表示该依赖包为间接依赖,说明在当前程序中的所有 import 语句中没有发现引入这个包。
另外在执行go get
命令下载一个新的依赖包时一般会额外添加-u
参数,强制更新现有依赖。
第二种方式是我们直接编辑go.mod
文件,将依赖包和版本信息写入该文件。例如我们修改holiday/go.mod
文件内容如下:
go
module holiday
go 1.16
require github.com/q1mi/hello latest
表示当前项目需要使用github.com/q1mi/hello
库的最新版本,然后在项目目录下执行go mod download
下载依赖包。
bash
holiday $ go mod download
如果不输出其它提示信息就说明依赖已经下载成功,此时go.mod
文件已经变成如下内容。
go
module holiday
go 1.16
require github.com/q1mi/hello v0.1.1
从中我们可以知道最新的版本号是v0.1.1
。如果事先知道依赖包的具体版本号,可以直接在go.mod
中指定需要的版本然后再执行go mod download
下载。
这种方法同样支持指定想要下载的commit进行下载,例如直接在go.mod
文件中按如下方式指定commit hash,这里只写出来了commit hash的前7位。
go
require github.com/q1mi/hello 2ccfadda
执行go mod download
下载完依赖后,go.mod
文件中对应的版本信息会自动更新为类似下面的格式。
go
module holiday
go 1.16
require github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
下载好要使用的依赖包之后,我们现在就可以在holiday/main.go
文件中使用这个包了。
go
package main
import (
"fmt"
"github.com/q1mi/hello"
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi() // 调用hello包的SayHi函数
}
将上述代码编译执行,就能看到执行结果了。
bash
holiday $ go build
holiday $ ./holiday
现在是假期时间...
你好,我是七米。很高兴认识你。
当我们的项目功能越做越多,代码越来越多的时候,通常会选择在项目内部按功能或业务划分成多个不同包。Go语言支持在一个项目(project)下定义多个包(package)。
例如,我们在holiday
项目内部创建一个新的package------summer
,此时新的项目目录结构如下:
bash
holidy
├── go.mod
├── go.sum
├── main.go
└── summer
└── summer.go
其中holiday/summer/summer.go
文件内容如下:
go
package summer
import "fmt"
// Diving 潜水...
func Diving() {
fmt.Println("夏天去诗巴丹潜水...")
}
此时想要在当前项目目录下的其他包或者main.go
中调用这个Diving
函数需要如何引入呢?这里以在main.go
中演示详细的调用过程为例,在项目内其他包的引入方式类似。
go
package main
import (
"fmt"
"holiday/summer" // 导入当前项目下的包
"github.com/q1mi/hello" // 导入github上第三方包
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi()
summer.Diving()
}
从上面的示例可以看出,项目中定义的包都会以项目的导入路径为前缀。
如果你想要导入本地的一个包,并且这个包也没有发布到到其他任何代码仓库,这时候你可以在go.mod
文件中使用replace
语句将依赖临时替换为本地的代码包。例如在我的电脑上有另外一个名为liwenzhou.com/overtime
的项目,它位于holiday
项目同级目录下:
bash
├── holiday
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── summer
│ └── summer.go
└── overtime
├── go.mod
└── overtime.go
由于liwenzhou.com/overtime
包只存在于我本地,并不能通过网络获取到这个代码包,这个时候应该如何在holidy
项目中引入它呢?
我们可以在holidy/go.mod
文件中正常引入liwenzhou.com/overtime
包,然后像下面的示例那样使用replace
语句将这个依赖替换为使用相对路径表示的本地包。
go
module holiday
go 1.16
require github.com/q1mi/hello v0.1.1
require liwenzhou.com/overtime v0.0.0
replace liwenzhou.com/overtime => ../overtime
这样,我们就可以在holiday/main.go
下正常引入并使用overtime
包了。
go
package main
import (
"fmt"
"holiday/summer" // 导入当前项目下的包
"liwenzhou.com/overtime" // 通过replace导入的本地包
"github.com/q1mi/hello" // 导入github上第三方包
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi()
summer.Diving()
overtime.Do()
}
我们也经常使用replace
将项目依赖中的某个包,替换为其他版本的代码包或我们自己修改后的代码包。
go.mod文件
go.mod
文件中记录了当前项目中所有依赖包的相关信息,声明依赖的格式如下:
bash
require module/path v1.2.3
其中:
- require:声明依赖的关键字
- module/path:依赖包的引入路径
- v1.2.3:依赖包的版本号。支持以下几种格式:
- latest:最新版本
- v1.0.0:详细版本号
- commit hash:指定某次commit hash
引入某些没有发布过tag
版本标识的依赖包时,go.mod
中记录的依赖版本信息就会出现类似v0.0.0-20210218074646-139b0bcd549d
的格式,由版本号、commit时间和commit的hash值组成。
go.sum文件
使用go module下载了依赖后,项目目录下还会生成一个go.sum
文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum
文件内容通常是以类似下面的格式出现。
go
<module> <version>/go.mod <hash>
或者
go
<module> <version> <hash>
<module> <version>/go.mod <hash>
不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了go.sum
机制来对依赖包进行校验。
依赖保存位置
Go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod
目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。
bash
mod
├── cache
├── cloud.google.com
├── github.com
└──q1mi
├── hello@v0.0.0-20210218074646-139b0bcd549d
├── hello@v0.1.1
└── hello@v0.1.0
...
如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache
命令。
使用go module发布包
在上面的小节中我们学习了如何在项目中引入别人提供的依赖包,那么当我们想要在社区发布一个自己编写的代码包或者在公司内部编写一个供内部使用的公用组件时,我们该怎么做呢?接下来,我们就一起编写一个代码包并将它发布到github.com
仓库,让它能够被全球的Go语言开发者使用。
我们首先在自己的 github 账号下新建一个项目,并把它下载到本地。我这里就以创建和发布一个名为hello
的项目为例进行演示。这个hello
包将对外提供一个名为SayHi
的函数,它的作用非常简单就是向调用者发去问候。
bash
$ git clone https://github.com/q1mi/hello
$ cd hello
我们当前位于hello
项目目录下,执行下面的命令初始化项目,创建go.mod
文件。需要注意的是这里定义项目的引入路径为github.com/q1mi/hello
,读者在自行测试时需要将这部分替换为自己的仓库路径。
bash
hello $ go mod init github.com/q1mi/hello
go: creating new go.mod: module github.com/q1mi/hello
接下来我们在该项目根目录下创建 hello.go
文件,添加下面的内容:
go
package hello
import "fmt"
func SayHi() {
fmt.Println("你好,我是七米。很高兴认识你。")
}
然后将该项目的代码 push 到仓库的远端分支,这样就对外发布了一个Go包。其他的开发者可以通过github.com/q1mi/hello
这个引入路径下载并使用这个包了。
一个设计完善的包应该包含开源许可证及文档等内容,并且我们还应该尽心维护并适时发布适当的版本。github 上发布版本号使用git tag为代码包打上标签即可。
bash
hello $ git tag -a v0.1.0 -m "release version v0.1.0"
hello $ git push origin v0.1.0
经过上面的操作我们就发布了一个版本号为v0.1.0
的版本。
Go modules中建议使用语义化版本控制,其建议的版本号格式如下:
其中:
- 主版本号:发布了不兼容的版本迭代时递增(breaking changes)。
- 次版本号:发布了功能性更新时递增。
- 修订号:发布了bug修复类更新时递增。
发布新的主版本
现在我们的hello
项目要进行与之前版本不兼容的更新,我们计划让SayHi
函数支持向指定人发出问候。更新后的SayHi
函数内容如下:
go
package hello
import "fmt"
// SayHi 向指定人打招呼的函数
func SayHi(name string) {
fmt.Printf("你好%s,我是七米。很高兴认识你。\n", name)
}
由于这次改动巨大(修改了函数之前的调用规则),对之前使用该包作为依赖的用户影响巨大。因此我们需要发布一个主版本号递增的v2
版本。在这种情况下,我们通常会修改当前包的引入路径,像下面的示例一样为引入路径添加版本后缀。
go
// hello/go.mod
module github.com/q1mi/hello/v2
go 1.16
把修改后的代码提交:
bash
hello $ git add .
hello $ git commit -m "feat: SayHi现在支持给指定人打招呼啦"
hello $ git push
打好 tag 推送到远程仓库。
bash
hello $ git tag -a v2.0.0 -m "release version v2.0.0"
hello $ git push origin v2.0.0
这样在不影响使用旧版本的用户的前提下,我们新的版本也发布出去了。想要使用v2
版本的代码包的用户只需按修改后的引入路径下载即可。
bash
go get github.com/q1mi/hello/v2@v2.0.0
在代码中使用的过程与之前类似,只是需要注意引入路径要添加 v2 版本后缀。
go
package main
import (
"fmt"
"github.com/q1mi/hello/v2" // 引入v2版本
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi("张三") // v2版本的SayHi函数需要传入字符串参数
}
废弃已发布版本
如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用retract
声明废弃的版本。例如我们在hello/go.mod
文件中按如下方式声明即可对外废弃v0.1.2
版本。
go
module github.com/q1mi/hello
go 1.16
retract v0.1.2
用户使用go get下载v0.1.2
版本时就会收到提示,催促其升级到其他版本。