Go Path

在 Golang 1.11 之前,Go 采用的是手动依赖管理,也就是使用 Go Path 的方式管理依赖。

使用 Go Path 会有以下问题:

  1. 代码开发必须在 Go Path 的 src 目录下。
  2. 依赖手动管理。
  3. 依赖包没有版本可言。

Go Modules

Go Modules 是 Golang 1.11 新加的特性。Go Modules 的特点如下:

  • 模块是相关 Go 包的集合。
  • Modules 是源代码交换和版本控制的单元。
  • Go 命令直接支持使用 Modules,包括记录和解析对其他模块的依赖性。
  • Modules 替换了旧的 Go Path 方法。

在使用 Go Modules 之前需要对环境变量进行一些设置:

  • 设置 GO111MODULEon
  • 设置 GOPROXYhttps://goproxy.cn(七牛云的 Go 代理,)。

根据不同平台有不同的设置方式:

  • Golang 1.13 及以上:

    $ go env -w GO111MODULE=on
    $ go env -w GOPROXY=https://goproxy.cn,direct
    
  • MacOS 或 Linux

    $ export GO111MODULE=on
    $ export GOPROXY=https://goproxy.cn
    

    或者

    $ echo "export GO111MODULE=on" >> ~/.profile
    $ echo "export GOPROXY=https://goproxy.cn" >> ~/.profile
    $ source ~/.profile
    
  • Windows:

    C:\> $env:GO111MODULE = "on"
    C:\> $env:GOPROXY = "https://goproxy.cn"
    

    或者,直接在“高级系统设置”中的“环境变量”中添加对应的环境变量。


创建模块

创建 greetings 模块:

  1. 创建一个模块目录(在任意位置都可以,此时不用指定在 Go Path 下),并切换到目录中。

    假设工作目录为 example.com,在工作目录下创建 greetings 模块:

    $ cd example.com
    $ mkdir greetings
    $ cd greetings
    
  2. 使用 go mod init <module-name> 初始化模块。

    $ go mod init example.com/greetings
    

    go mod init 会在 greetings 目录下创建一个 go.mod 文件。go.mod 用于记录当前模块的名称、Golang SDK 版本以及项目依赖等信息。

    go.mod 的基本内容如下:

    // 模块名
    module example.com/greetings
    // Golang SDK 版本
    go 1.22.4
    

    在后续的使用中,go.mod 还可能包含以下内容:

    // 项目所需依赖
    require (
      // 依赖的格式如下:
      dependencyName vision
      // ...
    )
    
    // 项目中排除的依赖
    exclude (
      // 格式同上
      dependencyName vision
      // ...
    )
    
    // 替换第三方依赖
    replace (
      sourceName vision => targetName vision
      // ...
    )
    
    // 撤回当前项目中有问题的版本
    retract (
      version
      // ...
    )
    
  3. greetings 创建对应的 greetings.go

    package grettings
    
    import "fmt"
    
    func Hello(name string) (message string) {
        message = fmt.Sprintf("Hi, %v. Welcome!", name)
        return message
    }
    

创建 hello 模块:

  1. example.com 创建 hello 目录并初始化:

    $ mkdir hello
    $ cd hello
    $ go mod init example.com/hello
    
  2. hello 目录下创建 hello.go 并编写 main() 来调用 greetings.Hello(name string)

    package main
    
    import (
        "fmt"
    
        "example.com/greetings"
    )
    
    func main() {
        message := greetings.Hello("Linner")
        fmt.Println(message)
    }
    
  3. 由于 hello.go 使用到了 example.com/greetings,所以需要对其进行导入。由于 example.com/greetings 是当前 example.com 中的子模块,example.com/greetings 并未使用任何版本管理工具,所以需要使用 mod replace 为其指定路径。

    hello 目录下:

    # 指定模块路径(一般实在开发环境下使用)
    $ go mod edit -replace example.com/greetings=../greetings
    # 导入 example.com/greetings
    $ go get example.com/greetings
    

    执行以上命令后,在 hello/go.mod 中会添加如下内容:

    replace example.com/greetings => ../greetings
    
    require example.com/greetings v0.0.0-00010101000000-000000000000 // indirect
    

    由于没用使用版本工具发布版本(如使用 Git Tag),所以导入之后,Go Get 会使用一个伪版本号(pseudo-version number)暂替。

