CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Go系统接口编程 前言
  • 第一部分:引言
  • 第1章 为什么选择Go语言?
  • 第2章 重温并发与并行
  • 第3章 理解系统调用
  • 第二部分:与操作系统交互
  • 第4章 文件和目录操作
  • 第5章 处理系统事件
  • 第6章 理解进程间通信中的管道
  • 第7章 Unix套接字
  • 第8章 内存管理
  • 第三部分:性能
  • 第9章 性能分析
  • 第10章 网络编程
  • 第四部分:连接的应用程序
  • 第11章 遥测技术
  • 第12章 分布式部署你的应用程序
    • Go模块
      • 使用模块的常规流程
      • 创建新模块
      • 理解模块版本控制
      • 更新依赖项
      • 语义化导入版本控制
      • 使用私有库
      • 版本控制和go install
      • 模块工作区(Module workspaces)
    • 持续集成(CI)
      • 缓存(Caching)
      • 静态分析(Static analysis)
    • 发布你的应用程序
    • 总结
  • 第五部分:深入探索
  • 第13章 顶点项目——分布式缓存
  • 第14章 高效编码实践
  • 第15章 精通系统编程
目录

第12章 分布式部署你的应用程序

# 第12章 分布式部署你的应用程序

在本章中,我们将探讨使用模块、持续集成(CI,Continuous Integration)和发布策略来分布式部署Go应用程序的关键概念和实际应用。在学习过程中,你将熟练掌握使用Go模块进行依赖管理,设置持续集成工作流程以实现测试和构建自动化,并掌握发布流程,从而无缝地分布式部署你的应用程序。

本章将涵盖以下关键主题:

  • Go模块
  • 持续集成
  • 发布你的应用程序

在本章结束时,你将掌握精确管理依赖项的知识,能够自动化测试和构建过程以尽早发现错误,并高效地打包和发布应用程序。这些技能为维护健壮的软件项目奠定了基础,使得项目易于管理、更新,并且能够随着团队规模的扩大而扩展,同时不会增加额外的繁琐流程。

# Go模块

Go模块,堪称依赖管理混乱海洋中的希望灯塔。好吧,也许在Go语言中处理依赖项并不像周日早上在公园散步那么轻松。但不妨把它想象成规划一次火星任务——确实很复杂,不过有了合适的工具(模块),潜在的回报是巨大的。

Go模块于Go 1.11版本引入,从根本上重塑了Golang的包管理格局,在系统编程领域尤为重要。这项功能提供了一个强大的系统来管理项目依赖项,封装项目所依赖的外部包的特定版本。Go模块的核心是,通过利用模块缓存和一组定义好的依赖项来实现可重现的构建,从而消除了臭名昭著的“在我机器上能运行”的问题。

Go模块解决了几类问题,使其使用体验变得更加可靠。我可以重点讲其中三个:可靠的版本控制、可重现的构建以及管理依赖项膨胀。

下面我来分别解释在引入Go模块前后,这些方面的主要变化。我们先来看可靠的版本控制:

  • 引入前:在Go项目中指定依赖项的方式存在可解释的空间。你可能并不完全清楚自己请求的是某个包的哪个版本。由于这种模糊性,当你添加一个依赖项时,无法完全确定项目中会包含哪些代码。有可能引入意想不到的版本,甚至是不同的包。
  • 引入后:引入了语义化版本控制(Semantic Versioning,简称SemVer)的概念,确保你了解依赖项更新所包含的变更类型,减少不可预测的故障。

接下来看看可重现的构建:

  • 引入前:依赖外部包存储库意味着,如果依赖项发生变化或消失,后续的构建可能会失败。
  • 引入后:Go模块代理的引入以及将依赖项存储在项目内(vendoring,即把依赖项的副本存储在项目中)的功能,保证了你的代码始终以相同的方式进行构建。

最后,来看看管理依赖项膨胀:

  • 引入前:嵌套依赖项很容易失控,增加了项目的规模和复杂性。
  • 引入后:Go模块会计算所需依赖项的最小集合,使你的项目保持精简。

一个模块是一组相关的Go包。它是源代码的一个“可版本化”且可互换的单元。

模块有两个主要目标:维护依赖项的特定要求,以及创建可重现的构建。

