返回

Go modules详解

Go modules详细介绍。


概述

大多数语言都有“依赖”、“包”等概念,Go语言的依赖处理经历了几次变革。

最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。从v1.5开始引入vendor模式,如果项目目录下有vendor目录,那么go工具链会优先使用vendor内的包进行编译、测试等。

v1.11开始,引入了Go Modules 作为依赖解决方案,到v1.14宣布Go Modules已经可以用于生产环境,到v1.16版本开始Go Module默认开启。

什么是 Go Modules

Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,成长于 Go1.12,丰富于 Go1.13,正式于 Go1.14 推荐在生产上使用。

Go Modules使得Go语言开发者能够更方便地管理代码包及其版本,并能够与现有的版本控制工具(如Git、SVN等)集成使用。

在传统的GOPATH模式中,所有Go代码都必须位于一个全局的GOPATH路径之下,这使得在不同项目中使用不同版本的依赖包变得非常困难。然而,在Go Modules模式下,每个项目都可以独立管理自己的依赖关系,具有更好的兼容性。当使用Go Modules模式后,项目中会自动创建go.mod文件,其中记录了项目所依赖的模块及其版本信息。go.mod是Go语言项目中的模块文件,用于管理项目的依赖关系和版本信息。

Go Modules也支持语义化版本控制,这意味着开发者可以指定依赖包的版本范围,而不是仅仅依赖最新的版本。这种灵活性有助于确保项目的稳定性和可维护性。

Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题:

  • Go 语言长久以来的依赖管理问题。
  • “淘汰”现有的 GOPATH 的使用模式。
  • 统一社区中的其它的依赖管理工具(提供迁移功能)。

优势

  • 首先,研发者能够在任何目录下工作,而不仅仅是在GOPATH指定的目录。
  • 可以安装依赖包的指定版本,而不是只能从master分支安装最新的版本。
  • 可以导入同一个依赖包的多个版本。当我们老项目使用老版本,新项目使用新版本时会非常有用。
  • 要有一个能够罗列当前项目所依赖包的列表。这个的好处是当我们发布项目时不用同时发布所依赖的包。Go能够根据该文件自动下载对应的包。

GO PATH介绍

安装好go开发环境之后,可以运行go env查看go运行时的环境变量。要修改这些环境变量,可以通过配置环境变量来覆盖默认值(覆盖了就不能通过命令设置),如临时设置export GO111MODULE=on。或者通过命令go env -w key=value,如go env -w GO111MODULE=on。通过命令修改的环境变量保存在GOENV这个环境变量指向的文件。

有两个比较重要的环境变量:

  • GOROOT:Golang 安装目录的路径,包含编译器程序和系统包,也可以放置三方包(不推荐)。新版本已经不需要配置这个环境变量了,安装了go会自动推断出该变量的值。如果安装之后环境变量中没有$GORROOT/bin,需要手动添加,这样才能直接在命令行中运行go编译程序。
  • GOPATH:该工作目录,放置编译后二进制和 import 包时的搜索路径,一般有三个目录: bin、pkg、src。并且该环境变量必须手动设置。
    • bin:用来存放编译后的可执行文件。引入Go modules之后用于存放get install安装的可执行文件。
    • pkg:存储预编译的目标文件,以加快程序的后续编译速度。引入Go modules之后用于存放第三方包。
    • src:存储所有.go文件或源代码。在编写 Go 应用程序,程序包和库时,一般会以$GOPATH/src/github.com/foo/bar的路径进行存放。引入Go modules之后用一般不用,go项目可以放在任意目录中,不在是$GOPATH/src

因此在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。

GOPATH模式的弊端

在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有一些弊端。

  • A. 无版本控制概念. 在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。
  • B.无法同步一致第三方版本号. 在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。
  • C.无法指定当前项目引用的第三方版本号. 你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar。

Go 语言官方从 Go1.11 起开始推进 Go modules(前身vgo,知道即可,不需要深入了解),Go1.13 起不再推荐使用 GOPATH 的使用模式,Go modules 也渐趋稳定,因此新项目也没有必要继续使用GOPATH模式。