完成创建后,可以在 hello 目录中 runbuild

go run .
$ go build
$ ./hello.exe

Go 中的伪版本号的格式遵循语义化版本控制的原则,并在此基础上增加了一段额外的信息来唯一标识Git提交。通常伪版本号使用如下格式生成:

v0.0.0-YYYYMMDDHHMMSS-CommitHash
  • v0.0.0 表示模块尚未定义正式版本,或依赖的是一个无标签的提交;
  • YYYYMMDDHHMMSS 是提交日期的时间戳,精确到秒,确保了时间上的唯一性;
  • CommitHash 是 Git 提交的前几位哈希值,进一步确保了每个提交的唯一性。

由于 example.com/greetings 模块并未使用 Git,所以伪版本号是固定的 v0.0.0-00010101000000-000000000000


go mod 命令

使用 go help mod 可以查看 go mod 命令的信息:

$ go mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

        go mod <command> [arguments]

The commands are:

        download    download modules to local cache
        edit        edit go.mod from tools or scripts
        graph       print module requirement graph
        init        initialize new module in current directory
        tidy        add missing and remove unused modules
        vendor      make vendored copy of dependencies
        verify      verify dependencies have expected content
        why         explain why packages or modules are needed

Use "go help mod <command>" for more information about a command.
命令 说明
download 下载依赖包到本地缓存
edit 编辑 go.mod
graph 打印模块依赖图
verify 在当前目录初始化新的模块
tidy 拉取缺少的模块,移除不用的模块
vendor 将依赖复制到 vendor
verify 验证依赖是否正确
why 解释为什么包或模块依赖被依赖

代码仓库

通常情况下 Go Modules 是和 Git 一同使用的,在 Go 中创建一个模块的标准流程实际是:

  1. 初始化。

    在模块目录中进行如下操作:

    $ go mod init <module-name>
    $ git init
    $ git commit -am "init"
    $ git remote add origin <remote-url>
    $ git push -u origin main
    
  2. 开发模块。

    模块开发完成后,需要进行发布。Go Modules 中的发布实际就是使用 Git 提交代码。

    git push
    
  3. 在任意机器上运行如下命令即可自动安装依赖:

    go get <module-name>[@version]
    

在发布模块的时候需要注意。模块对应的远程仓库需要设置为 Public。如果要导入的模块存放在你的私有仓库中,需要将环境变量 GOPRIVATE 设置为你的远程仓库的用户目录。例如我的 Github 主页是 https://github.com/Linna-cy/ 那么我需要设置 GOPRIVATE=github.com/Linna-cy/*。再次运行 go get 命令,在通过用户名密码验证后就能正常从我的私有仓库下载依赖到本地。

一般情况下,在创建模块和远程仓库的时候,会将模块名称设置为 远程仓库地址/用户名/仓库名 的形式。


版本管理

Go Modules 中并没有与版本相关的配置项,而是依靠于 Git 进行版本管理。

如上例,假设模块名称为 github.com/Linna-cy/go-utils,运行 go get github.com/Linna-cy/go-utils 时,在 go.mod 中的结果可能如下:

require github.com/Linna-cy/go-utils v0.0.0-20240608124125-a86730578714

可以观察到,当没有使用 Git 给模块指定版本时,默认的版本号(伪版本号)是通过时间戳等信息生成的。

go get github.com/Linna-cy/go-utils 在没有指定版本时,会自动获取 github.com/Linna-cy/go-utils 的最新版本。而模块 github.com/Linna-cy/go-utils 恰巧没有使用 Gti 设置任何版本,所以获取到的最新的版本就是 Go 自动生成的。

设置版本号

要设置版本,需要通过 Git Tag 进行设置。例如给 github.com/Linna-cy/go-utils 设置一个 v1.0.0 的版本号并进行发布:

$ git tag v1.0.0
$ git push --tags

Git Tag 创建标签时,也可以在标签中包含一些信息(这种标签称为附注标签):

git tag -a <tagname> -m "Tag message describing the version."

导入依赖

然后就可以导入指定版本的依赖:

$ go get github.com/Linna-cy/go-utils@v1.0.0
go: downloading github.com/Linna-cy/go-utils v1.0.0
go: upgraded github.com/Linna-cy/go-utils v0.0.0-20240608124125-a86730578714 => v1.0.0

