$GOPATH/go.mod exists but should not

Modules

在学习 golang 之初,便对 golang 的包管理一直一知半解(尤其是一上来就是要了解 gopath、goroot)。本文就 1.11 新特性 Modules 的前世今生,对 golang 的包管理做一次系统学习。

包管理(Package manager)是什么 ?

一聊到包管理,我们可能会想到: * 操作系统中的: yum, api-get, brew ...
* 以及:npm (node package manager) , app-store ...

Wiki

A package manager or package-management system is a collection of software tools that automates the process of installing, upgrading, configuring, and removing computer programs for a computer's operating system in a consistent manner

简单而言:可以通过包管理对程序包(微信、支付宝)进行下载、更新、删除管理的一个系统(类比于我们平时手机上的应用商店 app store,只是对于包管理的操作进行了图形化界面的封装)

那么在包管理器出现之前在操作系统上是如何安装软件的呢?

摘抄一部分说明:

曾几何时,软件都是通过 FTP 或邮件列表(LCTT 译注:即通过邮件列表发布源代码的补丁包)来分发的(最终这些发布方式在互联网的迅猛发展下都演化成为一个个现今常见的软件发布网站)。(一般在一个 tar 文件中)只有一个非常小的文件包含了创建二进制的说明。你需要做的是先解压这个包,然后仔细阅读当中的 README 文件, 如果你的系统上恰好有 GCC(LCTT 译注:GNU C Compiler)或者其它厂商的 C 编译器的话,你得首先运行 ./configure 脚本,并在脚本后添加相应的参数,如库函数的路径、创建可执行文件的路径等等。除此之外,这个配置过程也会检查你操作系统上的软件依赖是否满足安装要求。如果缺失了任何主要的依赖,该配置脚本会退出不再继续安装,直到你满足了该依赖。如果该配置脚本正常执行完毕,将会创建一个 Makefile 文件。

当有了一个 Makefile 文件时, 你就可以接下去执行 make 命令(该命令由你所使用的编译器提供)。make 命令也有很多参数,被称为 make 标识(flag),这些标识能为你的系统优化最终生成出来的二进制可执行文件。在计算机世界的早期,这些优化是非常重要的,因为彼时的计算机硬件正在为了跟上软件迅速的发展而疲于奔命。今日今时,编译标识变得更加通用而不是为了优化哪些具体的硬件型号,这得益于现代硬件和现代软件相比已经变得成本低廉,唾手可得。

最后,在 make 完成之后, 你需要运行 make install (或 sudo make install)(LCTT 译注:依赖于你的用户权限) 来“真正”将这个软件安装到你的系统上。可以想象,为你系统上的每一个软件都执行上述的流程将是多么无聊费时,更不用说如果更新一个已经安装的软件将会多复杂,多么需要精力投入。(LCTT 译注:上述流程也称 CMMI 安装, 即Configure、Make、Make Install)

笔者没有经历过那个年代,可想而知如果没有包管理,想要对自己的软件进行下载安装是一件多么 ** 的事情。

Golang 的包管理

golang 的初体验是给我一种配置劝退的感觉,上手 fork 一个项目。不像 Java 中 maven install,对于 golang 需要先了解 GOPATH。当初实习的时候,也是照着配置 GOPATH,直到前段时间面试被问到 GOPATH,GOROOT, VENDOR 的区别竟哑口无言。也是一个契机,就着包管理这个话题,理解其中的联系。

