Go 语言-如何解决包的依赖管理(应用的构建)

476 篇文章 35 订阅
187 篇文章 6 订阅

当我们开始入门Go语言时,常常会会遇到程序无法运行的情况,这个时候千万不要慌,这个时候就引出了我今天要讲的主角-Go module。

在这篇博客中,我将围绕Go语言的构建模式展开来说,一步一步带你理解Go语言是如何进行应用的构建的。

我会重点讲解现在应用最为广泛的构建模式,Go Module的基本概念和应用构建方式。之后,我将以一个实际的案例去演示Go Module如何操作,在操作的基础上结合原理去更加深入的去理解Go Module的构建模式。

在此之前,我们需要了解一下Go语言构建模式的发展的过程,以及为什么现在选择Go module作为主流的构建模式。

Go语言构建模式的发展的过程

所有的Go语言的程序都会组织若干组文件,每组文件被称为一个包。Go语言的整个构建过程就是确定包名、包版本、编译包以及得到的目标文件链接起来的过程。

一开始Go语言的构建方式并不是Go Module,最早期是GOPATH、1.5版本出了Verdor机制,以及现在主流的Go Module。下面我将先介绍一下前面两个。

GOPATH

这个构建模式是在Go语言诞生之初就有的。在这种模式下面,Go语言的编译器可以在本地GOPATH环境变量配置的路径下,去搜寻Go程序依赖的第三方包。如果找到了对应的包我们就可以去编译;但是如果没有,则在编译时就会发生报错的情况。

go get

这个时候如果第三方包没有,我们可以通过go get的方式去下载第三方的依赖包。不过随着项目增多,GOPATH的一个缺陷也暴露出来了,它只能解决依赖包有没有的问题,但是版本控制几乎没有做。由于后续依赖包不断更新,在再次构建的过程中可能会出现不兼容代码,导致程序无法通过编译。

上面的提到的GOPATH的构建方式不能满足多项目、第三方依赖包多版本的问题。导致Go项目会受到依赖包版本的影响导致编译无法通过。于是Go语言核心团队推出了Vendor机制,试图去解决依赖包的版本问题。

Go Vendor

在Go 的1.5版本中推出了Vendor机制。它的本质就是在Go项目的某一个特定的目录下,将项目的所有依赖包缓存起来,这个目录的名称就是Vendor。

在Vendor机制出现后,Go的编译器就有优先去找Vendor目录下缓存的第三方依赖包,而不是像GOPATH一样,去所配置的路径下去寻找。这样,无论第三方依赖包如何变化,无论GOPATH中有没有第三方包,都不会影响到Go语言的构建。

单独的Vendor目录还有一个好处就是可以将目录下的依赖包连同项目代码一起提交给代码仓库,在其他开发者拿到项目代码后,可以根据Vendor目录,直接进行构建。

但是在使用Vendor之前,Go语言的项目必须位于GOPATH环境变量配置的某个路径的src下,如果不满足这个条件,那么Vendor目录在Go编译时,就不会生效,它还是会到GOPATH中去寻找。

随着项目越来越多,Vendor目录中的依赖包也可以变得难以去管理,有时还需要手动管理这些依赖包,比如某个项目的依赖包分析、版本的记录、依赖包获取和存放等等,都是令人头大的问题。

这个阶段出现的问题就是如何对众多的依赖包进行管理,这里终于可以来讲一下我们今天要讲的主角了——Go Module。

Go Module

什么是Go Module

Go Module是Go语言的官方包管理和版本控制体系,它从Go 1.11版本开始引入,并在Go 1.14之后成为生产环境的标准依赖管理方式。Go Module旨在解决长期以来Go项目中的依赖管理问题,如版本不一致、依赖包管理难等问题,同时“淘汰”了对传统GOPATH工作区的依赖,使得项目组织更加灵活和独立。

Go Modules通过一个名为go.mod的文件来记录项目的直接依赖及其版本信息,确保任何人在任何地方构建项目时都能获取到相同版本的依赖,从而实现构建的一致性和可复现性。此外,go.sum文件记录了所有直接和间接依赖的校验和,确保依赖的完整性。

这个构建模式下,就相当于每个项目都有一个对应的Go Modules。而这个Go Modules的顶层目录中就会放一个go.mod的文件,每个go.mod文件只会定义为唯一一个Module,也就是说Modules与go.mod是一一对应的。