我们先假设你正在整理一个图书馆。这个图书馆可以用来类比理解模块、版本控制和语义化版本控制。把一个模块想象成一套精彩的系列书籍。每一套系列书籍都是相关书籍的集合,逐卷发布。就像《哈利·波特》系列一样,每一本书都为整个故事增添内容,共同构成一个精彩且连贯的故事。现在,假设系列中的每一本书都列出了之前的所有卷,并指定了理解当前这本书所需的确切版本。这确保了你无论从哪一卷开始阅读这个系列,都能获得一致的阅读体验,就如同模块通过记录精确的依赖项要求来确保一致的构建一样。把版本控制存储库想象成图书馆里一个井然有序的书架。每个书架都存放着一整套系列书籍,摆放得整整齐齐,方便读者查找和按顺序阅读。

但它们之间是如何相互关联的呢?很简单:一个存储库就像是图书馆里专门存放特定系列或合集的区域。每个模块代表这个区域内的一套系列书籍。每套系列书籍(模块)由单独的书籍(包)组成。最后,每本书(包)包含章节(Go源文件),所有这些都在这本书的封面(目录)之内。

如果使用git,版本将与存储库的标签相关联。

语义化版本控制(SemVer)
语义化版本控制(主版本号.次版本号.修订号,Major.Minor.Patch)使用一种编号系统来表明软件更新中包含哪些类型的变更(重大变更、新功能、错误修复)。完整的语义化版本控制规范可在https://semver.org/上查看。

# 使用模块的常规流程

正常情况下的工作流程如下:

  1. 根据需要在你的.go文件中添加导入。
  2. go build或go test命令会自动添加新的依赖项以满足导入需求(自动更新go.mod文件并下载新的依赖项)。

有时,需要选择特定版本的依赖项。在这种情况下,应该使用go get命令。

go get命令的格式是<模块名称>@<版本>,例如:

go get foo@v1.2.3
1
直接更改模块文件
如有必要,也可以直接更改go.mod文件。但无论如何,最好还是让Go命令来对该文件进行更改。

下面我们来探索如何使用模块,首先创建我们的第一个模块。

# 创建新模块

我们先为项目创建一个新的Go模块。第一步是设置工作空间:

mkdir mybestappever
cd mybestappever
1
2

我们可以通过运行一个简单的命令来初始化模块:

go mod init github.com/yourusername/mybestappever
1

这个命令会创建一个go.mod文件,用于跟踪模块的依赖项。假设我们有一个新的main.go文件,内容如下:

package main

import (
    "context"
    "fmt"
    "time"
    "github.com/alexrios/timer/v2"
)

func main() {
    sw := &timer.Stopwatch{}
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    if err := sw.Start(ctx); err != nil {
        fmt.Printf("Failed to start stopwatch: %v\n", err)
        return
    }
    
    time.Sleep(1 * time.Second)
    sw.Stop()
    elapsed, err := sw.Elapsed()
    if err != nil {
        fmt.Printf("Failed to get elapsed time: %v\n", err)
        return
    }
    
    fmt.Printf("Elapsed time: %v\n", elapsed)
}
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

另外,假设我们有一个main_test.go文件,内容如下:

package main

import (
    "context"
    "testing"
    "time"
    "github.com/alexrios/timer/v2"
)

func TestStopwatch(t *testing.T) {
    sw := &timer.Stopwatch{}
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    if err := sw.Start(ctx); err != nil {
        t.Fatalf("Failed to start stopwatch: %v", err)
    }
    
    time.Sleep(1 * time.Second)
    sw.Stop()
    elapsed, err := sw.Elapsed()
    if err != nil {
        t.Fatalf("Failed to get elapsed time: %v", err)
    }
    
    if elapsed < 1*time.Second || elapsed > 2*time.Second {
        t.Errorf("Expected elapsed time around 1 second, got %v", elapsed)
    }
}
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

使用go test命令执行测试。运行该命令时,Go工具会自动解析任何新的依赖项,更新go.mod文件,并下载必要的模块。

如果你查看go.mod文件,应该会看到依赖项的新一行:

require github.com/alexrios/timer/v2 v2.0.0
1

# 理解模块版本控制

Go使用最小版本选择(MVS,Minimal Version Selection)算法来管理依赖项。最小版本选择算法确保构建过程使用满足go.mod文件要求的模块的最小版本。

