Go指南-GoModule依赖管理

7,242 阅读3分钟

前言

修订:2020.09.17

Golang的包管理一直是广大开发者吐槽的点之一。

Go依赖管理简史

Golang的包管理分为三个阶段,version < 1.11、 1.11 <= version < 1.13、 version >= 1.13。

version < 1.11

在这个阶段,Golang的包管理存在以下不足:

  • 必须设置GOPATH环境变量,且源代码必须存放在GOPATH下
  • 拉取外部依赖包时,总是拉取最新的版本,无法指定需要的版本

之所以设置GOPATH环境变量有两个原因:

  • 它规定了go get命令下载的依赖包的存储位置($GOPATH/src)
  • 通过设置GOPATH,可以方便Golang计算出import的路径

另外,由于无法指定依赖包的版本,因此容易导致“本地测试OK,但线上部署失败”的问题。这样的问题是广大开发者无法忍受的,所以,各种包管理工具开始涌现出来,典型的有dep,glide等,这里不再赘述。

1.11 <= version < 1.13

这个阶段默认使用的还是GOPATH的管理方式,但是开始支持Go Module的管理方式。

Go Module解决了上述的阶段存在的不足:
1.它不再需要GOPATH,即你的项目代码可以随意存放
2.它通过go.mod + go.sum解决依赖包的版本问题(后面会讲到)

如果需要迁移到Go Module,需要设置以下环境变量

vim ~/.bash_profile

export GO111MODULE=on

version >= 1.13

从这个阶段开始,Golang的包管理默认使用的是Go Module。

GOPATH VS GoModule

这两者既可以说是一种项目管理机制,也可以说是一种依赖管理机制,所以在这两点上它们的不同在于:

  • 1.项目管理上,GOPATH秉承的是一种单工作区的理念,所有代码存放在一个目录下;而GoModule突破了这种限制,可以随处新建项目。
  • 2.依赖管理上,GOPATH下载的依赖会存放在 $GOPATH/src 目录下,并且没有版本管理;而GoModule的依赖与源码分开,存放在pkg目录下,且包含了版本管理。

使用GOPATH进行依赖管理

如果想了解如何使用 GOPATH 进行依赖管理,可以参考本人之前的文章Go指南-GOPATH依赖管理

使用Go Module进行依赖管理

本节翻译自《Using Go Modules》

Module 是一系列依赖包的集合,通过go mod init xxx可初始化一份空的go.mod和go.sum,这两份文件存放于项目的根路径下。

对于 go.mod,它不仅存储了这些依赖包的路径及其版本,同时也指定了import的根路径,对于go.sum,它存放了依赖包内容的预期校验和,保证前一次下载的代码和现在下载的代码是一致的。

配置代理

由于Golang大部分依赖包都在国外,直接下载非常缓慢,在没有Go Module的时候,需要自己配置代理,比如socks;但是有了Go Module,就可通过设置环境变量来配置代理了,具体参考:goproxy.io/zh/。

配置时有几个注意点:
1.如果你有私有仓库和公共仓库,则需要加上direct参数,并配置GOPRIVATE(针对Go1.13)

# 有了direct,GOPRIVATE指定的仓库不会使用代理
go env -w GOPROXY=https://goproxy.io,direct

# 设置不走代理的私有仓库,多个用逗号相隔
go env -w GOPRIVATE=*.corp.example.com

2.如果你使用的是Golang IDE,则注意该IDE也要配置

3.如果你的~/.bash_profile或~./bashrc 文件存在GO111MODULE等环境变量,则go env 写入时会冲突

warning: go env -w GOPROXY=... does not override conflicting OS environment variable

初始化项目

1.新建文件夹

mkdir go-module-lab && cd go-module-lab

2.初始化Go Module项目,git.own.com/go-module是自定义的

go mod init git.own.com/go-module

3.查看go.mod

module git.own.com/go-module

go 1.13

自定义依赖测试

1.自定义库

mkdir hello && touch hello/hello.go

hello.go 内容

package hello

func Hello() string {
	return "Hello, world."
}

2.新建main.go测试,内容如下

package main

import (
	"fmt"
	// 前面提过,go.mod 指定了import时的根路径
	"git.own.com/go-module/hello"
)

func main()  {
	fmt.Println(hello.Hello())
}

下载第三方依赖测试

1.更新hello.go文件,引入rsc.io/quote依赖

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}

2.执行go run main.go,会自动下载依赖

➜  go-module-lab go run main.go 
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

Hello, world.

3.查看go.mod

module git.own.com/go-module

go 1.13

require rsc.io/quote v1.5.2

可以看到,使用Go Module的包管理方式,Golang会自动帮我们处理包的依赖关系,并把缺失的包添加到go.mod,并使用rsc.io/quote的最新版本。(这里的最新版本应理解为最新并打了tag的版本,如果没有打tag,则会使用一种pseudo-version的方式标识,下文会说到)

4.借助go list命令查看所有依赖

$ go list -m all
git.own.com/go-module
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0

补充:
pseudo-versions(伪版本)
一般情况下,go.mod使用语义化版本来标志依赖包的版本号,比如v1.0.0、v1.0.1。

它包含三个部分:

  • 主版本号:当你做了不兼容的 API 修改,比如v1.5.2的1
  • 次版本号:当你做了向下兼容的功能性新增,比如v1.5.2的5
  • 修订号:当你做了向下兼容的问题修正,比如1.5.2的2

