$GOPATH 耦合之殇

写在前面,

写这篇文章的初衷,是想向初学者解释为什么需要设 $GOPATH,且为什么要把代码放到 $GOPATH 下面。另一方面,最近 go 1.11 发布,新推出包管理工具 go modules 弃用了 $GOPATH,这里也是替 go modules 站站队,说明了用 $GOPATH 的痛点。


这天,作为优秀程序员的你,决定学习一下莫名火起来的 golang,你很厉害,花了一个小时便安装好了开发环境,配置好了 $GOPATH、$GOROOT 之类的环境变量。

一切还是要从 Hello World 开始,你迅速学习了一下 golang 的基本语法,写了一个 Hello World 代码放在桌面的 hello-world/main.go 里,执行 go build main.go,成功编译,成功执行,你很开心。

小有成就的你,决定写一个项目练练手,你新建了一个 golang 的源文件放在桌面的 tiny-project/main.go

这个项目有需要用到 github.com/xxx/yyy 这个三方包,这难不倒你,你执行 go get github.com/xxx/yyy,成功安装了这个包。你知道,这次安装其实就是把 https://github.com/xxx/yyy 上存放的代码下载到 $GOPATH 下面,编译器会去 $GOPATH 找所需要的包的代码。

这个小项目是迭代式开发的,代码越来越多,优秀的你知道不应该把所有代码都挤在一个 main.go 文件里,于是你是拆分成了多个文件:

main.go
logic.go
dao.go
entity.go

但是在编译时,你执行 go build main.go 是发现报错了,报告很多程序实体的定义没有找到,你搜索了一下,找到一个方案解决了这个问题,即执行 go build main.go logic.go dao.go entity.go,但是作为一个优秀的程序员,你敏锐的感觉到这样做是不优雅的。于是你进一步发掘了一下这个问题,明白了过来,你可以单纯执行 go build 就可以编译一个包,不需要指定额外的参数。

这时候你领悟了过来,原来对于 golang 来说,你写的这个 tiny-project 也应该是一个包,与 golang 的其他三方包没有什么太大的区别,所以要想规规矩矩成为一个包,你的这个小项目也应该按照 golang 包的命名规则,放到指定的文件路径下。

你思量了一下,把你的 tiny-project 放到了 $GOPATH/src/tiny-project.com/tiny-project-lib/tiny-project/,完成后你撇撇嘴嘟哝了几句,原来,包名中的 tiny-project.com 是你随便写的,你并没有这个域名的所有权,你也没有把代码放到 http://tiny-project.com 这个网站上,tiny-project 的代码你一直是托管在你自己私有的一个 SVN 上,并不打算公开,但是现在,你不得不假装它公开在 http://tiny-project.com/tiny-project-lib/tiny-project

虽然有些怪怪的,但是好在你在终于能方便地编译了,只需要在 $GOPATH/src/tiny-project.com/tiny-project-lib/tiny-project/ 下执行 go build 即可,方便多了。

项目继续开发,文件阅读越多,你意识到应该开一个子文件夹,于是,项目结构变成了:

main.go
logic.go
dao.go
entity/
   user.go
   product.go
   transaction.go

这时候你发现,原来对于 golang 来说,新建了一个文件夹就意味着新建了一个新包,这个包是tiny-project.com/tiny-project-lib/tiny-project/entity,从此,你项目中的 dao.go 想引用的 entity 下定义的实体时,虽然文件路径近在咫尺,却需要写:

import tiny-project.com/tiny-project-lib/tiny-project/entity

你觉得有一点啰嗦,想换成 import ./entity,你知道这样是可行的,但这时候蹦出来一个资深 golang 程序员,阅码无数,他告诉你,几乎没有人会在 golang 中使用相对引用。所以,你不想成为异类。

终于有一天,你的小项目瓜熟蒂落,变得相当复杂但也相当有用,你的朋友希望你开源,于是你决定把项目移动到 GitHub 上: gihub.com/tiny-project-lib/tiny-project,问题来了,你意识到项目的移动也意味着包的重命名,而包重命名意味着代码的文件路径需要更改,而更难受的是,竟然还需要修改代码,引用 entity 的代码需要改为:

import gihub.com/tiny-project-lib/tiny-project/entity

至此你发现,

$GOPATH 指向的路径,决定了代码的文件路径;

代码的文件路径,决定了包名;

包名,决定了包在互联网上的存放位置;

包名,也决定了代码里怎么写 import。

以上任何一个环节的改动,就会牵扯出其他环节的一系列改动。

这便是 $GOPATH 耦合之殇。


写在后面,

事实上,文中提到一些“必须修改”,在技术上确实可以做到一些规避的,比如 $GOPATH 可以指定多个文件夹、文件夹名不一定要和包名一致、go get a.com/xxx/yyy 也可以是从 GitHub 上下载源码,但是文中所说的“耦合”,更多时候是基于约定上的耦合,即:它在具体实现时不一定非要是这个样子,但在约定上,它应该是这个样子。

所以读者非要较真,说这些耦合可以通过怎样怎样解除,我是没有异议的。但是,改一个具体的项目容易,改整个生态系统的约定难,尤其,不是所有的约定都是应该被粗暴打破的,可能通过升级、补充来加以完善才是让大家满意的做法。

期待 go modules 打破僵局。