写点什么

Golang 中依赖管理的灾难

2016 年 8 月 15 日

我在 Golang 的世界里已经遨游一段时间了,虽然远说不上精通,但是我还是发现了一些自己不太喜欢抑或不太适应的东西。通常对我来说,在决定为一样事物投入比较多的精力之前,我都会做充分的了解,在生活中选择是否看某部电影是这样,coding 时也是这样。有充分的质疑,才会有充分的理解不是吗?:P

依赖管理在软件工程中一直是一个比较核心的话题。一个人的力量是有限的,在较大规模的软件开发中,你总是需要与别人协作编写代码,或是充分借助开源世界的力量。软件在它的有效生命周期内是一直在迭代的,那么在你依赖的这些"外部"代码库演进的过程中,如何保证自己的代码或工程是可以在任何时间任何地点重复编译重复部署的,而不会出现满屏的红色编译错误告警,甚至是莫名其妙的运行时崩溃?在 Java 世界中,通过 maven 工具和 gradle 工具编辑依赖清单列表 / 脚本,指定依赖库的位置 / 版本等信息,这些可以帮助你在合适的时间将项目固化到一个可随时随地重复编译发布的状态。这些工具对我来说已经足够优雅有效(虽然 maven 中也有不同依赖库的内部依赖版本冲突等令人心烦的问题)。

在初步了解了 Golang 中是如何管理依赖后,我确实有些失望。自然的,Golang 官方团队也早已意识到语言內建的依赖管理机制不够理想,需要有一个优雅而统一的解决方案。因此,如果你打算建立一个包含外部依赖的 Golang 工程,不妨先参考下官方建议

那么,让我们来看一看 Golang 的依赖管理问题到底是怎么回事吧。

依赖

假设你已经自己设置好一个 Golang 的开发环境和工作目录 (workdir) 了,然后我们在 workdir 里先创建一个新的 playground 工程:

复制代码
~/workdir/src/playground/

接下来在 playground 工程内新建 main.go 文件,并且使用一个开源的外部 http 库( echo 框架)提供一个简单的"hello world" http 服务。

复制代码
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/engine/standard"
)
func main() {
e := echo.New()
e.Get("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!\n")
})
e.Run(standard.New(":1234"))
}

这里我们有三个依赖:net/http 是 Golang 的标准库,另外两个是外部依赖–echo 框架。你只需要在 import 代码块中声明所需的依赖库,Golang 就会自动帮你去拉取它们。怎样运行这个简单的 http server 呢,执行以下命令(假设 GOPATH 环境变量已被设置为~/workdir):

复制代码
cd ~/workdir/src/playground/
go get ./...
go run main.go

声明依赖,执行几个 shell 命令就可以直接把工程跑起来,看起来相当简单明了。但是,事实真的如此吗?

问题在哪里

还记得我之前说过的吗,软件在它的有效生命周期内是一直在迭代的,也就是一直在变化的。如果我们依赖的 echo 框架改变了它的 API(比如方法签名变更,过时方法删除等等),playground 工程还能正常编译运行吗?项目的可靠交付受制于第三方的开源代码库,这是我们和客户都不能接受的!尽管在类似 GitHub 的开源平台上,知名的开源软件都尽力的保持着自身的可靠性和 API 的一贯性,但是作为软件开发人员,我们很清楚事与愿违的事情还是经常发生不是吗?总有一天,我们会发现当我们想要去升级一个老系统,迁移使用新版、性能更高的外部依赖库时,之前的代码都不工作了。更糟的是,如果没有做好模块或调用链的解耦,没有对外部依赖库做进一步的封装,工程内部到处是外部依赖库中"不稳定"API 的调用,在大型的软件系统中,基于这样的遗留代码的重构就是个灾难!(熟悉的"满屏飘红"又回来了)

与外部依赖库的最新版本保持一致的风险实在太大,如何避免这种后向兼容性被打破的情况?有一些团队的做法是直接将外部依赖库的某个特定版本代码进行拷贝并纳入到自身的版本控制系统中去。据我所知,Google 内部当时是这么做的。事实上,我所在的团队初期也是这么做的。这样,所有项目所需的依赖都使用被纳入到内部版本控制系统中的版本,这些代码由专人或团队去维护,一来可以统一不同团队使用的外部库版本;二来最大可能的降低了 API 被更新升级打破的可能性。

好吧,那就把代码拉进 svn/git 吧……现在没有问题了吧?让我们来看看工作目录的情况吧。之前我们执行了这条命令:

复制代码
go get ./...

如果进入 ~/workdir/src/github.com/ 目录,你就会发现labstack, mattnvalyala 这三个目录。labstack 目录是 echo 框架所在的目录,没有问题。mattnvalyala 则是 echo 框架本身所依赖的外部库,读到这里你是否已经嗅到了一丝危险的味道?如果你编写的项目依赖 mattn,并且也使用了 echo 框架,那么如果 echo 框架依赖的 mattn 版本和你依赖的版本不一致怎么办?

我们先搁置上面的问题,假设已经决定把外部依赖都纳入到版本控制系统中。好,这意味着,在 GitHub 上开源的代码库,对应本地的 ~/workdir/src/github.com/目录,必须受版本控制。另外,你会说,并不是所有的外部依赖库都来自 GitHub 吧,也有可能是 bitbucket、golang.org 等等,那么是不是要把整个 ~/workdir/src/ 目录都纳入版本控制呢?粗暴,丑陋……

生产环境的实践

在生产环境中,确实有一些团队就是按照上面所说把所有外部依赖纳入到自身的版本控制系统中去的。这样他们能够控制所有的依赖。但是要注意的一点是,在编写代码时,所有的导入路径(import path)必须被修改为使用内部版本控制系统管理的库版本。比如原先