Go Module 语义化版本规范

Go Module 的设计采用了语义化版本规范,语义化版本规范非常流行且具有指导意义,本文就来聊聊语义化版本规范的设计和在 Go 中的应用。

语义化版本规范

语义化版本规范(SemVer)是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立,旨在解决 依赖地狱 问题。

它清楚明了的规定了版本格式、版本号递增规:

版本格式:采用 X.Y.Z 的格式,X 是主版本号、Y 是次版本号、而 Z 为修订号(即:主版本号.次版本号.修订号),其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。

版本号递增规则:

主版本号:当做了不兼容的 API 修改。

次版本号:当做了向下兼容的功能性新增及修改。

修订号:当做了向下兼容的问题修正。

另外,先行版本号版本编译信息 可以加到 主版本号.次版本号.修订号 的后面,作为延伸。

完整版本格式如下:

版本格式

先行版本号可以有多个,如第一个为UTC时间,第二个为提交的哈希值:

1
2
v4.0.1-0.20210109023952-943e75fe5223+incompatible
v0.0.0-20240416160154-fe59bbe5cc7f

其中版本号核心部分 X.Y.Z 是必须的,使用 . 连接,先行版本号和版本编译信息是可选的,先行版本号通过 - 与核心部分连接,版本编译信息通过 + 与核心部分或先行版本号连接。

合法的几种版本号格式如下:

  1. 主版本号.次版本号.修订号
  2. 主版本号.次版本号.修订号-先行版本号
  3. 主版本号.次版本号.修订号+版本编译信息
  4. 主版本号.次版本号.修订号-先行版本号+版本编译信息

主版本号必须在有任何不兼容的修改被加入公共 API 时递增。每当主版本号递增时,次版本号和修订号必须归零。

次版本号必须在有向下兼容的新功能出现或有改进时递增,或在任何公共 API 的功能被标记为弃用时也必须递增。每当次版本号递增时,修订号必须归零。

修订号必须在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

存在先行版本号,意味着当前版本不够稳定,且可能存在兼容性问题。先行版本号是一连串以 . 分隔的标识符,由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,禁止出现空白符,数字类型则禁止在前方补零。合法示例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

版本编译信息标志符规格与先行版本号基本相同,略有差异的是数字类型前方允许补零。合法示例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

除了上面几点说明,还需要额外关注以下几点:

  1. 标记版本号的软件发行后,禁止改变该版本软件的内容。任何修改都必须以新版本发行。
  2. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。
  3. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。
  4. 社区中还存在一个不成文的规定,对于次版本号,偶数为稳定版本,奇数为开发版本。当然不是所有项目都这样设计。

使用语义化版本规范可能遇到的问题

在使用语义化版本规范过程中,可能人为或程序编写错误导致出现如下几种可预见的问题:

  1. 万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

    一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文档中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

    注意:不到万不得已,不要也不能去修改已发行的版本。

  2. 如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

    自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住,语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

  3. v1.2.3 是一个语义化版本号吗?

    v1.2.3 并不是的一个语义化的版本号。但是,在语义化版本号之前增加前缀 v 是用来表示版本号的常用做法。在版本控制系统中,将 version 缩写为 v 是很常见的。比如:git tag v1.2.3 -m "Release version 1.2.3" 中,v1.2.3 表示标签名称,而 1.2.3 是语义化版本号。go modules的模块版本也是在前面加v

如何验证语义化版本规范正确性