语义化版本规定,同一个主版本号的必须向下兼容,比如v1.5.2必须向下兼容v1.1.0;如果代码不兼容,则必须使用新的版本号。

但是语义化版本是基于项目有打tag的情况下,如果一些项目没有打tag,则Golang会使用一种pseudo-version来标识,类似v0.0.0-yyyymmddhhmmss-abcdefabcdef的形式。

其中,yyyymmddhhmmss使用的是UTC时间,abcdefabcdef对应的是你这次commit的哈希值(前12位),

对于前缀v0.0.0,则有三种情况:
1.当你的项目一个tag都没有的时候,形式为v0.0.0-yyyymmddhhmmss-abcdefabcdef

2.当你项目最近打的tag的名称为vX.Y.Z-pre的时候,形式为vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef

3.当你的项目最近打的tag的名称是vX.Y.Z的时候,形式为vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef

参考:golang.org/cmd/go/#hdr…

go.sum
之所以有go.sum文件,是因为单纯地通过语义化版本(v1.5.2)无法确定每次通过v1.5.2这个tag下载的都是同一份代码

比如发布者在 GitHub 上给自己的项目打上 v1.5.2 的tag之后,依旧可以删掉这个tag,并提交不同的内容后再重新打个 v1.5.2 的 tag。

为了确定是否是同一份代码,go.sum存放了特定模块版本的内容的预期校验和,如果该代码有改动,则预期校验和不匹配,就会导致编译错误。

verifying xxx/base@v1.3.0: checksum mismatch
	downloaded: h1:T2eK+D0jzzeu4+S+oP9KvGgovPnl4FjxYShqdNSPrjc=
	go.sum:     h1:Crwm2FliMjZ3BABjnydOpoJiFPaKcod/zYNOtcB9Xkw=

更新外部依赖

更新次版本号

更新次版本号比较简单,直接使用go get即可,比如更新golang.org/x/text

go get golang.org/x/text

通过查看go.mod的变化,我们可以看到golang.org/x/text的版本号由v0.0.0-20170915032832-14c0d48ead0c升级到v0.3.2。(indirect表明该依赖包在源码中没有用到,是间接依赖的)

module git.own.com/go-module

go 1.13

require (
        golang.org/x/text v0.3.2 // indirect
        rsc.io/quote v1.5.2
)

除此之外,我们还可以更新到特定版本,在此之前,我们先看看该模块有哪些可用版本(以rsc.io/quote为例)

$ go list -m -versions rsc.io/quote  
rsc.io/quote v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1

更新到特定版本:

go get rsc.io/quote@v1.4.0

如果想要使用特定的分支,只需要把版本号换成分支名即可(如果分支名包含特定符号,如"/",可用双引号将分支名括起来):

go get rsc.io/quote@dev

更新主版本号

如果需要更新主版本号,需要在代码中手动指定,因为不同主版本号相当于一个新的依赖(库)。

1.添加新函数

package hello

import (
	"rsc.io/quote"
	quoteV3 "rsc.io/quote/v3"  
)

func Hello() string {
	return quote.Hello()
}

func Proverb() string {
	return quoteV3.Concurrency()
}

2.自动下载依赖

package main

import (
	"fmt"
	"git.own.com/go-module/hello"
)

func main()  {
	fmt.Println(hello.Hello())

	fmt.Println("proverb", hello.Proverb())
}

3.查看go.mod

module git.own.com/go-module

go 1.13

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

从上面可以看出,Go Module每一个主版本号使用不同的路径表示,如v1,v2,v3;另外,Golang允许同时存在多个主版本号,因为路径不同,相当于是一个新的库,这样做的目的是保持增量迁移。

比如我一开始使用rsc.io/quote,后面有改动,且与之前不兼容,这是我就可以使用新的主版本号,比如rsc.io/quote/v3,但是Hello这个函数暂时还不能迁移到V3版本,这是多版本的作用就凸显出来了

删除多余依赖

当过了一段时间,我们已经把把rsc.io/quote的代码全部迁移到新版本rsc.io/quote/v3, 类似下面的代码

package hello

import (
	quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
	return quoteV3.HelloV3()
}

func Proverb() string {
	return quoteV3.Concurrency()
}

这时之前的go.mod里面的rsc.io/quote是多余的,我们可以通过go mod tidy 删除多余的rsc.io/quote

$ go mod tidy

$ cat go.mod
module git.own.com/go-module

go 1.13

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

总结

1.go mod init: 初始化一个Go Module项目,同时生成go.mod和go.sum文件
2.go build/test/run: 这三个命令都会自动下载依赖,并更新go.mod和go.sum文件
3.go list -m all:打印目前的所有依赖包
4.go get:手动下载依赖包,或者更改依赖包版本
5.go mod tidy:增加缺失的依赖,删除没有用到的依赖

补充

go env

配置一些环境变量。

# 环境变量说明文档
go help environment

# 环境变量配置文件路径
$ go env GOENV
/Users/xxx/Library/Application Support/go/env

# 列出所有环境变量
go env

# 列出所有环境变量(以json格式)
go env -json

# 修改某个环境变量
go env -w GOPROXY=https://goproxy.io,direct

# 重置某个变量
go env -u GOPROXY

参考/推荐

1.Go语言包管理简史
2.初窥Go module
3.Go modules:版本是如何选择的?
4.谈谈go.sum