go.mod文件所在顶目录也被称为module的根目录,module根目录以及它子目录下所有Go包均属于这个Go Module,这个module也被称为main module。

听了上面的一连串解释你有可能还没有明白Go modules的原理,但是也不需要慌张,现在我们就基于Go module的构建模式去创建一个Go 项目。

创建一个GO项目(Gin Web)

这里我将使用Go语言Gin框架来进行实验。

第一步,我们先用vscode或者goland IDE在一个名叫Gin的空白目录下新建一个main.go文件

package main  
  
import "github.com/gin-gonic/gin"  
  
func main() {  
 r := gin.Default()  
 r.GET("/ping", func(c *gin.Context) {  
  c.JSON(200, gin.H{  
   "message": "pong",  
  })  
 })  
 r.Run() // 监听并在 0.0.0.0:8080 上启动服务  
}  

这个时候我们直接go run main.go 会在终端中发现报错,运行不了。

main.go:3:8: no required module provides package github.com/gin-gonic/gin: go.mod file not found in current directory or any parent directory; see 'go help modules'  

这个就是因为我们创建的目录不在GOPATH目录的下的src,程序也就无法找到对应的gin的第三方依赖包。

第二步,在终端中输入go mod init 项目名,创建go,mod文件,让项目变成Go modules构建的。

这里我就用gin作为项目名了。

>go mod init gin  
go: creating new go.mod: module gin  
go: to add module requirements and sums:  
        go mod tidy  

这里输入完之后,在main.go文件的同级目录下生成了一个go.mod文件。此时的文件内容:

module gin  
  
go 1.22.3  
  

这里我们看到了module的名称就是我们输入的项目名,同时也包括了go的版本。

go mod init 命令还输出了两行日志,提示我们可以使用 go mod tidy 命令,添加 module 依赖 以及校验和。go mod tidy 命令会扫描 Go 源码,并自动找出项目依赖的外部 Go Module 以及 版本,下载这些依赖并更新本地的 go.mod 文件。我们按照这个提示执行一下 go mod tidy 命 令:

> go mod tidy  
go: finding module for package github.com/gin-gonic/gin  
go: downloading github.com/gin-gonic/gin v1.10.0  
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.10.0  
go: downloading github.com/gin-contrib/sse v0.1.0  
go: downloading golang.org/x/net v0.25.0  
go: downloading github.com/stretchr/testify v1.9.0  
go: downloading google.golang.org/protobuf v1.34.1  
go: downloading github.com/go-playground/validator/v10 v10.20.0  
go: downloading github.com/pelletier/go-toml/v2 v2.2.2  
go: downloading github.com/ugorji/go/codec v1.2.12  
go: downloading github.com/bytedance/sonic v1.11.6  
go: downloading github.com/goccy/go-json v0.10.2  
go: downloading golang.org/x/sys v0.20.0  
go: downloading github.com/gabriel-vasile/mimetype v1.4.3  
go: downloading github.com/go-playground/universal-translator v0.18.1  
go: downloading github.com/leodido/go-urn v1.4.0  
go: downloading golang.org/x/crypto v0.23.0  
go: downloading golang.org/x/text v0.15.0  
go: downloading github.com/go-playground/locales v0.14.1  
go: downloading github.com/cloudwego/base64x v0.1.4  
go: downloading golang.org/x/arch v0.8.0  
go: downloading github.com/bytedance/sonic/loader v0.1.1  
go: downloading github.com/twitchyliquid64/golang-asm v0.15.1  
go: downloading github.com/klauspost/cpuid/v2 v2.2.7  
go: downloading github.com/cloudwego/iasm v0.2.0  
go: downloading github.com/go-playground/assert/v2 v2.2.0  
go: downloading github.com/google/go-cmp v0.5.5  
go: downloading golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543  

它会自动将程序中gin框架的所需要的依赖包下载下来,这个时候我们观察go.mod文件:

module gin  
  
