最新发布的 Go - Go 1.13 (opens new window) -- 终于实现了对 Go Modules (opens new window) 的完整支持 -- 一个优秀且令人等待已久的内置依赖版本管理问题的解决方案。现在模块会默认开启,也就形式上抛弃了 GOPATH。
技术上讲,GOPATH 依然还是被支持的。Go 1.13 版本说明 (opens new window) 用“GOPATH mode”来指代这种模式(相对的是“modules mode”)。然而,对于个人来说,这无疑是 Go 开发生态革命中的一个结构转变。现在我们或静态分析工具再也无法假设所有代码都存放在一个大目录下面了。
许多人都强烈反感 GOPATH 这个死板的想法和概念。在开始上手 Go 之前,成千上万的人得花费小时级别的时间来设置一个单一的环境变量(仅个人看法)。毫不意外,能够在任何目录下编写 Go 程序的事实得到了大肆赞美。
然而。
要是被问起的话,我依然觉得 GOPATH 是解决一系列复杂问题的一个漂亮而优雅的方案,并以其特有而唯一的方式塑造着 Go 生态系统。
所以,屏蔽 GOPATH 之前,我想对这 6 个字母致以起码的敬意,它们给某些人带来了如此多的喜悦,而狠狠地伤了另外一些人的心。
# GOPATH:一个单一的配置
让我从新手入门任何新语言的起点说起--配置开发环境。
对于大多数编程语言来说,耗费一到两天来仅仅配好环境然后才能写出第一个“hello world”应用是相当平常的。某些语言需要复杂的工具检测并找到所需二进制和库版本用于适配特定的 OS 和机器架构。另外一些语言里,开发者得负责手动地正确设置好辅助工具的所有目录、系统变量、路径、文件系统权限和版本等。一些成功的公司 (opens new window)就是专门设计解决方案来处理搭建开发环境的复杂任务的。
因此,为一门新的编程语言配置一台电脑一直都是一项繁琐复杂的事情。尽管这是体验游戏的一部分。全宇宙都知道这是世界的工作原理。
GOPATH 将这个大问题精简为设置一个单一值即可 -- 仅需告诉 Go 我们的工作目录在哪,其他的就留给它们就行了。选个目录,设置环境变量,然后就可以百分百确定 Go 不会触碰文件系统的其他地方。
还能更简单一点吗?
如此简洁之余,GOPATH 给予我们信心相信 Go 对文件系统的真正操作姿势。机器不会产生晦涩的点后缀文件,没有平台专用的系统级目录 -- 只有一个选定的可控目录。
就是那么清楚、简洁和别具一格。
在我看来,GOPATH 采用其默认值(~/go
)会降低这份明确性 -- 新用户不再需要理解单一目录形式的工作空间的概念。
# GOPATH:命名风格
随之而来的是,GOPATH 为我们的目录提供了一个简单且干净的结构 -- bin/
、pkg/
和src/
三元组。每个文件夹的作用是显而易见的,看名字就能猜出来。
随着目录往下,结构参照 Go 的导入路径名,这个导入路径名也参照包的版本控制系统 URL。
我第一次看到 Go 里面的这个命名风格时,惊喜之余不免羞愧 -- 开发其他项目时为啥我自己一直没想出这个优雅的结构?!
没有接触到 Go 之前,我的 home 目录,除去非开发相关的东西不说,简直一团糟 -- 像 ~/Work/
、~/Projects/
、~/CompanyName/
、~/Soft
、~/ImportantProjectA
之类的目录组成了我传统的开发配置文件结构。当然,~/Soft
和 ~/Project
这样的文件夹日积月累了一大堆容易被忘记的名字和名字-版本元组标识的文件。更要命的是,没有快捷方式能够还原某个特定项目的来源或下载地址。
那些抱怨自己智能手机上无法卸载的应用程序的人呐,想必你是没见过我采用 GOPATH 之前的 HOME 目录呀!😃
从一开始,GOPATH 把所有代码放到一个地方,根据代码 VCS 的 URL 组织它们的想法就表现出其精妙和用处。现在一旦知道包源码的远程 git URL 或 Github 页面地址,不用过脑就可以知道其在本地磁盘的位置。反之亦然 -- 通过查看文件夹路径即可找到原始来源。
很快我就不自觉地在基于其他语言的软件项目采用了 GOPATH。我把 GOPATH 改为指向自己的 HOME 目录。现在个人涉猎过的所有软件都会放在 ~/src
,按照其 Github URL 来组织其文件结构。
GOPATH 使得我的文件系统更加有序和整洁。直至今日,我都沿用着这个设置,一直想不到更好的结构。欢迎改变我的想法~
# GOPATH:和代码玩耍
接下来我要说的是:直至研究 Go 的静态分析工具之后,我才领会到这么个点 -- GOPATH 使得代码分析工具开发者的工作变得如此简单。没有随系统变化的系统级特殊目录,没有不同目录用于区分不同 Go 版本,没有版本依赖问题(哈哈,这个是我的最爱)-- 只有一个单一的环境变量,调用一下os.Getenv()
就可以访问到你需要的所有代码。
它也使得我们这些凡人活得更加舒服了。说出来你可能不信:除非就职于 NASA,软件开发很少是单向地把说明文档转化为代码的。它总是一个双向的活动,一遍修补一遍试验,边创造边发现,就像 Escher's 画出来的手 -- 手画着手画着手... 我们需要快速变更局部程序及其依赖的能力,查看变更是如何改变程序行为的,更新我们设想的架构然后调整代码,如此往复。
这个简洁的调整能力往往没有得到足够的重视和领会。GOPATH 使得这项任务变得如何简单 -- 只要跳转到编辑器里面的任何代码,改变它,检查结果,然后决定下一步怎走。这些改动可以涉及到调试用的 printf
语句或触发拉取新代码的依赖修复。所有这些都不需要我们离开编辑器窗口。
有了 GOPATH,我总能确信以下两点:
- 可以随意改变依赖
- 明确知道当前所更改文件的文件系统路径
这些都能给我日复一日的活动带来满满的信心。
# GOPATH:盘活整个生态系统
最后是一个最隐晦而又难以解释的点。如我们所知,GOPATH 并没有试图解决版本问题。这个问题的解决被推迟下放到第三方工具去,并最终导向了 Go Modules。
这是个绝妙的解决方案。如果设计次优的解决方案乘于其实现成本要高于没有解决方案的话,那一开始就不要提供解决方案。这就像比特币解决数字金融系统里面数字身份问题的天才方案 -- 不要身份。GOPATH 里只有一个唯一的 “master” 版本,就这么整。
对于需要可半复现构建的非 monorepos 项目(工业界的大多数小到中级的代码库似乎都是这样的)比较痛苦,但是对于 monorepos 和开源软件来说简直不要太幸福 -- 它们基本都是一个样,一个超大的 monorepository。这对另一个不是那么明显的方向也是很友好的 -- 激励我们一直保持代码最新且与其依赖同步。
# 潜在的激励
鉴于个人预见具有争议性,让我在此啰嗦一下 -- 基于 GOPATH 的无版本模式驱动我们保持依赖最新。作为一个开源项目的维护者,我对这么个现象很感兴趣:任何时候用户都可以执行go get
,然后以最新版本的依赖来编译项目。
委婉一点表达,经历几次“无法构建”问题和/或 PR 之后,不管之前的理解如何,我们会逐步意识到维护依赖并非是零成本的。下次往自己代码添加其他 leftpad 库时,请三思。再久一点,你自然就会拥抱“小量复制要优于少量依赖”的说法 (opens new window)了。
我们也会逐步意识到其他项目依赖着我们的库,除非是安全更新或无法以其他方式实施的变更,我们都不想破坏它们的代码。我们的 Go 项目也会不自觉地遵循着 Go1 的兼容性承诺 (opens new window)。
GOPATH 创造出了这么个激励机制来自然地导向这些结论。
# 人与人之间的交流
许多时常通过锁定版本解决的依赖问题都可以通过人与人之间的交流解决。并不总是如此,但是要比我们想象的要来得多。
通过构造一种不同的设计或出于历史原因保留旧 API 的代码可以解决一些 API 破坏性的变更带来的问题。可能作者没有意识到有人依赖着她的项目,不知道新的变更会破坏其他人的构建。那就直接找作者谈话呗。她要是高兴的话,也许会找到一种更好的不会破坏 API 的解决方法,或者两种都保留,或者找一些其他解决方案,再或者协助你过渡。
对于我们如此低估开源世界里人类彼此交流的作用的现象,我一度震惊不已。交流要比锁定版本号要清爽得多。
GOPATH 代表的绝不仅是一个大目录。它是 Go 宇宙在我们本地磁盘的一个副本,是 Go 生态系统在我们机器的一个映像,如此活跃。
# 结语
我们的机器上有着数不清的 GOPATH -- 你的,你同事的,Go 作者,我的 -- 都是这个生态系统的副本。我们都尽力维持着彼此的同步。我们的共识是:趋向于保持库的完整以及它和其他版本主分支的同步。这把 Go 生态系统的动态塑造得和其他许多语言很不相同。
请欣赏以下出自 Andrei Kashcha (opens new window) 的 Go 宇宙的一个可视化动图 (opens new window)。
这里面的一些点有你的包、你贡献的包、你项目使用的包。
它们有自己的分组、集群,通过更多图中没有看到的节点(用户终端程序)连接到一起。这些用户终端程序躺在私有和企业级仓库里面或者永不离开我们机器的范围。但是他们依然把这些点连到了一起,使得 Go 的生态系统成为一个庞大而又活跃的有机体。
少了 GOPATH,这个活体组织就不可能存在。
和 GOPATH 说再见之后,我不知道这个活体组织的还会多活跃。还会有足够动力来保持这个巨大而不失美丽的活物生机勃勃和互连吗?
GOPATH,我依然爱着你。