当你构建或测试一个Go模块时,最小版本选择算法会根据你的模块及其依赖项的go.mod文件中指定的版本要求,来确定要使用的模块版本集。最小版本选择算法解析这些版本的过程如下:

  • 起点:该过程从你的模块的go.mod文件开始,这个文件指定了直接依赖项的版本。
  • 传播需求:最小版本选择算法读取直接依赖项的go.mod文件,收集它们的依赖项及其所需版本。这个过程会递归地应用于所有依赖项。
  • 选择版本:最小版本选择算法在所有提及某个模块的go.mod文件中,选择该模块的最高所需版本。最高所需版本被视为满足所有版本要求的最小版本。
  • 最小化版本:该算法确保选择每个模块的最小版本,也就是说,不会选择比必要版本更高的版本。这降低了引入意外变更或不兼容性的风险。

通过始终选择满足所有要求的最小版本,最小版本选择算法避免了不必要的升级,并降低了因依赖项更新而引入破坏性变更的风险。

要列出当前模块及其所有依赖项,可以使用go list命令:

go list -m all
1

输出将包括主模块及其依赖项:

github.com/yourusername/mybestappever
github.com/alexrios/timer/v2 v2.0.0
1
2

嘿!文件夹里有一个新文件:go.sum。

go.sum文件包含特定模块版本内容的校验和。这确保了在不同的构建过程中始终使用相同的模块版本。以下是go.sum文件中可能出现的内容示例:

github.com/alexrios/timer/v2 v2.0.0 h1:...
github.com/alexrios/timer/v2 v2.0.0/go.mod h1:...
1
2

# 更新依赖项

要将依赖项更新到最新版本,可以使用go get命令:

go get github.com/alexrios/timer@latest
1

要更新到特定版本,则明确指定版本:

go get github.com/yourusername/timer@v1.1.0
1

有时,你需要指定依赖项的确切版本。你可以直接在go.mod文件中进行设置,或者像前面展示的那样使用go get命令。这对于确保兼容性,或者需要特定版本的某些功能或错误修复时非常有用。

# 语义化导入版本控制

Go模块遵循语义化版本控制,它使用版本号来传达有关版本稳定性和兼容性的信息。版本编号方案是v<主版本号>.<次版本号>.<修订号>:

  • 主版本号表示不兼容的API变更。
  • 次版本号以向后兼容的方式添加功能。
  • 修订号包含向后兼容的错误修复。

例如,v1.2.3表示主版本号为1,次版本号为2,修订号为3。

当一个模块达到版本2或更高时,主版本号必须包含在模块路径中。例如,github.com/alexrios/timer的版本2被标识为github.com/alexrios/timer/v2。

你可以随时使用以下命令触发依赖项验证:

go mod tidy
1

Go中的go mod tidy命令对于维护整洁准确的go.mod和go.sum文件至关重要。它会扫描项目的源代码,以确定使用了哪些依赖项,将缺失的依赖项添加到go.mod文件中,并删除其中未使用的依赖项。此外,它还会更新go.sum文件,以确保所有依赖项的校验和一致。这个过程有助于使项目摆脱不必要的依赖项,降低安全风险,确保可重现的构建,并使依赖项管理更加易于操作。定期使用go mod tidy可确保Go项目的依赖项是最新的,并且准确反映代码库的需求。

github.com/alexrios/timer库是公共库,但在公司中,你可能会使用闭源库,通常称为私有库。下面我们来看看如何使用它们。

# 使用私有库

当使用托管在私有仓库中的Go模块时,你需要进行一些设置,以确保能够安全、直接地访问,绕过公共的Go模块代理。本节将指导你配置Git和Go环境,以便在GitHub上无缝使用私有Go模块。

首先,你需要导航到主目录并打开.gitconfig文件,添加以下配置:

[url "ssh://git@github.com/"]
insteadOf = https://github.com/
1
2

这些行告诉Git,每当遇到以https://github.com/开头的GitHub URL时,自动使用SSH(ssh://git@github.com/)。

完成上述操作后,我们现在可以为私有模块配置Go环境。

GOPRIVATE环境变量可防止Go工具尝试从公共Go模块镜像或代理获取其中列出的模块。相反,它会直接从源获取这些模块,这对于私有模块来说是必要的。

