正在或即将被使用的Go依赖包管理方法:Go Modules,Go 1.13的标准特性

Source

公众号原文地址:https://mp.weixin.qq.com/s/SGGV3tWEg5AAJ7I_FcK0cg

目录

  • 目录

  • 说明

  • 初始化

  • 依赖包的默认导入

  • 依赖包的特定版本导入

  • 查看已添加依赖

  • 依赖包的存放管理

  • 依赖包的版本切换

  • 删除未使用依赖包

  • 引用项目中的 package

  • 引用不同版本的父子目录

  • 实例演示

  • 需要注意的坑

  • IDE 与 Go Modules

  • IntelliJ IDEA/Goland

  • vim

  • 参考

说明

Go 的依赖包管理一直是个问题,先后出现了 godep、glide、dep 等一系列工具,vendor 机制使依赖包的管理方便了很多,但依然没有统一的管理工具,不同的项目各用各的方法。

另外使用 vendor 后,每个项目都完整拷贝一份依赖包,既不方便管理又浪费了本地空间。

此外,Go 项目中的 import 指令后面的 package 路径与项目代码的存放路径相关,项目目录不能随意移动,必须安分守己地趴在 $GOPATH/src 中,否则 import 会找不到项目中的 package,虽然可以通过在容器中编译或者为每个项目准备一套 Go 环境的方式解决,但是麻烦且有额外开销。

Go1.11 和 Go1.12 引入的 Go Modules 机制,提供了统一的依赖包管理工具 go mod,依赖包统一收集在 $GOPATH/pkg/mod 中进行集中管理,并且将 import 路径与项目代码的实际存放路径解耦,使 package 定义导入更加灵活。

Go Modules 将成为 Go1.13 默认的依赖包管理方法,在 Go1.11 和 Go1.12 中, Go Modules 只能在 $GOPATH 外部使用,Using Go Modules 中有详细介绍。

很多开源项目已经改用 Go Modules 了,浏览代码的时候会发现,很多项目的 master 分支中增加了 go.mod 和 go.sum 文件。

Go Modules 的主要功能就四个:添加依赖、更新依赖、删除依赖,以及多版本依赖。

初始化

Go Modules 的初始化命令为 go mod init <ROOTPATH>,ROOTPATH 是项目的 import 路径。

在 $GOPATH 外部创建一个目录,然后初始化,项目的路径设置为 exampe.com/hello

$ mkdir go-modules-example$ cd go-modules-example$ go mod init example.com/hello        # 该项目代码的引用路径是 example.com/hellogo: creating new go.mod: module example.com/hello

引用该项目中的 package 时使用前缀 example.com/hello

项目下生成一个 go.mod 文件,里面记录了 module 路径和 go 的版本,刚创建时这个文件中没有依赖信息:

$ cat go.modmodule example.com/hellogo 1.12

对于 Go1.11 和 Go1.12,如果在 $GOPATH 中执行 go mod 会遇到下面的错误:

$ go mod init example.com/hellogo: modules disabled inside GOPATH/src by GO111MODULE=auto; see 'go help modules'

依赖包的默认导入

在 go-modules-example 中创建一个 main.go,简单写几行代码,引入 “github.com/lijiaocn/golib/version”:

// Create: 2019/05/05 16:53:00 Change: 2019/05/05 16:56:53// FileName: main.go// Copyright (C) 2019 lijiaocn <lijiaocn@foxmail.com>//// Distributed under terms of the GPL license.package mainimport (    "github.com/lijiaocn/golib/version")func main() {    version.Show()}

用下面的 Makefile 编译( Makefile 纯粹为了方便,直接用 go build 也可以):

# Makefile# lijiaocn, 2019-05-05 16:56#VERSION=1.0.0COMPILE=$(shell date -u "+%Y-%m-%d/%H:%M:%S")all: buildbuild:   go build -ldflags "-X github.com/lijiaocn/golib/version.VERSION=${VERSION} -X github.com/lijiaocn/golib/version.COMPILE=${COMPILE}"

编译或者用 go test 运行测试代码时,默认将 import 引入的 package 的最新版本写入 go.mod 和 go.sum:

$ makego build -ldflags "-X github.com/lijiaocn/golib/version.VERSION=1.0.0 -X github.com/lijiaocn/golib/version.COMPILE=2019-05-05/09:55:04"go: finding github.com/lijiaocn/golib v0.0.1go: downloading github.com/lijiaocn/golib v0.0.1go: extracting github.com/lijiaocn/golib v0.0.1

go.mod 中写入依赖关系:

$ cat go.modmodule example.com/hellogo 1.12require github.com/lijiaocn/golib v0.0.1

go.sum 中记录的完整依赖:

$ cat go.sumgithub.com/lijiaocn/golib v0.0.1 h1:bC8xWHei7xTa8x65ShiPBNjVYXoxt6EDmnSUaGgRUW8=github.com/lijiaocn/golib v0.0.1/go.mod h1:BUO0RF2eDlol519GuXLQtlku8pdUim0h+f6wvX/AsNk=

