GO依赖管理,看这篇就够了

2024-02-21 11:15:59 浏览数 (1)

大家好,我是「云舒编程」,今天我们来聊聊GO依赖管理。

前言

Golang在项目早期只是单纯的使用GoPath进行依赖管理,但是GoPath无法管理同一个依赖的不同版本,并且由于把所有的依赖都放在同一个路径下,对于多项目的依赖管理非常不方便,于是增加了vendor,运行把依赖和项目放在一起,但是依旧没有解决版本问题,导致依赖关系不清楚,升级困难。在这段期间,也出现了很多第三方依赖管理工具,有点百家争鸣的意思。 直到Go 1.11官方才推出了依赖管理工具Go Module,才统一了六国,正式进入了“书同文 车同轨”的时代。

一、GoPath

GOPATH 是什么?

在Golang中存在两个重要目录,分别是GOROOT、GOPATH。

  1. GOROOT:Go 语言安装目录,属于 Go 语言顶级目录。
  2. GOPATH:用户工作空间目录,属于用户域范畴。

实际 Go 项目有一个或者多个 package 组成,package 按照来源可能分为:标准库、第三方库、项目私有库。标准库全部位于 GOROOT 目录中,而第三方库和私有库,都位于 GOPATH 目录。

GOPATH如何管理依赖?

当项目需要依赖package时,GoPath会优先在GOROOT中寻找同名包,如果在GOROOT找到了那么就会停止,否则就会继续在GOPATH中寻找。这样就会导致如果GOROOT、GOPATH存在同名包,那么GOROOT就会覆盖GOPATH中的包。

GOPATH缺点?

依赖包没有版本可言,都是指master最新代码。 如果不同项目想使用同一个包的不同版本,那么就无法实现。例如A项目想使用X包的v1版本,B项目想使用X包的v2版本,在GoPath中是无法实现的。

开始Golang这么设计是有原因的,因为Google是一个实践Mono Repo(把所有的相关项目都放在一个仓库中)的公司。但更多的公司和组织更多的是用Multi Repo(按模块分为多个仓库),GOPATH 至少解决了第三方源码依赖的问题,虽然它还不够完美。

二、vendor

上面有提到GoPath的问题是无法做到不同项目的依赖隔离,并且由于每次构建都有可能触发依赖包的更新,如果三方依赖包存在 bug 或不向下兼容,将直接影响 Golang 程序的稳定性。为了解决这些问题,于是在 Golang v1.5 版本中引入 vendor 机制。

所谓 vendor 机制,就是在不同的Golang项目的目录中,创建一个目录名为vendor的目录,将Golang项目的所有依赖包缓存到该目录中。

Golang 程序在编译时,Golang 编译器会优先在 vendor 目录中查找 Golang 程序依赖的三方包,而不是在 GOPATH 环境变量配置的本地路径下查找。

我们只需将 vendor 目录一起提交到代码仓库中,这样构建项目时就不会改变三方依赖包的版本。

但是随着项目不断迭代,依赖的三方包会越来越多,vendor目录会变得越来越大,将vendor目录提交到代码仓库,还会影响代码的下载速度。同时,vendor目录中的三方依赖包,也需要我们手动管理,比如手动记录依赖三方包的版本号,手动下载三方依赖包等。

三、Go Module

无论是GoPath还是vendor都没有解决同一个依赖包的不同版本问题,同时无法直观的梳理项目依赖,导致依赖的管理和升级、项目的构建都非常困难。 Go官方团队为了解决这个问题,在Go 1.11推出了Go Module打算彻底解决以上问题。同时实现了两个重要的目标:

  1. 准确记录项目依赖:记录项目依赖了哪些包,以及包的精确版本。同时可以导出全局依赖树,方便管理和升级。
  2. 可重复构建:在任何环境、平台的构建产物一致。

go.mod

类似java中的maven将依赖声明在.pom文件中一样,golang将依赖声明在go.mod中,下面是一个典型的go.mod文件组成:

代码语言:javascript复制
module github.com/xx/test

go 1.18

require (  
        github.com/antlabs/strsim v0.0.3  
        github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de  
        github.com/fsnotify/fsnotify v1.6.0  
        github.com/gin-gonic/gin v1.8.1  
        github.com/gogf/gf v1.16.9  
        github.com/lestrrat-go/file-rotatelogs v2.4.0 incompatible  
        github.com/mozillazg/go-pinyin v0.20.0  
        github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5  
        github.com/robfig/cron/v3 v3.0.1  
        github.com/sirupsen/logrus v1.9.3  
        github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.690  
        github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.690  
        github.com/xuri/excelize/v2 v2.7.1  
        gorm.io/driver/mysql v1.5.1  
        gorm.io/gorm v1.25.2  
)

exclude (
        github.com/robfig/cron/v3 v3.0.1  
)

replace (
        gorm.io/gorm v1.25.2 => gorm.io/gorm v1.25.7
)