你可以通过在终端中运行以下命令,为单个私有仓库设置GOPRIVATE,将<org>和<project>替换为你的GitHub组织和仓库名称:

go env -w GOPRIVATE="github.com/<org>/<project>"
1

或者,为组织中的所有仓库设置GOPRIVATE。如果你在同一组织下使用多个私有仓库,使用通配符(*)来涵盖所有仓库会很方便:

go env -w GOPRIVATE="github.com/<org>/*"
1

以下是一些额外的提示:

  • 验证你的GOPRIVATE设置:使用go env GOPRIVATE来显示当前设置。
  • 在GitHub上使用SSH密钥:确保已设置SSH密钥并将其添加到GitHub账户。这样的设置可让你在推送和拉取仓库内容时,无需每次都输入用户名和密码。
  • 故障排除:如果你在访问私有仓库时遇到问题,请验证SSH密钥是否已加载到SSH代理中。你可以通过运行ssh-add ~/.ssh/your-ssh-key(将your-ssh-key替换为你的SSH密钥路径)将SSH密钥添加到SSH代理。

你已经配置好了环境,可以安全地使用托管在GitHub私有仓库中的Go模块了!这种设置通过自动化Git操作的身份验证,确保了对私有Go模块的直接、安全访问,从而简化了开发工作流程。

# 版本控制和go install

最基本的方法是将Go程序代码托管在公共版本控制仓库(如GitHub、GitLab等)中。安装了Go的用户可以使用go install命令,后跟你的仓库URL,来获取、编译和安装你的程序。

在模块的根目录下,我们可以简单地执行以下命令:

go install
1

这样,在系统的任何目录中,你只需在终端中输入程序名称,就可以访问该程序。要测试安装是否成功,打开一个新的终端窗口并输入以下内容:

hello
1

使用go install时,编译后的程序会存放在$GOPATH/bin目录下。

从Go 1.16版本开始,go install命令可以安装特定版本的Go可执行文件。为了便于理解,我们以https://github.com/alexrios/endpoints仓库为例。

这个仓库有一个v0.5.0标签,所以要安装这个特定版本,可以运行以下命令:

go install github.com/alexrios/endpoints@v0.5.0
1

当你不知道或不想去了解可执行文件的最新版本时,你可以简单地使用latest:

go install github.com/alexrios/endpoints@latest
1

请注意,在Go生态系统中,你无需任何特殊工具或流程就可以安装其他可执行文件。很强大,对吧?

但是对于包含多个模块的项目呢?我们处理起来会有多容易呢?简单回答:在Go 1.18版本之前,一点都不容易。

我们先不讲Go早期那些复杂的情况和让人头疼的问题了,让我们“回到未来”,专注于现在如何轻松处理这类项目。Go 1.18版本引入了模块工作区(module workspaces)的概念。

# 模块工作区(Module workspaces)

Go模块工作区是一种将属于同一个项目的多个Go模块组合在一起的方式。这个功能的出现是为了解决依赖管理这一难题,它让开发者能够同时处理多个模块。这不仅能让项目结构更整洁,还从根本上改变了Go工具链解析依赖的方式。

Go工作区是一个包含唯一的go.work文件的目录,该文件引用了一个或多个go.mod文件,每个go.mod文件代表一个模块。这种设置让我们能够构建、测试和管理多个相互关联的模块,而不会出现常见的版本冲突问题。

在工作区内,Go编译器将这些模块视为同级模块,而不是为每个模块依赖外部的go.mod文件。它会查看工作区的go.work文件,该文件列出了项目中的所有模块,确保各个模块能协同工作。

换句话说,工作区为你的项目创建了一个自包含的生态系统。你在一个模块中所做的任何更改会立即影响到其他模块。这大大简化了开发过程,尤其是在处理大型应用中相互关联的组件时。

话不多说,让我们来看看实际操作。考虑以下简单的工作区设置:

go work init ./myproject
go work use ./moduleA
go work use ./moduleB
1
2
3

go.work文件在这个工作区中起着协调作用。它确保当你运行Go命令时,所有被引用的模块都被视为一个统一代码库的一部分。当你开发相互依赖的模块,或者想要在不将本地更改提交到上游的情况下跨模块测试这些更改时,这一功能特别有用。