go.modgithub.com/Linna-cy/go-utils 将更新为:

require github.com/Linna-cy/go-utils v1.0.0

go.mod 更新的同时,还生成了 go.sum,其中除了软件包名和版本号,还包含了软件包的哈希值,以确保具有正确的版本和文件。

依赖导入成功后,就可以在代码中使用 import 进行导入:

import "github.com/Linna-cy/go-utils"

通常情况下,版本号使用语义化版本号(Semantic Versioning,SemVer)。SemVer 的格式通常为 MAJOR.MINOR.PATCH,每个部分都是一个非负整数,并且在数值上递增。其具体含义如下:

  1. MAJOR(主版本号):当做了不兼容的 API 修改时,主版本号应该递增。这表明新版本无法向后兼容旧版本,使用者可能需要修改代码才能适配新版本。
  2. MINOR(次版本号):当新增了向后兼容的功能时,次版本号应该递增。这意味着新版本添加了功能,但所有公共接口保持与旧版本兼容,用户无需修改代码即可安全升级。
  3. PATCH(修订号):当进行了向后兼容的错误修正时,修订号应该递增。这类更新修复了问题,但不对公开的 API 做任何改变,因此对用户而言是透明的升级。

分支

在使用 Git Tag 给提交标记上版本号后,假设当前标记的版本号是当前主版本下的第一个版本,一般情况下会给当前主版本创建一个新的分支,用于当前主版本后续的修复推送。例如 github.com/Linna-cy/go-utils,在推送版本号之后,还可以:

$ git checkout -b v1 v1.0.0
$ git push -u origin v1

其中,checkout 的含义如下(tag-name 是可选的):

git checkout -b <new-branch> [tag-name]

迭代和修复

一般版本后续的迭代和修复,不会直接在主分支(mainmaster)上进行修改,而是创建新的分支进行修改。当修改后的内容通过测试,再将其合并到主分支上。

例如 github.com/Linna-cy/go-utils v1.0.0 有 Bug。当我们修复完成后,可以:

# 提交示例(根据实际情况进行修改)
$ git commit -am "fix: 修复了 xxx 问题"
$ git tag v1.0.1
$ git push --tags origin v1
# 如果已经 git push -u origin v1,可以直接 git push --tags
$ git push origin v1:v1
# 如果已经 git push -u origin v1,可以直接 git push

当修复完成后,在使用了 github.com/Linna-cy/go-utils v1.0.0 的模块中,需要对 github.com/Linna-cy/go-utils 进行更新。更新通常有以下方式:

# 对所有依赖进行更新升级
$ go get -u
$ go get -u=patch
# 指定包和版本进行更新升级
$ go get github.com/Linna-cy/go-utils@v1.0.1

如果要对主版本进行迭代,即发布新的主版本,一般步骤如下:

  1. 修改 go.mod

    由于主要版本可能会破坏向后兼容性,所以可以通过修改 go.modmodule 项的方式,告知两个版本并不兼容。

    module <module-name>/<major>
    go <version>
    

    例如在 github.com/Linna-cy/go-utils 中添加了一系列新的接口,或对原有的接口进行修改,且修改后的接口不兼容旧版本。此时就需要对主版本进行迭代。假设将主版本迭代到 v2,并且发布了新的版本 v2.0.0,此时对应的 go.mod

    module github.com/Linna-cy/go-utils/v2
    
    go 1.22.4
    
    // 省略其它内容...
    
  2. 发布新版本

    假设要发布 github.com/Linna-cy/go-utils v2.0.0

    # 提交示例(根据实际情况进行修改)
    $ git commit -am "feat: 发布新版本 v2.0.0,xxx"
    $ git tag -a v2.0.0 -m "添加了 xxx,修改了 xxx,新版支持 xxx"
    # 签出分支
    $ git checkout -b v2 v2.0.0
    $ git push --tags origin v2
    
  3. 使用新版本。

原先使用了 github.com/Linna-cy/go-utils 的模块并不会受到影响。如果需要升级模块的主版本,或在其它模块中使用 github.com/Linna-cy/go-utils/v2 需要将 import "github.com/Linna-cy/go-utils" 修改为 import "github.com/Linna-cy/go-utils/v2"