go 1.22.3wype v1.4.3 // indirect  
 github.com/gin-contrib/sse v0.1.0 // indirect  
 github.com/go-playground/locales v0.14.1 // indirect  
 github.com/go-playground/universal-translator v0.18.1 // indirect  
 github.com/go-playground/validator/v10 v10.20.0 // indirect  
 github.com/goccy/go-json v0.10.2 // indirect  
 github.com/json-iterator/go v1.1.12 // indirect  
 github.com/klauspost/cpuid/v2 v2.2.7 // indirect  
 github.com/leodido/go-urn v1.4.0 // indirect  
 github.com/mattn/go-isatty v0.0.20 // indirect  
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect  
 github.com/modern-go/reflect2 v1.0.2 // indirect  
 github.com/pelletier/go-toml/v2 v2.2.2 // indirect  
 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect  
 github.com/ugorji/go/codec v1.2.12 // indirect  
 golang.org/x/arch v0.8.0 // indirect  
 golang.org/x/crypto v0.23.0 // indirect  
 golang.org/x/net v0.25.0 // indirect  
 golang.org/x/sys v0.20.0 // indirect  
 golang.org/x/text v0.15.0 // indirect  
 google.golang.org/protobuf v1.34.1 // indirect  
 gopkg.in/yaml.v3 v3.0.1 // indirect  
)  

将gin框架所需的依赖包以及以来版本都展示在这个文件中。

第三步,我们重新去执行main.go

GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.  
  
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.  
 - using env: export GIN_MODE=release  
 - using code: gin.SetMode(gin.ReleaseMode)  
  
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)  
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.  
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.  
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default  
[GIN-debug] Listening and serving HTTP on :8080  
  

发现gin web已经成功启动了。

到这里,我们已经完成了一个基于Go Module机制完成项目构建了,但是现在还有一个疑问产生了,项目中第三方依赖包本身就存在许多版本,Go module是如何选出最合适的版本呢?如果想知道答案,我们就需要深入得去理解Go Module构建模式的工作原理,明白了原理,自然答案就出来了。

深入理解Go Module的构建模式

Go语言使用Go Module构建模式就是用来解决“包依赖管理”的问题,其中有几个创新点,这里需要提到语义导入版本 (Semantic Import Versioning),以及和不用于其他语言的最小版本选择 (Minimal Version Selection) 等机制。下面我将告诉你这些机制的底层原理,相信你看过之后一定能真正掌握Go Module的构建模式。

Go Module 语义导入版本机制

从上面Gin项目中的go.mod的require段中依赖的版本号,都符合vX.Y.Z.D的格式。它就是符合Go Module的版本号,由一个前缀v和一个次版本号组成了语义版本号规范。

版本标识模块的不可变快照,该快照可以是发布版,也可以是预发布版。每个版本都以字母 v 开头,后跟一个语义版本。请参阅语义版本控制 2.0.0,了解有关如何格式化、解释和比较版本的详细信息。

https://semver.org/spec/v2.0.0.html

总而言之,语义版本由三个非负整数(主要、次要和补丁版本,从左到右)组成,用点分隔。补丁版本后面可能跟着一个以连字符开头的可选预发布字符串。预发布字符串或补丁版本后面可能跟一个以加号开头的构建元数据字符串。例如, v0.0.0 、、 v1.12.134 v8.0.5-prev2.0.9+meta 是有效版本。

下面是一些版本更新的要求:

  • 在对模块的公共接口或记录的功能进行向后不兼容的更改后(例如,在删除软件包之后),必须递增主要版本,次要版本和补丁版本必须设置为零。

  • 在向后兼容的更改(例如,添加新功能后)后,必须递增次要版本并将修补程序版本设置为零。

  • 在不影响模块公共接口的更改(例如错误修复或优化)后,必须递增补丁版本。

  • 预发行后缀表示版本是预发行版。预发布版本在相应的发布版本之前排序。例如, v1.2.3-prev1.2.3 .之前。

  • 为了比较版本,将忽略生成元数据后缀。在版本控制存储库中,将忽略带有生成元数据的标记,但将保留 go.mod 在文件中指定的版本中。后缀 +incompatible 表示在迁移到模块版本主要版本 2 或更高版本之前发布的版本(请参阅与非模块存储库的兼容性)。

不过要注意的是,按照语义版本的规范,主版本号不同的两个版本是相互不兼容的。而且,在主版号相同的情况下,次要版本号大一定是向后兼容次要版本号小的版本,换句话说次要版号更新,是不会影响项目的正常使用的。补丁版本号同理也不影响兼容性。

另外Go Module规定:如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的。那么我们该如何理解呢?