通过这种配置,moduleA和moduleB都属于同一个工作区,实现了无缝的集成测试和开发。

其实际意义非常深远。假设你有两个模块:moduleA和moduleB。moduleA依赖于moduleB。按照传统方式,更新moduleB可能会陷入版本固定和向后兼容性的困境。然而,使用工作区,你可以同时修改这两个模块,并实时测试它们的集成情况。

下面是一个简单的go.work文件示例:

go 1.21
use (
    ./path/to/module-a
    ./path/to/module-b
)
1
2
3
4
5

使用模块工作区的最后一点是同步工作区内的修改。每次我们修改模块中的go.mod文件,或者在工作区中添加或删除模块时,都应该运行以下命令:

go work sync
1

这个命令可以避免出现以下问题:

  • 不一致性:你对go.mod文件所做的更改可能与go.work文件不同步。这种差异可能会导致混淆和潜在错误。
  • 意外的构建/测试行为:当你运行诸如go build或go test之类的命令时,根据Go解析依赖的方式,可能会出现以下几种情况:
    • Go可能忽略你的更改:如果根据go.work文件所需的版本已在本地缓存,Go可能会使用缓存版本,而不是你在修改后的go.mod中指定的版本。
    • 构建可能失败:如果你的更改引入了不兼容的依赖版本,或者所需版本不可用,你的构建和测试可能会失败。
    • 协作问题:如果你和其他人一起参与一个项目,不一致的依赖声明可能会导致在不同机器上构建项目时出现问题。

如果你忘记运行这个命令,可能会浪费时间去调试因依赖不一致导致的意外构建失败。此外,如果你忘记手动修改过go.mod文件,可能很难找到问题的根本原因。从团队协作的角度来看,当不同开发者的环境中依赖状态不同时,项目维护会变得更加困难。

这种模块管理方法不仅能让你保持头脑清醒,还能营造一个不会因后勤问题阻碍创新的环境。所以,下次当你在处理Go模块时,请记住,工作区是你的好帮手,而不是给项目增加复杂性的负担。

虽然我们对模块的了解已经可以接受检验了,但我们还是希望确保一切都能顺利运行,并且(希望)没有漏洞。这时候就需要持续集成(CI)了。

# 持续集成(CI)

持续集成就像是你代码的保姆。哈!如果你真这么认为,那你可能从来没有尝试过在努力让一群不受控制的微服务井然有序的同时,还要应对一个比撒哈拉沙漠里的冰棍融化得还快的基础设施。

说实在的,持续集成更像是在对付一只多头水螅:一个头吐出单元测试,另一个头吐出集成测试,在这一团乱麻中,可能还隐藏着构建管道和部署环节。正确实施的持续集成与其说是在照顾代码,不如说是一场残酷的代码训练营,有一个毫不留情的教官。

那么,那些潮人所说的CI到底是什么呢?在Go语言的领域里,尤其是对于系统编程而言,持续集成是一种不断将代码更改合并到共享存储库中,并对结果进行严格测试的技术。它的目的是尽早发现错误,确保新代码不会破坏整个系统,并且自动化那些繁琐的任务,否则这些任务会让我们对着键盘痛哭。

把持续集成想象成你的自动化代码质量控制。在这里,你可以放入构建脚本和测试套件,也许还可以加上一些静态分析和代码检查(linting),然后将它们连接在一起,以便每次有人提交更改时都能运行。你可能会问,为什么要这样做呢?因为在代码库中,没有什么比反复破坏代码并迫使你的队友去修复它更能塑造代码的“品格”了。

让我们来实际操作一下。下面是一个使用GitHub Actions为你的Go项目进行基本持续集成设置的代码片段:

name: Go CI on Commit
on:
  push:
jobs:
  test-and-dependencies:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: '^1.21'
    - name: Get dependencies
      run: go mod download
    - name: Run tests
      run: go test -v ./...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 缓存(Caching)

利用缓存机制,就好比从看着构建过程像生锈的蒸汽引擎一样缓慢运行,转变为见证高速列车般的高效运行。让我们看看如何让依赖下载成为过去式。

