包管理的重要性不言而喻。随着项目的推进,没有合适的包管理,每一次迭代都将成为开发者的噩梦。尤其是对于进行持续集成的项目,自动化应该深入骨髓。golang的import是其一大亮点,但也是它最被人诟病的缺陷之一。在最近的vendor化改造中,我对此深有感触。

Begining Of The Story

It was a drossy afternoon... 我正在啪啪啪地写BUG,同事A走过来拍了拍我的肩膀说:“我给你发钉钉怎么没回?快来帮我看看编译不过是怎么回事,有个冲突我感觉解决不了了……”

我问:“你搞了啥?”

A说:“我就是下了份最新的代码,然后就make不了了。”

我内心:“……”

其实这个问题早就是一个隐患了

我们的go项目并没有做包管理,只是写了Makefile去进行编译,原因有很多:

  • golang目前还没有比较权威的包管理工具
  • 官方并没有给出包管理的best practice
  • 项目开始时golang版本甚至不到1.5
  • 从小需求开始,并没有考虑到项目会变得非常庞大
Node.js

所以我们选择用Makefile的方式进行编译打包,每次构建都会:

GOPATHgo get
colossal project

项目有很多人参与进来,很多人在添加不同的功能进去,你很难再对整个项目有个全局的把控,你甚至不知道他们加了什么代码,总之上线之后不影响我的接口就好。

单纯用Makefile的缺点有很多:

  • 每次去go get拉取package,如果package更新了且不向下兼容,就会是个大问题。
  • 每次go get非常耗时,编译很慢,因为github非常慢

还有很多其它缺点后面会讲到,但是到目前为止这就是Makefile的缺点。但是以上两点并不是无法忍受,因为我们绝大多数包都是引用的内网gitlab上的包,是可控的,速度也很快。即使少数的github包,运气很好它们也没有出现不向下兼容的问题。

但总有漏网之鱼

同事A这次添加新功能之后进行编译,刚好依赖的某个包发生了break APIs的现象。这让我不禁想起了著名的墨菲定律:

Anything that can go wrong will go wrong
如果一件坏事可能会发生,那么它终将发生

Vendorize

vendorize
vendor化
紧跟官方的节奏
vendorGOROOTGOPATH

把项目依赖放到vendor目录里也有两种思路:

  • 把项目依赖的所有代码都下载下来放到vendor里,依赖也加入git管理
  • 像npm一样,只管理一个用于描述依赖的json文件,但是json文件能指定依赖的版本。
all in one
golang.org/xx/yygo getgo get

有时候做选择就是做权衡,没有万全之策。

当然还有godep这样的工具进行包管理,但是我们的原则是跟进官方的脚步,因此我们只考虑用vendor这种方式,于是可选的返回就缩小了。

govendor

Pick Up A Shit From A Range Of Shits

govendor
govendor
govendorvendor.json
govendor initgovendor fetch packagegovendor syncnpm installGOPATH/.cache
govendorvendor.jsongovendor syncshell脚本Makefile
govendorgovendor

govendor的vendor没有层次

vendor和“层次”在一起是什么概念?或者说什么样的才是有vendor层次?考虑这个例子吧:



main中引用PackageA和PackageB的V1版本,PackageA引用了PackageB的V2版本。这样的情况下会发生什么情况呢?

对main来说,它就是引用了PackageA和PackageB,不用去关心PackageA是否引用了其它包,只要PackageA run as expected就行了。换句话说,main下的vendor中只应该存放两个它直接引用的包,即PackageA和PackageB(V1),PackageA对它应该是个黑盒。PackageB(V2)应该放在PackageA的vendor中,这样即使V1和V2版本不兼容,这个代码也是可以按预期运行的。

govendor
node_modulesnode_modulesnode_modules

bar引用了baz,如果bar的vendor中没有baz,即使main的vendor中有baz,也无法引用。只能去GOPATH中查找baz。

所以,package本身加上它依赖的vendor才是一个完整的包。

govendor没有去解析依赖中的vendor.json,我觉得是有问题的。要不要造一个轮子?This is a question

改造方案

vendorizationsrc/github.com/foo/barcommon/thrid_party

方法其实也很简单,就是每次在构建时,把common目录也clone下来,然后把common/third_party也加入GOPATH,同时不要把common相关的依赖加入到我们自己的vendor.json里。整个过程如下:

  • git clone common
  • export GOPATH=pwd:common/thrid_party
  • cp all files to pwd/src/group/project
  • cd pwd/src/group/project && govendor sync
  • go build

这一方案的缺陷在于:

golang.org/x/syx/unixgovendor syncrecursive vendor

对于上述的缺陷一,目前的解决方案是把这类包从vendor.json里删除,直接把代码下载下来,用git进行管理,依赖跟着项目走。当然这也只是暂时的,更好的方案是像淘宝一样做一个类似于cnpm的package镜像hub,定时同步墙外的依赖,通过国内的CDN进行加速。当然目前来说这是不现实的。网上也有类似的项目,比如gopm.io。但是尴尬的是,这个站点不翻墙根本上不去……

缺陷二,目前没有特别好的解决办法。我已经给项目组提了issue,但是并没有什么卵用。最近需要仔细研究研究他govendor项目,看看是不是有这样的功能只是文档没有注明,实在不行就只能fork一份自己造轮子了。

总结

对于没有官方集中式package repository的社区,不论哪种语言都会存在或多或少的包管理的问题。Dependency译为依赖,依赖意味着信任,因此你需要对引用第三方包持有更加审慎的态度。对于一个第三方包的可靠程度,我大致列了以下几个评估项:

  • star数
  • 生产环境使用程度
  • 文档是否简洁明了
  • 代码活跃程度
  • close issue数
  • issue解决速度
  • release是否规范
    从这几个角度去评估一个依赖是否可靠,然后再决定是否把它用到自己的项目中。