官方提供了两个正则可以检查语义化版本号的正确性。

  1. 支持按组名称提取匹配结果

    1
    
    ^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
    

    Go 语言示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"regexp"
    )
    
    func main() {
    	version := "0.1.2-alpha+001"
    	pattern := regexp.MustCompile(`^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
    	r := pattern.FindStringSubmatch(version)
    
    	m := make(map[string]string)
    	for i, name := range pattern.SubexpNames() {
    		if i == 0 {
    			m["version"] = r[i]
    		} else {
    			m[name] = r[i]
    		}
    	}
    
    	result, _ := json.MarshalIndent(m, "", "  ")
    	fmt.Printf("%s\n", result)
    }
    
    /*
    {
      "buildmetadata": "001",
      "major": "0",
      "minor": "1",
      "patch": "2",
      "prerelease": "alpha",
      "version": "0.1.2-alpha+001"
    }
    */
    
  2. 支持按编号提取匹配结果

    1
    
    ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
    

    Go 语言示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    package main
    
    import (
    	"fmt"
    	"regexp"
    )
    
    func main() {
    	version := "0.1.2-alpha+001"
    	pattern := regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
    	r := pattern.FindStringSubmatch(version)
    
    	for i, s := range r {
    		fmt.Printf("%d -> %s\n", i, s)
    	}
    }
    
    /*
    0 -> 0.1.2-alpha+001
    1 -> 0
    2 -> 1
    3 -> 2
    4 -> alpha
    5 -> 001
    */
    

Go Modules版本设计

依赖地狱

我们先来看下早期 Go 依赖包存在的依赖地狱问题:

依赖地狱

首先存在两个包 pkg1pkg2,分别依赖 pkg3v1.0.0 版本和 v2.0.0 版本,现在我们开发一个 app 包,它依赖 pkg1pkg2,那么此时由于 app 包只允许包含一个 pkg3 依赖,所以 Go 构建工具无法抉择应该使用哪个版本的 pkg3。这就是所谓的依赖地狱问题。

语义导入版本

为了解决依赖地狱问题,Go 在 1.11 版本时引入和 Go Modules:

Go Modules

Go Module 解决问题的方式是,把 pkg3v1.0.0 版本和 v2.0.0 版本当作两个不同的包,这样也就允许了 app 包能够同时包含多个不同版本的 pkg3

在使用时,需要在包的导入路径上加上包的主版本号。这里以 go-micro 包使用为例,展示下 Go Module 语义导入版本的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import "go-micro.dev/v4"

// create a new service
service := micro.NewService(
    micro.Name("helloworld"),
)

// initialise flags
service.Init()

// start the service
service.Run()

可以看到导入路径为 "go-micro.dev/v4",其中 v4 就代表了需要引入 go-microv4.y.z 版本。


Go Modules基本使用

go modules相关命令

在 Go modules 中,我们能够使用如下命令进行操作:

命令介绍
go mod init <project>初始化项目依赖,生成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 modules参数配置

  • GO111MODULE

    Go语言提供了 GO111MODULE 这个环境变量来作为 Go modules 的开关,其允许设置以下参数:

    参数说明
    auto只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。
    on启用 Go modules,推荐设置,将会是Go1.16版本之后的默认值。
    off禁用 Go modules,不推荐设置。

    你可能会留意到 GO111MODULE 这个名字比较“奇特”,实际上在 Go 语言中经常会有这类阶段性的变量, GO111MODULE 这个命名代表着Go语言在 1.11 版本添加的。后续版本中可能会去掉。

  • GOPROXY

    这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS(版本控制系统,如github,就是源地址下载) 方式,直接通过镜像站点来快速拉取。值为off表示禁止模块代理。

    设置GOPROXY可以加速模块下载确保构建确定性(提供稳定的构建版本),提高安全性确保模块始终可用

    GOPROXY 的默认值是:https://proxy.golang.org,direct,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前国内社区使用比较多的有两个 https://goproxy.cnhttps://goproxy.io,当然如果你的公司有提供GOPROXY地址那么就直接使用。并且修改的代理,通过go get命令下载自己的公共模块,也会同步到 https://pkg.go.dev/

    设置GOPAROXY的命令如下:

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

    GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。就是代理失败之后用传统方式(源地址下载模块)。

  • GOPRIVATE

    • GONOPROXY/GONOSUMDB/GOPRIVATE

      这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。

      更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。

      而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE。

    设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。

    GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:

    1
    
    $ go env -w GOPRIVATE="git.example.com,github.com/arlettebrook/demo"
    

    设置后,前缀为 git.xxx.com 和 github.com/arlettebrook/demo的模块都会被认为是私有模块。

    如果不想每次都重新设置,我们也可以利用通配符,例如:

    1
    
    $ go env -w GOPRIVATE="*.example.com"
    

    这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。

    此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none,允许通内部代理拉取私有仓库的包。

go modules模块文件

  • 初识化项目

    在项目的根目录下运行go mod init <project>,如go mod init github.com/arlettebrook/demodemo是项目名,github.com/arlettebrook/demo是模块导入路径,当导入的时候,如果本地没有,会去该路径下载。

  • go.mod 文件

    在初始化项目时,会生成一个 go.mod 文件,是启用了 Go modules 项目所必须的最重要的标识,同时也是 GO111MODULE 值为 auto 时的识别标识,它描述了当前项目(也就是当前模块)的元信息,每一行都以一个动词开头。

    示例文件

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    module github.com/arlettebrook/demo
    
    
    go 1.22.1
    
    
    require (
        example.com/apple v0.1.2
        example.com/banana v1.2.3
        example.com/banana/v2 v2.3.4
        example.com/pear // indirect
        example.com/strawberry // incompatible
    )
    
    
    exclude example.com/banana v1.2.4
    replace example.com/apple v0.1.2 => example.com/fried v0.1.0 
    replace example.com/banana => example.com/fish
    

    说明

    • module:用于定义当前项目的模块路径。
    • go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本,目前来看还只是个标识作用。
    • require:用于设置一个特定的模块版本。
    • exclude:用于从使用中排除一个特定的模块版本。
    • replace:用于将一个模块版本替换为另外一个模块版本。

    另外你会发现 example.com/pear 的后面会有一个 indirect 标识,indirect 标识表示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 go get 拉取下来的,也有可能是你所依赖的模块所依赖的,情况有好几种。incompatible:不兼容的

  • go.sum 文件

    在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。

    1
    2
    3
    4
    
    github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
    github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
    github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
    github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
    

    可以看到一个模块路径可能有如下两种:

    1
    2
    
    github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
    github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
    

    h1 hash 是 Go modules 将目标模块版本的 zip 文件开包后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。

    而 h1 hash 和 go.mod hash 两者,要不就是同时存在,要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash,就会出现不存在 h1 hash,只存在 go.mod hash 的情况。

    go.modgo.sum都应该被提交到git仓库中去。当别人使用你的项目时,mod保证依赖版本一直,sum保证依赖不被篡改。

go modules模块下载

  • 我们下载、添加模块使用go get -u <module path>

  • 默认下载、添加最新版本,首先会检查本地(pkg:全局模块缓存)是否存在,没有,在去下载。

  • 在项目中下载会自动添加到go.mod文件中。

  • -u选项会更新模块的依赖包到最新版本,推荐加上。

  • 还可以指定下载版本

    命令作用
    go get golang.org/x/text@latest拉取最新的版本,若存在tag,则优先使用。可以省略。
    go get golang.org/x/text@master拉取 master 分支的最新 commit。@branch
    go get golang.org/x/text@v0.3.2拉取 tag 为 v0.3.2 的 commit。@version,version必须满足语义化版本规范且前面加v。
    go get golang.org/x/text@342b2e拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2。@commit
    go get golang.org/x/text/v2下载主版本号为2的最新版
  • 最新版本的选择

    分两种情况

    1. 最新版本有发布tags:就以发布的版本,version一般为标签名,如v2.1.2
    2. 最新版本没有发布tags:就以提交的最新版本,version一般为已发布标签-最新提交日期-最新提交哈希+版本编译信息,版本编译信息一般没有。如v2.1.2-20240416160154-fe59bbe5cc7f,如果一次tags也没有发布,版本号则为v0.0.0,如v0.0.0-20240416160154-fe59bbe5cc7f
    3. 子模块同理

go modules全局缓存

Go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。

1
2
3
4
5
6
7
mod
├── cache
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
...

如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache 命令。

go modules模块导入

  • go模块导入用import "模块路径"

  • 当导入多个模块的时候用

    1
    2
    3
    4
    5
    
    import (  
        "fmt"  
        "os"  
        "path/filepath"  
    )
    
  • 别名导入用import 别名 "模块路径"

    1
    
    import f "fmt"
    
  • 点导入用import . "模块路径"

    点导入是一种特殊的导入方式,它将包中的所有公共标识符(函数、变量、类型等)提升到当前文件的命名空间中,这样在代码中就可以直接使用这些标识符,而不需要加上包名前缀。但是,这种方式可能会导致命名冲突和代码可读性下降,因此一般不建议使用。

  • 空导入用import _ "模块路径"

    空导入通常用于初始化包中的变量或者执行包中的初始化函数,而不直接使用该包中的其他标识符。

  • 注意事项

    • 当模块的主版本号为0或1的时候省略了主版本标识。

    • 当主版本号为2及以上时,不能省略主版本标识。否则会出现冲突。

    • 主版本标识只能为/v主版本号,不能用@version,一般使用主版本的最新版,这与语义化版本规范有关。

    • 如:

      1
      2
      
      import "github.com/jordan-wright/email"
      import "github.com/jordan-wright/email/v4"
      
    • 为什么忽略 v0 和 v1 的主版本号

      还是与语义化版本规范有关,v0属于开发初始阶段,其公共api不被视为稳定版,当版本到达v1,其公共api基本确定,在此之后如果不出现不兼容api的修改,是不会修改主版本号的。后续的次版本、修订号会向下兼容。这是官方所鼓励的。当api做了不兼容的修改,主版本号就会修改。为了不出现冲突就会加上主版本标识


gopkg.in介绍

  • gopkg.in是旧go包管理工具中的一个,并不是官方包管理工具。作用是下载时重定向到相应github仓库。优点是: URL 更干净、更短、导入路径稳定、易于使用、支持版本控制。

  • 浏览器打开链接,会提供对应包的godoc在线链接以及github仓库链接。

  • gopkg.in/ini.v1对应github仓库为githu.com/go-ini/ini,当没有指定用户名时,用户名默认为go-包名

    1
    2
    
    gopkg.in/pkg.v3       github.com/go-pkg/pkg (branch/tag v3, v3.N, or v3.N.M)
    gopkg.in/user/pkg.v3  github.com/user/pkg   (branch/tag v3, v3.N, or v3.N.M)
    
  • 版本控制用.vNumber表示.。

  • go modules的区别:

    • v1gopkg.in必须指定。go mod不用。
    • gopkg.in分隔符是.go mod/)。
    • v0为开发版、不稳定版,不指定默认为开发版,go mod不指定默认为v0或v1。
    • gopkg.in主版本为1就要指定主版本标识。go mod主版本为2才需要指定。
  • 如何让gopkg.in收录自己的模块:

    • go mod一样,当我们使用go get下载已经存在的版本仓库时,会自动同步到在线的godoc中。
    • 建议仓库名与用户名关系是pkggo-pkg,推荐gopkg.in。当然也可以直接使用github仓库路径。
    • 其他情况可以使用go mod。并通过像 proxy.golang.org 这样的代理服务器来分发你的模块。
  • gopkg.in版本控制同样遵循[语义化版本控制](#Go Module 语义化版本规范)。


总结

至此我们大致介绍了 Go modules 的前世今生、语义化版本规范以及基本使用。

Go modules 的成长和发展经历了一定的过程,如果你是刚接触的读者,直接基于 Go modules 的项目开始即可,如果既有老项目,那么是时候考虑切换过来了,Go1.14起已经准备就绪,并推荐你使用。


参考

  1. https://semver.org/lang/zh-CN/
  2. Go Module 语义化版本规范
  3. Go Modules详解
  4. Go module详细介绍
Licensed under CC BY-NC-SA 4.0
最后更新于 May 15, 2024 10:57 +0800