依赖包的特定版本导入

在使用 go modules 的项目目录中,用 go get 下载的代码包自动作为依赖包添加,例如:

$ go get github.com/lijiaocn/codes-go/01-02-hellogo: finding github.com/lijiaocn/codes-go/01-02-hello latestgo: finding github.com/lijiaocn/codes-go latestgo: downloading github.com/lijiaocn/codes-go v0.0.0-20180220071929-9290fe35de7ego: extracting github.com/lijiaocn/codes-go v0.0.0-20180220071929-9290fe35de7e

go.mod 中增加了一行记录,新增的依赖被标注为 indirect,意思是还没有被使用:

$ cat go.modmodule example.com/hellogo 1.12require (    github.com/lijiaocn/codes-go v0.0.0-20180220071929-9290fe35de7e // indirect github.com/lijiaocn/golib v0.0.1)

在用 go get 添加依赖的时候,可以用 @v1.1 样式的后缀指定依赖的版本,例如:

$ go get github.com/lijiaocn/glib@v0.0.2

查看已添加依赖

go list 命令列出当前项目的依赖包以及代码版本:

$ go list -m allexample.com/hellogithub.com/lijiaocn/codes-go v0.0.0-20180220071929-9290fe35de7egithub.com/lijiaocn/golib v0.0.1

依赖包的存放管理

依赖包既不在 GOPATH/src 目录中,也不在 vendor 目录(Go Moduels 不会创建 vendor 目录),而是在 `GOPATH/pkg/mod` 目录中:

$ ls $GOPATH/pkg/mod/github.com/lijiaocn/codes-go@v0.0.0-20180220071929-9290fe35de7e golib@v0.0.1$ ls $GOPATH/pkg/mod/github.com/lijiaocn/golib@v0.0.1config container generator terminal version virtio

如上所示,目录名中包含版本信息,例如 golib@v0.0.1。

$GOPATH/pkg/mod/cache/download/ 中有原始代码的缓存,避免重复下载:

$ ls $GOPATH/pkg/mod/cache/download/github.com/lijiaocncodes-go golib$ ls $GOPATH/pkg/mod/cache/download/github.com/lijiaocn/golib/@vlist           list.lock      v0.0.1.info    v0.0.1.lock    v0.0.1.mod     v0.0.1.zip     v0.0.1.ziphash

依赖包的版本切换

依赖代码的版本更新很简单,直接用 go get 获取指定版本的依赖代码即可,例如将 lijiaocn/glib 更新到 v0.0.2:

$ go get github.com/lijiaocn/glib@v0.0.2go: finding github.com/lijiaocn/golib v0.0.2go: downloading github.com/lijiaocn/golib v0.0.2go: extracting github.com/lijiaocn/golib v0.0.2

可以看到依赖的代码版本发生了变化:

$ go list -m allexample.com/hellogithub.com/lijiaocn/codes-go v0.0.0-20180220071929-9290fe35de7egithub.com/lijiaocn/golib v0.0.2

删除未使用依赖

不需要的依赖必须手动清除,执行 go mod tidy,清除所有未使用的依赖:

$ go mod tidy
$ go list -m allexample.com/hellogithub.com/lijiaocn/golib v0.0.2

引用项目中的 package

在项目中创建一个名为 display 的 package:

$ tree displaydisplay└── display.go

导入时使用 go mod 初始化时定义的前缀,example.com/hello/display

import (    "example.com/hello/display"    "github.com/lijiaocn/golib/version"    )

引用当前项目中的 package 时,import 使用的路径和项目所在的路径彻底解耦,但是要注意,如果提供给外部项目使用,需要确保 go get 能够从 example.com 获得 /hello/display。

引用不同版本的父子目录

Using Go Modules 中有一节是 Adding a dependency on a new major version,示例中引入了 v1.5.2 版本的 rsc.io/quote,和 v3.1.0 版本的 rsc.io/quote/v3,这两个 package 是父子目录,版本不相同:

package helloimport (    "rsc.io/quote"    quoteV3 "rsc.io/quote/v3")func Hello() string {    return quote.Hello()}func Proverb() string {    return quoteV3.Concurrency()}
➜  rsc.io tree quotequote├── LICENSE├── README.md├── buggy│   └── buggy_test.go├── go.mod├── go.sum├── quote.go├── quote_test.go└── v3    ├── go.mod    ├── go.sum    └── quote.go2 directories, 10 files

注意,v3 是一个真实存在的子目录,必须是用 go modules 管理的,rsc.io/quote 和 rsc.io/quote/v3 是父子目录,但它们是完全独立的 package。

引用 1.5.2 版本的 rsc.io/quote 和 v3.1.0 版本的 rsc.io/quote/v3 :

$ go get rsc.io/quote@v1.5.2  ...$ go get rsc.io/quote/v3@v3.1.0  ...

可以看到两个版本同时存在:

$ go list -m rsc.io/q...rsc.io/quote v1.5.2rsc.io/quote/v3 v3.1.0

实例演示

实现一个用 go modules 管理的 package: github.com/introclass/go_mod_example_pkg

image

在另一个使用 go modules 的项目中引用 v1.0.1 版本:github.com/introclass/go-mod-example

$ go get github.com/introclass/go_mod_example_pkg@v1.0.1go: finding github.com/introclass/go_mod_example_pkg v1.0.1go: downloading github.com/introclass/go_mod_example_pkg v1.0.1go: extracting github.com/introclass/go_mod_example_pkg v1.0.1

查看依赖的代码,显示依赖的是 v1.0.1:

$ go list  -m allexample.com/hellogithub.com/introclass/go_mod_example_pkg v1.0.1github.com/lijiaocn/golib v2.0.1+incompatible

在 main 函数中使用导入的依赖包:

package mainimport (    "example.com/hello/display"    pkg "github.com/introclass/go_mod_example_pkg"    "github.com/lijiaocn/golib/version")func main() {    version.Show()    display.Display("display print\n")    pkg.Vesrion()}

编译执行,输出的v1.0.1:

$ ./helloversion:    compile at:   golib v2display printv1.0.1

将依赖包切换到版本 2.0.1:

$ go get github.com/introclass/go_mod_example_pkg@v2.0.1go: finding github.com/introclass/go_mod_example_pkg v2.0.1

重新编译执行,输出的版本是 v2.0.1:

$ ./helloversion:    compile at:   golib v2display printv2.0.1

引用依赖包 v3.0.1 版本的 v3 子目录(事实上是一个独立的 pacakge ):

$ go get github.com/introclass/go_mod_example_pkg/v3@v3.0.1go: finding github.com/introclass/go_mod_example_pkg/v3 v3.0.1go: downloading github.com/introclass/go_mod_example_pkg/v3 v3.0.1go: extracting github.com/introclass/go_mod_example_pkg/v3 v3.0.1

修改 main 函数,引用 v3:

package mainimport (    "example.com/hello/display"    pkg "github.com/introclass/go_mod_example_pkg"    pkgv3 "github.com/introclass/go_mod_example_pkg/v3"    "github.com/lijiaocn/golib/version")func main() {    version.Show()    display.Display("display print\n")    pkg.Vesrion()    pkgv3.Vesrion()}

重新编译执行,分别输出 v2.0.1 和 v3.0.1 in v3:

$ ./helloversion:    compile at:   golib v2display printv2.0.1v3.0.1 in v3

需要注意的坑

1、引用不同版本的父子目录,被引用的父子目录必须是用 go mod 管理的 package,非 go mod 管理的代码不行;

2、go mod 会在本地缓存代码,如果被引用的代码的版本号不变,但是代码变了(在做实验或者代码版本管理比较乱的时候,可能会出现的这种情况),清除本地缓存( GOPATH/pkg/mod/cache 和GOPATH/pkg/mod/ 依赖代码 )后,才能重新拉取最新的代码(可能会有其它的更新缓存的方法);

3、如果被外部项目引用,go.mod 中设置的 package 路径需要与代码的获取地址相同,项目内部引用没有该限制,github.com/introclass/go-mod-example 的 go.mod 中标注的是 example.com/hello,代码获取地址 github.com/intraoclass/go-mode-example 与 example.com/hello 不一致,在另一个项目中用 github 地址加载时会失败:

$ go get github.com/introclass/go-mod-examplego: finding github.com/introclass/go-mod-example latestgo: github.com/introclass/go-mod-example@v0.0.0-20190605063729-4a841a8278e3: parsing go.mod: unexpected module path "example.com/hello"go: error loading module requirements

IDE 与 Go Modules 项目

IntelliJ IDEA/Goland

在 IntelliJ IDEA 或者 Goland 中(需要是最新的2019.1版本)导入使用 Go Module 的项目的时候,要选择 Go Module(vgo),否则 IDE 找不到 import 导入的代码,create-a-project-with-vgo-integration 有更多介绍:

image

IntelliJ IDEA/Goland 左侧编码显示的依赖代码(带有版本号或者 commit id):

image

vim

vim插件 vim-go 从 v1.19 开始支持 go.mod,但是代码跳转等还不支持。

cmd/go: track tools/tooling updates to support modules 列出了一些工具对 go module 的支持情况。

参考

  1. Using Go Modules

  2. Where is the module cache in golang?

  3. create-a-project-with-vgo-integration

  4. cmd/go: track tools/tooling updates to support modules

上一篇:源代码阅读方法(附Go语言项目的代码阅读技巧)

关注后加作者微信

image

公众号原文地址:https://mp.weixin.qq.com/s/SGGV3tWEg5AAJ7I_FcK0cg