想象一下,你的持续集成管道就像不知疲倦的工厂工人,而那些依赖项就像是让生产持续运转所需的原材料。每次构建过程启动时,都必须重新获取所有依赖项,这不仅会减慢速度,还会产生麻烦。缓存就像是在工厂旁边建造了一个储备充足的仓库 —— 下次你需要那些材料时,无需长途跋涉,只需快速前往仓库即可。

在持续集成中缓存Go依赖项的关键在于理解两点:

  • 依赖项的存储位置:Go将下载的依赖项存储在Go模块缓存中,通常位于~/go/pkg/mod目录下。这就是我们的“仓库”。
  • 持续集成工作流程如何存储数据:大多数持续集成系统,如GitHub Actions,都有内置的机制来在工作流程运行之间缓存文件或目录。

让我们将这些概念结合起来。以下是如何修改基本的GitHub Actions工作流程以缓存Go依赖项:

name: Go CI on Commit

on:
  push:

jobs:
  test-and-dependencies:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: '^1.21'

    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-

    - name: Get dependencies
      run: go mod download
    - name: Run tests
      run: go test -v ./...
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

神奇的事情发生在“缓存Go模块”这一步:

  • actions/cache@v3:这是GitHub Actions中用于缓存的可靠工具。
  • path:我们告诉它要缓存Go模块目录。
  • key:这个唯一标识你的缓存。注意,它包含了go.sum文件的哈希值;如果依赖项发生变化,就会创建一个新的缓存。
  • restore-keys:在找不到确切的缓存键时提供一个备用方案。

可以这样理解缓存:你的持续集成管道每次运行时都会留下一些“面包屑”。下次运行时,它会检查这些“面包屑”,如果找到,就会获取预先打包好的依赖项,而不是去重新下载。

# 静态分析(Static analysis)

静态分析工具就像是一个自动化的代码审查团队,不知疲倦地检查你的Go代码中是否存在潜在的问题、是否偏离最佳实践,甚至是一些细微的风格不一致之处。这就好比有一群一丝不苟的程序员一直在你身后看着你写代码,但又不会让你有那种被人盯着写代码的尴尬感觉。

