zoukankan      html  css  js  c++  java
  • Go依赖管理--module

    1. Module的定义

    举个栗子

    https://github.com/blang/semver 项目中可以包含一个或多个package,不管有多少package,这些package都随项目一起发布,即当我们说github.com/blang/semver某个版本时,说的是整个项目,而不是具体的package。此时项目https://github.com/blang/semver就是一个module。

    官方给module的定义是:一组package的集合,一起被标记版本,即是一个module。

    通常而言,一个仓库包含一个module(虽然也可以包含多个,但不推荐),所以仓库、module和package的关系如下:

    • 一个仓库包含一个或多个Go module;
    • 每个Go module包含一个或多个Go package;
    • 每个package包含一个或多个Go源文件;

    此外,一个module的版本号规则必须遵循语义化规范(https://semver.org/),版本号必须使用格式v(major).(minor).(patch),比如v0.1.0v1.2.3v1.5.0-rc.1

    2. 语义化版本规范

    语义化版本(Semantic Versioning)已成为事实上的标准,几乎知名的开源项目都遵循该规范,更详细的信息请前往https://semver.org/ 查看,在此只提炼一些要点,以便于后续的阅读。

    版本格式v(major).(minor).(patch)中major指的是大版本,minor指小版本,patch指补丁版本。

    • major: 当发生不兼容的改动时才可以增加major版本;比如v2.x.y与v1.x.y是不兼容的;
    • minor: 当有新增特性时才可以增加该版本,比如v1.17.0是在v1.16.0基础上加了新的特性,同时兼容v1.16.0;
    • patch: 当有bug修复时才可以 增加该版本,比如v1.17.1修复了v1.17.0上的bug,没有新特性增加;

    语义化版本规范的好处是,用户通过版本号就能了解版本信息。

    除了上面介绍的基础概念以外,还有描述依赖的go.mod和记录module的checksum的go.sum等内容

    3.快速开始

    Go module到底是做什么的?

    我们在前面的章节已介绍过,但还是想强调一下,Go module实际上只是精准的记录项目的依赖情况,包括每个依赖的精确版本号,仅此而矣。

    那么,为什么需要记录这些依赖情况,或者记录这些依赖有什么好处呢?

    试想一下,在编译某个项目时,第三方包的版本往往是可以替换的,如果不能精确的控制所使用的第三方包的版本,最终构建出的可执行文件从本质上是不同的,这会给问题诊断带来极大的困扰。

    一个项目若要使用Go module,那么其本身需要先成为一个module,也即需要一个module名字。

    在Go module机制下,项目的module名字以及其依赖信息记录在一个名为go.mod的文件中,该文件可以手动创建,也可以使用go mod init命令自动生成。推荐自动生成的方法,如下所示:

    [root@wsl-maoyifei]# go mod init github.com/renhongcai/gomodule
    go: creating new go.mod: module github.com/renhongcai/gomodule
    

    完整的go mod init命令格式为go mod init [module]:其中[module]为module名字,如果不填,go mod init会尝试从版本控制系统或import的注释中猜测一个。这里推荐指定明确的module名字,因为猜测有时需要一些额外的条件,比如 Go 1.13版本,只有项目位于GOPATH中才可以正确运行,而 Go 1.11版本则没有此要求。

    上面的命令会自动创建一个go.mod文件,其中包括module名字,以及我们所使用的Go 版本:

    [root@wsl-maoyifei]# cat go.mod 
    module github.com/renhongcai/gomodule
    
    go 1.16
    

    3.1 管理依赖

    引用一个第三方包github.com/google/uuid来生成一个UUID,这样就会产生一个依赖,代码如下:

    package main
    
    import (
        "fmt"
    
        "github.com/google/uuid"
    )
    
    func main() {
        id := uuid.New().String()
        fmt.Println("UUID: ", id)
    }
    

    在开始编译以前,我们先使用go get来分析依赖情况,并会自动下载依赖:

    [root@wsl-maoyifei]# go get 
    go: finding github.com/google/uuid v1.1.1
    go: downloading github.com/google/uuid v1.1.1
    go: extracting github.com/google/uuid v1.1.1
    

    从输出内容来看,go get帮我们定位到可以使用github.com/google/uuid的v1.1.1版本,并下载再解压它们。

    注意:go get总是获取依赖的最新版本,如果github.com/google/uuid发布了新的版本,输出的版本信息会相应的变化。关于Go Module机制中版本选择我们将在后续的章节详细介绍。

    go get命令会自动修改go.mod文件:

    [root@ecs-d8b6 gomodule]# cat go.mod 
    module github.com/renhongcai/gomodule
    
    go 1.16
    
    require github.com/google/uuid v1.1.1
    

    可以看到,现在go.mod中增加了require github.com/google/uuid v1.1.1内容,表示当前项目依赖github.com/google/uuid的v1.1.1版本,这就是我们所说的go.mod记录的依赖信息。

    由于这是当前项目第一次引用外部依赖,go get命令还会生成一个go.sum文件,记录依赖包的hash值:

    [root@wsl-maoyifei]# cat go.sum 
    github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
    github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
    

    该文件通过记录每个依赖包的hash值,来确保依赖包没有被篡改。关于此部分内容我们在此暂不展开介绍,留待后面的章节详细介绍。

    经go get修改的go.mod和创建的go.sum都需要提交到代码库,这样别人获取到项目代码,编译时就会使用项目所要求的依赖版本。

    至此,项目已经有一个依赖包,并且可以编译执行了,每次运行都会生成一个独一无二的UUID:

    [root@wsl-maoyifei]# go run main.go
    UUID:  20047f5a-1a2a-4c00-bfcd-66af6c67bdfb
    

    注:如果你没有使用go get在执行之前下载依赖,而是直接使用go build main.go运行项目的话,依赖包也会被自动下载。但是在v1.13.4中有个bug,即此时生成的go.mod中显示的依赖信息则会是require github.com/google/uuid v1.1.1 // indirect,注意行末的indirect表示间接依赖,这明显是错误的,因为我们直接import的。

    3.2版本间差异

    由于Go module在Go v1.11初次引入,历经Go v1.12、v1.13的发展,其实现细节上已有了一些变化,按照之前的规划Go module将会在v1.14定型,推荐尽可能使用最新版本,否则可能会产生一些困扰。

    比如,在v1.11中使用go mod init初始化项目时,不填写module名称是没有问题,但在v1.13中,如果项目不在GOPATH目录中,则必须填写module名称。

    4. Replace指令

    go.mod文件中通过指令声明module信息,用于控制命令行工具进行版本选择。一共有四个指令可供使用:

    • module:声明module名称;
    • require:声明依赖以及其版本号;
    • replace:替换require中声明的依赖,使用另外的依赖及其版本号;
    • exclude:禁用指定的依赖;

    4.1 replace 工作机制

    顾名思义,replace指替换,它指示编译工具替换require指定中出现的包,比如,我们在require中指定的依赖如下:

    
    module github.com/renhongcai/gomodule  
    
    go 1.13  
    
    require github.com/google/uuid v1.1.1
    

    此时,我们可以使用go list -m all命令查看最终选定的版本:

    [root@wsl-maoyifei]# go list -m all
    github.com/renhongcai/gomodule
    github.com/google/uuid v1.1.1
    

    毫无意外,最终选定的uuid版本正是我们在require中指定的版本v1.1.1。

    如果我们想使用uuid的v1.1.0版本进行构建,可以修改require指定,还可以使用replace来指定。
    需要说明的是,正常情况下不需要使用replace来修改版本,最直接的办法是修改require即可,虽然replace也能够做到,但这不是replace的一般使用场景。
    下面我们先通过一个简单的例子来说明replace的功能,随即介绍几种常见的使用场景。

    比如,我们修改go.mod,添加replace指令:

    [root@wsl-maoyifei]# cat go.mod 
    module github.com/renhongcai/gomodule
    
    go 1.13
    
    require github.com/google/uuid v1.1.1
    
    replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
    

    replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0指定表示替换uuid v1.1.1版本为 v1.1.0,此时再次使用go list -m all命令查看最终选定的版本:

    [root@wsl-maoyifei]# go list -m all 
    github.com/renhongcai/gomodule
    github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
    

    可以看到其最终选择的uuid版本为 v1.1.0。如果你本地没有v1.1.0版本,你或许还会看到一条go: finding github.com/google/uuid v1.1.0信息,它表示在下载uuid v1.1.0包,也从侧面证明最终选择的版本为v1.1.0。

    到此,我们可以看出replace的作用了,它用于替换require中出现的包,它正常工作还需要满足两个条件:

    • 第一,replace仅在当前module为main module时有效,比如我们当前在编译github.com/renhongcai/gomodule,此时就是main module,如果其他项目引用了github.com/renhongcai/gomodule,那么其他项目编译时,replace就会被自动忽略。

    • 第二,replace指定中=>前面的包及其版本号必须出现在require中才有效,否则指令无效,也会被忽略。
      比如,上面的例子中,我们指定replace github.com/google/uuid => github.com/google/uuid v1.1.0,或者指定replace github.com/google/uuid v1.0.9 => github.com/google/uuid v1.1.0,二者均都无效。

    4.2replace 使用场景

    使用replace替换require中的依赖,在实际项目中replace在项目中经常被使用,其中不乏一些精彩的用法。
    但不管应用在哪种场景,其本质都一样,都是替换require中的依赖。

    4.2.1 替换无法下载的包

    由于中国大陆网络的问题, 有些包无法顺利瞎子啊, 比如golang.org 组织下的包, 值得庆幸的是这些包在Github都有镜像,此时,就可以使用Github的包进行替换。

    比如项目中使用了golang.org/x/text包:

    package main
    
    import (
        "fmt"
        
        "github.com/google/uuid"
        "golang.org/x/text/language"
        "golang.org/x/text/message"
    )
    
    func main(){
        id = uuid.New().string()
        fmt.Print("UUID: ",id)
        
        p := message.NewPrinter(language.BritishEnglish)
        p.Printf("Number format: %v.
    ", 1500)
    
        p = message.NewPrinter(language.Greek)
        p.Printf("Number format: %v.
    ", 1500)  
        
    }
    

    上面的简单例子,使用两种语言language.BritishEnglishlanguage.Greek分别打印数字1500,来查看不同语言对数字格式的处理,一个是1,500,另一个是1.500。此时就会分别引入"golang.org/x/text/language""golang.org/x/text/message"

    执行go getgo build 命令时会再次分析依赖情况, 并更新go.mod文件。 网络正常情况下, go.mod文件会编程下面的内容:

    module github.com/renhongcai/gomodule
    
    go 1.13
    
    require (
        github.com/google/uuid v1.1.1
        golang.org/x/text v0.3.2
    )
    
    replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
    

    我们看到,依赖golang.org/x/text被添加到了require中。(多条require语句会自动使用()合并)。此外,我们没有刻意指定golang.org/x/text的版本号,Go命令行工具根据默认的版本计算规则使用了 v0.3.2版本,此处我们暂不关心具体的版本号。

    没有合适的网络代理情况下,golang.org/x/text 很可能无法下载。

    那么此时,就可以使用replace来让我们的项目使用GitHub上相应的镜像包。我们可以添加一条新的replace条目,如下所示:

    replace (
        github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
        golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
    )
    

    此时,项目编译时就会从GitHub下载包。我们源代码中import路径 golang.org/x/text/xxx不需要改变。

    也许有读者会问,是否可以将import路径由golang.org/x/text/xxx改成github.com/golang/text/xxx?这样一来,就不需要使用replace来替换包了。

    遗憾的是,不可以。因为github.com/golang/text只是镜像仓库,其go.mod文件中定义的module还是module golang.org/x/text,这个module名字直接决定了你的import的路径。

    4.2.2 调试依赖包

    有时我们需要调试依赖包,此时就可以使用replace来修改依赖,如下所示:

    replace (
        github.com/google/uuid v1.1.1 => ../uuid
        golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
    )
    

    语句github.com/google/uuid v1.1.1 => ../uuid使用本地的uuid来替换依赖包,此时,我们可以任意地修改../uuid目录的内容来进行调试。

    除了使用相对路径,还可以使用绝对路径,甚至还可以使用自已的fork仓库。

    使用fork仓库

    有时在使用开源的依赖包时发现了bug,在开源版本还未修改或者没有新的版本发布时,你可以使用fork仓库,在fork仓库中进行bug fix。
    你可以在fork仓库上发布新的版本,并相应的修改go.mod来使用fork仓库。

    比如,我fork了开源包github.com/google/uuid,fork仓库地址为github.com/RainbowMango/uuid,那我们就可以在fork仓库里修改bug并发布新的版本v1.1.2,此时使用fork仓库的项目中go.mod中replace部分可以相应的做如下修改:

    github.com/google/uuid v1.1.1 => github.com/RainbowMango/uuid v1.1.2
    

    需要说明的是,使用fork仓库仅仅是临时的做法,一旦开源版本变得可用,需要尽快切换到开源版本。

    4.2.3 禁止被依赖

    另一种使用replace的场景是你的module不希望被直接引用,比如开源软件kubernetes,在它的go.modrequire部分有大量的v0.0.0依赖,比如:

    module k8s.io/kubernetes
    
    require (
        ...
        k8s.io/api v0.0.0
        k8s.io/apiextensions-apiserver v0.0.0
        k8s.io/apimachinery v0.0.0
        k8s.io/apiserver v0.0.0
        k8s.io/cli-runtime v0.0.0
        k8s.io/client-go v0.0.0
        k8s.io/cloud-provider v0.0.0
        ...
    )
    

    由于上面的依赖都不存在v0.0.0版本,所以其他项目直接依赖k8s.io/kubernetes时会因无法找到版本而无法使用。
    因为Kubernetes不希望作为module被直接使用,其他项目可以使用kubernetes其他子组件。

    kubernetes 对外隐藏了依赖版本号,其真实的依赖通过replace指定:

    replace (
        k8s.io/api => ./staging/src/k8s.io/api
        k8s.io/apiextensions-apiserver => ./staging/src/k8s.io/apiextensions-apiserver
        k8s.io/apimachinery => ./staging/src/k8s.io/apimachinery
        k8s.io/apiserver => ./staging/src/k8s.io/apiserver
        k8s.io/cli-runtime => ./staging/src/k8s.io/cli-runtime
        k8s.io/client-go => ./staging/src/k8s.io/client-go
        k8s.io/cloud-provider => ./staging/src/k8s.io/cloud-provider
    )
    

    前面说过,replace指令在当前模块不是main module时会被自动忽略的,Kubernetes正是利用了这一特性来实现对外隐藏依赖版本号来实现禁止直接引用的目的。

    5. exclude 指令

    go.mod文件中的exclude指令用于排除某个包的特定版本,其与replace类似,也仅在当前module为main module时有效,其他项目引用当前项目时,exclude指令会被忽略。

    exclude指令在实际的项目中很少被使用,因为很少会显式地排除某个包的某个版本,除非我们知道某个版本有严重bug。
    比如指令exclude github.com/google/uuid v1.1.0,表示不使用v1.1.0 版本。

    5.1 排除指定版本

    github.com/renhongcai/gomodule的v1.3.0版本中,我们的go.mod文件如下:

    module github.com/renhongcai/gomodule  
    
    go 1.13  
    
    require (  
      github.com/google/uuid v1.0.0  
      golang.org/x/text v0.3.2  
    )  
    
    replace golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
    

    github.com/google/uuid v1.0.0说明我们期望使用 uuid包的v1.0.0版本。

    假如,当前uuid仅有v1.0.0v1.1.0v1.1.1三个版本可用,而且我们假定v1.1.0版本有严重bug。
    此时可以使用exclude指令将uuid的v1.1.0版本排除在外,即在go.mod文件添加如下内容:

    exclude github.com/google/uuid v1.1.0
    

    虽然我们暂时没有使用uuid的v1.1.0版本,但如果将来引用了其他包,正好其他包引用了uuid的v1.1.0版本的话,此时添加的exclude指令就会跳过v1.1.0版本。

    5.1.2 创建依赖包

    为了进一步说明exclude用法,我们创建了一个仓库github.com/renhongcai/exclude,并在其中创建了一个modulegithub.com/renhongcai/exclude,其中go.mod文件(v1.0.0版本)如下:

    module github.com/renhongcai/exclude
    
    go 1.13
    
    require github.com/google/uuid v1.1.0
    

    可以看出其依赖github.com/google/uuid 的 v1.1.0 版本。创建github.com/renhongcai/exclude的目的是供github.com/renhongcai/gomodule使用的。

    5.1.3使用依赖包

    由于github.com/renhongcai/exclude也引用了uuid包且引用了更新版本的uuid,那么在github.com/renhongcai/gomodule引用github.com/renhongcai/exclude时,会被动的提升uuid的版本。

    在没有添加exclude之前,编译时github.com/renhongcai/gomodule依赖的uuid版本会提升到v1.1.0,与github.com/renhongcai/exclude保持一致,相应的go.mod也会被自动修改,如下所示:

    module github.com/renhongcai/gomodule
    
    go 1.13
    
    require (
        github.com/google/uuid v1.1.0       
        github.com/renhongcai/exclude v1.0.0
        golang.org/x/text v0.3.2
    )
    
    replace golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
    

    但如果添加了exclude github.com/google/uuid v1.1.0 指令后,编译时github.com/renhongcai/gomodule依赖的uuid版本会自动跳过v1.1.0,即选择v1.1.1版本,相应的go.mod文件如下所示:

    module github.com/renhongcai/gomodule
    
    go 1.13
    
    require (
        github.com/google/uuid v1.1.1             # 直接跳过v1.1.0
        github.com/renhongcai/exclude v1.0.0
        golang.org/x/text v0.3.2
    )
    
    replace golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
    
    exclude github.com/google/uuid v1.1.0
    

    在本例中,在选择版本时,跳过uuid v1.1.0版本后还有v1.1.1版本可用,Go 命令行工具可以自动选择v1.1.1版本,但如果没有更新的版本时将会报错而无法编译。

    ♥永远年轻,永远热泪盈眶♥
  • 相关阅读:
    js optional chaining operator
    Linux Bash Script conditions
    Linux Bash Script loop
    js Nullish Coalescing Operator
    js sort tricks All In One
    React Portal All In One
    最小生成树&&次小生成树
    链式前向星实现以及它的遍历
    [2015.11.8|9图论]解题代码集合
    最短路算法及其延伸
  • 原文地址:https://www.cnblogs.com/failymao/p/15085684.html
Copyright © 2011-2022 走看看