我们可以就以Gin为例子,它发布很多版本,我们从中选择了v1.10.0和v1.9.1这两个近期更新的版本。按照上面的语义版本规则我们一步一步看,首先,可以看到它们两个的版本号是一样的,新版本v1.10.0是兼容老版本v1.9.1的。那么,我们就可以得出如果一个项目依赖gin包,无论它使用的是v1.10.0还是v1.9.1版本,它都可以使用下面的包导入语句导入gin包:

import "github.com/gin-gonic/gin"  

上面的例子就是主版号一样的情况,那么随着gin包的不断升级,总有一天它的版本号会变为v2.0.0版本。根据语义版本的规则,我们可以得到主板号为2,这个时候已经和上面两个主版号1不同了,这个时候v2.0.0版本就不会兼容前两个版本了。那我们按照Go Module的方式就没办法以相同的路径去导入gin v2.0.0版本了,这个时候我们应该如何进行引入呢?

Go Module给出了一个方法:可以将包的主版本号引入到包导入路径之中,这里我拿gin包举例子,不过现在v2版本还是没有出的哦。

import "github.com/gin-gonic/gin/v2"  

这样一来就可以同时导入同一个包的两个不兼容版本

import (  
 "github.com/gin-gonic/gin"  
 ginv2 "github.com/gin-gonic/gin/v2"  
 )  

不过到这里,你可能会问,v0.y.z 版本应该使用哪种导入路径呢?

按照语义版本规范的说法,v0.y.z 这样的版本号是用于项目初始开发阶段的版本号。在这个阶段任何事情都有可能发生,其 API 也不应该被认为是稳定的。Go Module 将这样的版本 (v0) 与主版本号 v1 做同等对待,也就是采用不带主版本号的包导入路径,这样一定程度降低了 Go 开发人员使用这样版本号包时的心智负担。

Go 语义导入版本机制是 Go Module 机制的基础规则,同样它也是 Go Module 其他规则的基础。

那么接下来我将要讲解关于Go语言的最小版本选择原则。

Go Module 最小版本选择原则

在上面的gin框架的例子中,只有一个gin包,项目中没有其他的第三方依赖包了。但是随着项目的不断的推进,我们会遇到一种两个依赖包之间没有共同依赖的情况,这种是较好解决的情况;但如果遇到的依赖包按照下图展示的这样那我们应该如何去选择哪个版本呢?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从上图可以观察到Go Project是直接依赖于A,B两个包,并且它们两个包都与同一个依赖包C,但是A依赖C的v1.5.0版本,而B依赖C的v1.3.0版本,同时现在C 的最新版本已经更新到了v1.7.6版本,这个时候一下子就出现了三个不同的版本包,Go 语言在Go module构建模式下应该如何去选择包版本这是我们应该思考的问题。

对于其他语言来看,一般它们会直接按照最新的版本去选择,这样的机制叫做“最新最大版本”。这样应该是最稳定和安全的版本,并且应该有向后兼容性。至少在相同的主版本依赖树中是可以做到的。

这个时候我们再来看Go 语言本身,它是按照“最小版本”原则进行,因为Go在开发时考虑到最新最大版本的稳定和安全,另外还考虑到Go module本身的需求,首先我们看A要求的是C v1.50,B的要求是C v1.3.0,C 的v1.7.6根本没有考虑。Go根据项目中最小版本选择的原则来进行选择。

对于上面的项目中C v1.3.0是整个项目中要求的版本最小的,所有对于Go来说,它会直接选择C v1.3.0。并且,Go 团队认为“最小版本选择”为 Go 程序实 现持久的和可重现的构建提供了最佳的方案。

说了这么多,对于上面的语义导入版本与最小版本选择两种机制应该会有一个更深的理解了吧。对于前面遗留的问题利用上面讲的两个机制就可以清楚的回答出来了。

黑客&网络安全如何学习

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

1.学习路线图

攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去就业和接私活完全没有问题。

2.视频教程

网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己录的网安视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。

内容涵盖了网络安全法学习、网络安全运营等保测评、渗透测试基础、漏洞详解、计算机基础知识等,都是网络安全入门必知必会的学习内容。

(都打包成一块的了,不能一一展开,总共300多集)

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

3.技术文档和电子书

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本,由于内容的敏感性,我就不一一展示了。

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

4.工具包、面试题和源码

“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。

还有我视频里讲的案例源码和对应的工具包,需要的话也可以拿走。

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。

这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。

参考解析:深信服官网、奇安信官网、Freebuf、csdn等

内容特点:条理清晰,含图像化表示更加易懂。

内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值