go.mod文件通常由以下几部分组成:

  1. module
  2. go版本声明
  3. require
  4. exclude
  5. replace

module

项目声明,一般采用仓库 module name的方式定义。如果我们项目提供了依赖包给其他人使用的话,对方可以根据这个module声明去对应的仓库中去查询,或者让go proxy到仓库中去查询。

go版本声明

它并不是指你当前使用的Go版本,而是指你的代码所需要的Go的最低版本。

require

声明了依赖包的路径和名字、版本 golang 对于依赖包的版本管理基于语义化,即版本号需要按照以下规定:

代码语言:javascript复制
v<major>.<minor>.<patch> 

major(主版本号): 当做了不兼容的API修改时,一般是重大架构、技术、功能升级,API已经不兼容原来的版本
minor(次版本): 当做了向下兼容的功能性新新增,一般是正常的版本、功能迭代,要求API向后兼容
patch(修订版本号):当做了向下兼容的问题修正,要求API向后兼容

假设我们需要引入依赖github.com/robfig/cron,选择任何v1.x.y都是兼容现在的代码的,例如v1.0.0, v1.2.0。 但是如果我们想使用v3.0.0,直接去修改了go.mod升级了依赖的版本到v3.0.0,这个时候就会出现编译错误,因为主版本号升级后不承诺API的兼容性。

针对这个问题Go Module给的解决方案是,从主版本号的2开始将主版本号加入到go moudle的path中,具体规则如下:

语义化版本

module path

导入go moudle中的包

v1.x.y

github.com/robfig/cron

import “github.com/google/uuid”

v2.x.y

github.com/robfig/cron/v2

import “github.com/robfig/cron/v2”

v3.x.y

github.com/robfig/cron/v3

import “github.com/robfig/cron/v3”

这样如果将项目依赖的外部go moudle的主版本号升级时,就需要切换moudle path和代码中导入package路径,同时对使用的不兼容的API做出修改调整。

exclude

如果你想在你的项目中排除某个依赖库的某个版本,你就可以使用这个字段。

代码语言:javascript复制
exclude ( 
        github.com/robfig/cron/v3 v3.0.1 
)

replace

代码语言:javascript复制
replace (  
        source latest => target latest  
)

replace用来强制替换某些依赖库的版本。通常可以用于以下场景:

  1. 替换无法下载的包。比如有些包因为网络问题无法下载,如果这个包在 Github 上有镜像,那么可以替换为 Github上的包。
  2. 替换为fork仓库。比如有的包有 bug,在开源版本还没有修复时,可以暂时fork下来修复 bug,替换为 fork 版本,等修复后再使用开源包,这种是临时做法。
  3. 禁止被依赖。比如某个module不希望被直接引用,那么可以在 require 中把包的版本号都写为 v0.0.0,然后在下面 replace 中替换为实际的版本号。这样其他包引用这个包时,会因为找不到 v0.0.0而无法使用。

go.sum

go.sum 文件是什么

go.sum 文件中每行记录由包名、版本号、哈希值组成,使用空格分开。go.sum 文件存在的意义是,希望在任何环境中构建项目时使用的依赖包必须跟 go.sum 中记录的完全一致,从而达到一致构建的目的。

同时因为 go.mod 一般不会记录间接依赖,而 go.sum 会把直接依赖、间接依赖,全部记录上,所以go.sum 文件中行数会比 go.mod 文件函数多很多。

正常情况下,每个依赖包版本会包含两条记录:

  1. 第一条记录为该依赖包版本整体(所有文件)的哈希值,
  2. 第二条记录仅表示该依赖包版本中go.mod文件的哈希值
代码语言:javascript复制
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=  
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

如果该依赖包版本没有go.mod文件,则只有第一条记录。如上面的例子中,v0.3.1表示该依赖包版本整体,而v0.3.1/go.mod表示该依赖包版本中go.mod文件。

依赖包版本中任何一个文件(包括go.mod)改动,都会改变其整体哈希值,此处再额外记录依赖包版本的go.mod文件主要用于计算依赖树时不必下载完整的依赖包版本,只根据go.mod即可计算依赖树。

go.sum 文件怎么用

当构建项目时,Go 会先从本地缓存中获取依赖包,然后计算本地依赖包的哈希值,和 go.sum 中的哈希值对比,如果不一致,就会拒绝构建。因为有可能本地缓存的包被篡改,也有可能时go.sum文件中的值被篡改,不过Go更倾向于相信 go.sum 文件中的哈希值,因为第一次写入的时候是经过校验的。

此时可以尝试删除 go.sum 文件,使用 go build 时会自动生成 go.sum 文件,重新写入哈希值,且第一次写入的时候,哈希值是经过校验和数据库校验的。这个校验和数据库的地址在环境变量 GOSUMDB 中有写到,它是一个提供依赖包哈希值查询的服务。

使用

go mod已经集成在go安装包中,只需要使用命令 go mod init [module name]即可初始化一个go mod。

0 人点赞