import "github.com/labstack/echo"

这样的代码必须修改为类似下面这样:

import "own_dependency_root_path/labstack/echo"

你不仅要对自己的源码做更改,还必须包含依赖库的源码,以及依赖库的依赖等等,一直递归下去。有一些自动化的工具可以帮我们做这些事,但这整个方案仍然显得粗暴丑陋。

幸好,基于 Golang 的官方建议,我们已经可以找到许多不错的解决方案。

Golang 在 1.5 版本引入了实验性的 vendoring 标记GO15VENDOREXPERIMENT,并且从 1.6 版本起使 vendoring 机制成为默认行为,不再需要额外配置。现在,如果一个 package 或它的父目录内包含 vendor 目录,它就会被作为 import 指令的依赖搜索路径。有很多工具帮我们轻松的做到这点,例如 godep ,那么,挑一个顺手的,去阅读他们的使用文档吧~

总结

Golang 开发社区一直秉承着"简约"的宗旨,尽力不去给用户增加额外的设计上的负担。然而依赖管理的处理,确实是一个需要谨慎而精心设计的技术点。Golang 官方团队对此也十分重视,它的重要性甚至超过软件工程中普遍认同的"DRY"(“Don’t Repeat Yourself”,强调代码的复用) 原则:

“Through the design of the standard library, great effort was spent on controlling dependencies. It can be better to copy a little code than to pull in a big library for one function. Dependency hygiene trumps code reuse.” - Go at Google

Golang 官方团队强调的是代码的洁净 / 清晰度胜过代码的复用。在 Golang 的世界里,这很可能是一种非常值得借鉴的编程哲学。

那么最后,结合官方团队对依赖管理的建议,这里也给出一些我在 Golang 日常实践中提炼出的指导性思路供参考:

  • 当你开源类库或者提供代码作为公司内部的公共库时,请尽量的少用第三方库,推崇使用标准库。
  • 发布的类库,尽量辅以版本管理(版本固化),例如使用 gopkg 来管理版本。
  • 使用官方推荐的工具来管理依赖。如果你有自己的想法,可以直接对这些官方推荐的工具做贡献,加入社区共同解决问题。

作者

Mr.Key,目前就职于途牛旅游网,关注 DevOps,云计算平台,分布式系统


感谢木环对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 8 月 15 日 17:037129

评论 1 条评论

发布
用户头像
在官方go vendor出现之前,就已经有很多第三方的依赖管理软件了。
2020 年 02 月 07 日 10:16
回复
没有更多了
发现更多内容

热情空前,家长纷纷变身“寒假规划师”,如何抓住这波热潮?

ZEGO即构

AI 在线教育 在线课堂

WebRTC 的现状和未来:专访 W3C WebRTC Chair Bernard Aboba

阿里云视频云

WebRTC

《我想进大厂》之分布式事务篇

艾小仙

Java 面试 后端

简化业务代码开发:看Lambda表达式如何将代码封装为数据

华为云开发者社区

函数式接口 数据 代码 函数 lambad

SpringCloud 从入门到精通 11---Nacos负载均衡

Felix

惊喜来袭!253页全彩免费电子书《Python 编程参考》正式上线发布

Python编程参考官方账号

Python go redis 程序设计

【有奖调研】中国人工智能开发者调研

百度大脑

阿里架构师深入讲解Android开发!教你一种更清晰的Android架构!BAT大厂面试总结

欢喜学安卓

android 程序员 面试 移动开发

iTerm2 实现 ssh 自动登录,并使用 Zmodem 实现快速传输文件

米开朗基杨

iterm2

redis持久化怎么选?成年人从来不做选择...

moon聊技术

我所认为的产品经理能力模型

day day up

Java 程序经验小结:返回零长度的数组或集合,而不是null

后台技术汇

28天写作

Soul网关源码阅读番外篇(一) HTTP参数请求错误

Java 源码阅读 网关

COCO聊天挖矿系统开发|COCO聊天挖矿软件APP开发

开發I852946OIIO

系统开发

是找茬?还是装B?阿里面试每轮必问的“Spring Boot”意义何在?

比伯

Java 编程 架构 面试 计算机

《2020年微信视频号研究报告》 | 视频号 28 天 (11)

赵新龙

28天写作

TarsBenchmark | 服务性能压测利器

TARS基金会

微服务 压力测试 TARS

QA为什么转换角色

BY林子

软件测试 QA 职业发展

盘点2020 | 百度AI的2020

百度大脑

盘点2020

案例加源码:万字长文带你彻底搞懂MySQL的索引优化

程序员小毕

MySQL sql 源码 性能优化 索引

2020中国ToB独角兽:估值逆势起飞,寡头效应加剧

ToB行业头条

阿里架构师经验分享!Android面试知识点总结宝典助你通关!顺利通过阿里Android岗面试

欢喜学安卓

android 程序员 面试 移动开发

阿里巴巴2021年最新开源十亿级Java高并发系统设计手册

Java架构追梦

Java 阿里巴巴 架构 并发 系统架构设计手册

作业1

瑾瑾呀

IM即时通讯实现的原理

v16629866266

使用Apollo升级一下yml文件管理和发布

Sky彬

springboo

合约跟单交易软件系统开发|合约跟单交易APP开发

开發I852946OIIO

系统开发

源中瑞情报智能研判预警平台开发,合成作战系统建设方案

WX13823153201

架构师系列 14 PageRank算法

桃花原记

架构师 3 期 3 班 -week8- 作业

zbest

作业 week8

iOS音视频--视频合集

程序员 音视频 OpenGL ES GPUImage Metal

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

Golang中依赖管理的灾难-InfoQ