GOROOT, GOPATH

  • GOROOT

    This is the location of your Go installation. It is used to find the standard libraries.

    GOROOT 很好理解,它是 Go 语言包安装的位置, 用于查找标准库。开发中用于设置项目的 SDK。
    在 Windows 中,GOROOT 的默认值是 C:/go,而在 Mac OS 或 Linux 中 GOROOT 的默认值是 usr/loca/go,如果将 Go 安装在其他目录中,而需要将 GOROOT 的值修改为对应的目录。
    另外,GOROOT/bin则包含 Go 为我们提供的工具链,因此,应该将 GOROOT/bin 配置到环境变量 PATH 中,方便我们在全局中使用 Go 工具链。

    goroot gobin

  • GOPATH > If you come from other programming languages you’re probably used to placing source code anywhere in the file system.

    Go tools expect a certain layout of the source code.

    GOPATH 是工作区的根目录,包含以下几个文件夹

    • src - 源文件的存放地址 (location of source files: .go, .c, .g, .s )
    • pkg - 编译好的包的位置 (location of compiled packages (.a files))
    • bin - 可执行二进制文件的位置 (location of executables built by Go)

    GOPATH 可以存在多个用 ';' 隔开。

    1
    2
    3
    4
    5
    
    import (
    "app/config"
     
    "google.golang.org/grpc"
    )

    例如: GO 会去 GOPATH/src 下寻找 import(不需要写全路径,而对于GOPATH中的包而) 的包, 那么当我们引入本项目内自定义的 "app/config" 包时,意味着我们自己编写的项目必须设置为 GOPATH/src 目录下。 其本质是一个存放代码依赖的地方,所以当我们 go get 下载依赖时会默认下载到第一个 GOPATH/src 目录下。

    gopath.1 gopath.2 gopath.3

    多个 GOPATH: GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当存在多个值时,Go 程序会依次去寻找有没有对应的依赖包。
    区别

  • GOROOT: 作为安装包目录,需要额外配置环境变量以使用 golang 自带的可执行文件。

  • GOPATH: 作为 golang 的工作区,除 GOROOT 以外的所有代码都存 GOPATH 中(包括自己项目的代码,和依赖的第三方包),go get

使用 go env 查看当前配置

1
2
3
4
$ go env
set GOBIN= 
set GOROOT=C:\Go\bin
set GOPATH=C:\Users\**

Q&A

为什么需要 GOROOT 与 GOPATH ? > 两个目录对于我们开发实质都是提供依赖,而 GOROOT 提供语言级的(SDK 源码, go commend),GOPATH 提供除语言级以外的依赖(自身项目包、第三方引用包)

那么在只要 GOROOT 和 GOPATH 下如何对每个项目做到包管理? > 简而言之就是每一项目做一个 GOPATH, 但这样有一个很大的缺点就是,对于每一个项目而言: 很多第三方包引用,包括公司内部公共库引用都是相同。而一旦需要对其进行升级、删除等操作,就牵扯到每一个项目需要手动更新。 因为其公共模块不能共享。

那么将公共模块抽出作为一个单独的 GOPATH 呢? > 这样确实是能解决公共包共享,但是对于其公共部分的版本升级,就不允许每个项目实现依赖版本私有化 (例如 A、B 项目同时需要 ORM 模块,但是 A 版本只希望版本为 1.0 ,而 B 希望版本为 2.0).

社区包管理工具

终于,在 Golang 官方面对 GOPATH 管理的各种乱象始终无动于衷的时候,社区看不下去了,相继出现各类包管理工具。 例如 Godep, glide .... 很大程度上解决了依赖问题但是都有各自的问题存在。

Vendor

时间走到了 2015 年,Golang 官方终于看不下去了,在推出 go1.5 版本的同时,首次实验性质的加入了 vendor 机制 功能。当然,这个功能毕竟是实验性质的,默认情况下是关闭的,导致大多数用户实际上根本不会用它。直到 2016 年,在官方推出 go1.6 版本的时候,vendor 机制 才默认变成开启的状态。


那么到底什么是 vendor 机制 呢? 通俗的说,就是在你的项目中包含了一个 vendor 文件夹,go 语言会把它默认为一个 GOPATH。于是,你就可以在里面放你的依赖库啦。

其基本思路是,将引用的外部包的源代码放在当前工程的vendor目录下面,go 1.6以后编译go代码会优先从vendor目录先寻找依赖包,其在执行 go build 或 go run 时,会按照以下顺序去查找包

  • 当前包下的 vendor 目录
  • 向上级目录查找,直到找到 src 下的 vendor 目录
  • 在 GOROOT 目录下查找
  • 在 GOPATH 下面查找依赖包

但仍然存在许多问题

  1. 嵌套的 vendor 目录问题:vendor 目录下面的项目里面的 vendor 目录怎么办?
  2. vendor 机制本身没有版本概念,不同版本间类型不兼容问题依旧存在。
  3. 与其他 GOPATH 下的包 init 函数冲突问题:出现了相同的包,重复的 init() 函数又怎么办?

随后又产生了更多的管理工具 详细, 其中也出现了大多数使用的 Govendor 工具

Golang modules

参考