我们可能在很多地方如 README 文件、Makefile 文件以及 Dockerfile 文件中看到GO111MODULE=on, 对于刚接触的Golang的开发者可能对此有很多疑惑。

1
GO111MODULE=on go get -u golang.org/x/tools/gopls@latest

这片文章,我将详细介绍GO111MODULE变量的意义,以及什么时候需要使用到该变量, 同时也总结了一些在使用 Go Modules 时需要注意的细节,帮助你在下次遇到这个变量时不再疑惑。

从 GOPATH 到 GO111MODULE

首先,对于 GOPATH,无论是资深开发者和新开发者都不会陌生。GO 在 2009 年发布时,并没有提供包管理器(Package manager), go get会根据导入路径拉取所有的源代码,存储在$GOPATH/src目录中。这种方式使用master分支作为包的稳定版本。 可想而知,这种方式缺乏灵活性同时很容易造成版本管理混乱的状态。

Go Modules(旧版本被成为vgo-versiond Go)在 Go 1.11 被正式引入,与 GOPATH 方式存储a signle git checkout of every package的方式不同, Go Module 通过go.mod文件管理包的版本信息,并且能够指定版本号。

两种包管理方式GOPATHGo Module同时存在,成为 Go 语言使用最大的坑,后续官方试图通过一个变量:GO111MODULE去解决这个问题, 不过随之而来的是更大的灾难。

GO111MODULE 环境变量

环境变量GO111MODUEL的作用是控制 Go 包导入的方式,可以设置为on,off,auto三个值,由于该变量的语意会根据 Go 的版本变化, 这在很多地方造成严重的歧义。

Go1.11 和 Go.12 版本 GO111MODULE

  • GO111MODULE=on无论项目是否在 GOPATH 目录,都会强制使用 Go Modules。
  • GO111MODULE=off无论项目是否在 GOPATH 目录,都会强制使用 GOPATH。
  • GO111MODULE=auto是默认模式,当项目不在 GOPATH 中会启用 Go Modules;当项目在 GOPATH 目录会强制使用 GOPATH。

当你的项目在 GOPATH 目录中,但是你又想使用 Go modules 下载一个指定版本的包或打包时,只能通过 GO111MODULE:

1
GO111MODULE=on go get github.com/golang/mock/tree/master/mockgen@v1.3.1

Go1.13 版本 GO111MODULE

在 Go 1.13 版本,GO111MODULE默认auto的用法发生了该改变:

  • 如果项目包含go.mod 或者项目不再 GOPATH 目录中(无论是否包含go.mdo文件)都会使用 Go module。所以使用 Go 1.13, 你可以将所有项目保存到 GOPATH 中。
  • 只有在项目保存在GOPATH中同时又不包含文件go.mod文件时,才会使用GOPATH

为什么很多地方使用 GO111MODULE

Go Module可以通过git tag控制项目依赖包的版本,而GOPATH默认拉取master分支的最新提交。 环境变量GO111MODULE控制了是否启用 Go module。我们则可以GO111MODULE=on go get xxxx/@1.33的方式拉取指定版本的包。

1
2
3
4
GO111MODULE=on go get -u golang.org/x/tools/gopls@latest
GO111MODULE=on go get -u golang.org/x/tools/gopls@master
GO111MODULE=on go get -u golang.org/x/tools/gopls@v0.1
GO111MODULE=on go get golang.org/x/tools/gopls@v0.1.8

latest标签使用最新版本 git tag。-u选项强制更新,在指定版本是这个选项没有意义。

使用 Go Modules 的建议

其实在新版本Golang中,如果不接触旧的项目,可以不用关注这个变量意义。这里总结了使用 Go Modules 的一些建议

记住 go get 将会更新 go.mod 文件

go get主要用于下载依赖包和安装二进制文件,但是当项目中存在go.mod文件,使用go get会自动修改go.mod文件。 这听起来很奇怪,但这也是 Go modules 的一大亮点:自动维护依赖包版本。

在一些CICD流程中,我们需要注意这点。比如当使用Jenkins时,其会在 slave 节点workspace目录中上拉取代码,如果需要执行一些脚本命令时, 如测试,编译等,如果项目依赖有变化,则go.mod文件会产生修改,这就导致下次执行构建时,产生错误。

这里提供有两种方式解决,一种方法是添加一个clean stage,丢弃暂存区的修改记录;还有一种通过容器方式进行构建。 关于Golang项目CICD流程优化有机会会在后续单独写一篇博客系统介绍,这里只提一下。

Go Module 的依赖源来自哪里

当使用 Go Modules,go build会使用存储在$GOPATH/pkg/mod下的包,在使用 vim、VScode 等编辑器开发 Golang 项目, 可能默认会使用 GOPATH 中的包而不是使用 pkg/mod。

第二个问题是当我们在测试时需要替换一个包版本时,通常有以下几种做法:

Solution 1: 使用go mod vendor + go build -mod=vendor,这将会强制 go 使用 vendor/目录下的包,而不是$GOPATH/pkg/mod 中包

Solution 2:go.mod文件中使用replace../beers是相关依赖的 copy。

1
use replace github.com/maelvls/beers => ../beers

私有 Go modules 和 Dockfile

在公司中,通常会使用很多私有仓,我们可以使用GOPRIVATE去设置让Go跳过包代理,直接从私有代码仓拉取包。 但是但我们使用 docker 构建镜像时,如何拉取私有仓呢,有以下方案

  1. vendoring: 使用go mod vendor,这样就不要传递私有仓凭证给 docker build context。因为所有依赖都保存在 vendor 目录 中,构建时需要使用选项-mod=vendor
  • 优点:加速 docker 镜像构建,(go mod download 和 docker cache 配合使用也可以优化构建速度)

    1
    2
    3
    4
    5
    6
    ENV GOPROXY https://goproxy.io
    WORKDIR /workspace
    ## All these steps will be cached
    COPY go.mod go.sum ./
    ## Get denpendencies will also be cached if wo don't change go.mod and go.sum
    RUN go mod download
  • 缺点:代码管理复杂,需要跟踪依赖包的更新

  1. no vendoring: 如果vendor/目录很大(kubernetes vendor 目录大约是 30MB),这种情况使用 vendoring 的方式会 很糟糕,这时候可以通过定义变量GITHUB_TOKEN传递给 dockerfile 文件。
1
2
git config --global url."https://foo:${GITHUB_TOKEN}@github.com/company".insteadOf "https://github.com/company"
export GOPRIVATE=github.com/company/\*

参考

  1. Why is GO111MODULE everywhere, and everything about Go Modules