让我们将Staticcheck(https://staticcheck.dev/ (opens new window))这个严谨的代码检查工具集成到你的Go持续集成工作流程中,帮助你保持代码的高质量。

Staticcheck不仅仅进行基本的代码检查(linting),它会更深入地挖掘,识别潜在的漏洞、低效的代码模式,甚至是那些可能影响Go代码质量的细微风格问题。它就像是你的自动化代码侦探,不知疲倦地寻找那些可能在粗略检查中被忽略的问题。

让我们利用GitHub Actions和dominikh/staticcheck-action这个动作(action),简化我们在工作流程中的集成过程。

虽然你可以在工作流程中直接安装和执行Staticcheck,但使用预先构建好的GitHub动作有几个优点:

  • 简化设置:该动作会为你处理安装和执行的细节,降低工作流程配置的复杂性。
  • 潜在的缓存功能:一些动作可能会自动缓存Staticcheck的结果,以加快未来的运行速度。
  • 社区驱动:许多动作都在被积极维护,确保与最新的Go和Staticcheck版本兼容。

以下是使用这个动作后,修改后的工作流程:

name: Go CI
on: [push]
jobs:
  build-test-staticcheck:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: dominikh/staticcheck-action@v1.2.0
      with:
        # Optionally specify Staticcheck version:
        # version: latest
1
2
3
4
5
6
7
8
9
10
11

要点如下:

  • 使用简便:注意集成过程有多简洁;你主要是引用这个动作(dominikh/staticcheck-action),还可以选择自定义其版本。
  • 版本控制:固定动作版本(v1.2.0)可确保在持续集成运行过程中的行为一致。 | staticcheck-action配置
    有关支持的选项,如指定Staticcheck版本,请查阅该动作的文档(https://github.com/dominikh/staticcheck-action (opens new window))。 | | ------------------------------------------------------------ |

# 发布你的应用程序

当然,你的单元测试非常出色,甚至可能已经接近代码覆盖率的理想状态。但真正的考验在于,当你必须将自己创建的杰作打包并发布到外部时 —— 这时GoReleaser(https://goreleaser.com/ (opens new window))就登场了,它能将你的发布过程从一场令人尴尬的折磨,转变为一场自动化的交响乐。

忘掉为每一个操作系统构建二进制文件,也别再为tar包和校验和而苦恼了。想象一下这样一个世界,你的发布难题就像一场和谐的编码会话一样,没有任何问题出现,如同神话一般。欢迎来到交叉编译、自动版本标记、Docker镜像创建、Homebrew taps的领域…… GoReleaser可不只是一个工具,它是你发布日保持理智的法宝。

从本质上讲,GoReleaser就像是你个人的发布管家。你只需描述希望如何打包和分发你宝贵的Go应用程序,它就会像经验丰富的装配线一样高效处理那些繁琐的细节。需要在GitHub上发布一个闪亮的新版本,包含针对所有已知操作系统预先构建的二进制文件?没问题。想要将Docker镜像推送到你喜欢的注册表?轻而易举。

builds:
- ldflags:
  - -s -w
  - -extldflags "-static"
  env:
  - CGO_ENABLED=0
  goos:
  - linux
  - windows
  - darwin
  goarch:
  - amd64
  mod_timestamp: '{{ .CommitTimestamp }}'
archives:
- name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
  wrap_in_directory: true
  format: binary
  format_overrides:
  - goos: windows
    format: zip
    
dockers:
- image_templates:
  - "ghcr.io/alexrios/endpoints:{{ .Tag }}"
  - "ghcr.io/alexrios/endpoints:v{{ .Major }}"
  - "ghcr.io/alexrios/endpoints:v{{ .Major }}.{{ .Minor }}"
  - "ghcr.io/alexrios/endpoints:latest"
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

太棒了!现在,我们希望每次给存储库打标签时,都能提供我们的二进制文件。为了实现这一点,我们应该使用一个包含GoReleaser任务的新工作流程:

name: GoReleaser
on:
  push:
    tags:
    - '*'
jobs:
  goreleaser:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: write
    steps:
    - name: Checkout
      uses: actions/checkout@v2
      with:
        fetch-depth: 0
    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.21
    - name: Run GoReleaser
      uses: goreleaser/goreleaser-action@v2
      with:
        version: latest
        args: release --rm-dist --clean
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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

我还记得有一次,我花了整个下午手动编写发布说明,祈祷不要遗漏任何关键的漏洞修复。有了GoReleaser,我可以嘲笑过去的自己有多痛苦了。它能轻松自动生成发布说明,发布推文进行公告,如果你好好请求的话,说不定还会烤庆祝饼干呢(好吧,也许不会烤饼干)。

就像一位睿智的老将军视察部队一样,我在实践中深刻体会到,一个稳固的持续集成设置对于产出无漏洞代码来说是非常有价值的。在持续集成出现之前的“史前时代”,我们常常要花几天甚至几周的时间,去梳理那些在发布前才暴露出来的、极其复杂的集成问题。持续集成就是应对这种混乱局面的解药。

让我们把持续集成想象成一场高风险的同步舞蹈:持续进行小而频繁的更改就像是优雅的华尔兹。而大的、不频繁的合并呢?那就像一场混乱的狂欢舞池,没有人能毫发无损地从中走出来。

# 总结

在本章中,我们探讨了分发Go应用程序的方法和工具。我们重点关注了依赖项的精细管理、测试和集成过程的自动化,以及高效的软件发布策略。我们从Go模块和工作区开始,讨论了它们如何通过更好的依赖管理来提高项目的一致性和可靠性。然后,我们探索了持续集成及其在保持软件高质量方面的关键作用。最后,我们介绍了使用GoReleaser部署应用程序的要点,它通过自动化跨不同平台的打包和分发,简化了发布过程。这些都是关键的概念和工具,将构成你顶点项目(Capstone project)的基础。

在下一章迈向顶点项目的过程中,你将有机会应用本书中所学的所有知识和技能。这个最终项目旨在巩固你在实际场景中的理解和熟练程度,挑战你运用书中讨论的最佳实践和工具,从头到尾实现一个完整的解决方案。

顶点项目将见证你的学习历程和努力成果,展示你有效开发、管理、自动化和发布强大应用程序的能力。我对此感到很兴奋,你呢?

第11章 遥测技术
第五部分:深入探索

← 第11章 遥测技术 第五部分:深入探索→

最近更新
01
第一章 auto与类型推导
03-27
02
第二章 关键字static及其不同用法
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式