包管理的重要性不言而喻。随着项目的推进,没有合适的包管理,每一次迭代都将成为开发者的噩梦。尤其是对于进行持续集成的项目,自动化应该深入骨髓。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是否规范
从这几个角度去评估一个依赖是否可靠,然后再决定是否把它用到自己的项目中。