大家好,我是煎鱼。


(资料图)

平时我们经常会进行网上冲浪,学习经验、知识以及吃瓜。在代码界,还有同学调侃我们就是 c+v (复制粘贴)工程师。

我的专用快捷键:

在 Go 语言中,有一句谚语也指出了 ”复制“ 的有益之处,叫做:"A little copying is better than a little dependency"(复制一点总比依赖一点好)。

重点关键字是:复制,依赖。

复制一点 vs 引入依赖复制,只要核心

如果可以自己写一些短小精悍的代码,那就没有必要直接导入一个库去做(可以只复制核心算法)。

例如 UUID 的案例:

func main() { f, _ := os.Open("/dev/urandom") b := make([]byte, 16) f.Read(b) f.Close() uuid := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) fmt.Println(uuid)}

虽然有很多 UUID 的第三方库,但普遍会有许多功能堆积在一个库中,这样会引入许多不必要的新依赖。

如果只是要一点新功能,可以自己简单实现,封装为公司内部方法导入。

可以有效减少依赖管理的负担,缩小二进制文件大小,带来更大的稳定性、安全、测试第三方库这方面大多都是不清楚的。

引入大依赖,易折腾

指向的副作用是在我们引用依赖了太多的东西时,会导致产生一个应用,依赖过多的场景:

比较经典的是微服务的依赖。更贴近我们的场景,那就是 Go modules 中带来的各第三方组件库的版本互相制衡了。

最小版本选择

以下介绍的是 Go Modules 的最小版本选择的计算规则,其会带来版本间的互相制衡。

一个模块往往依赖着许多其它许许多多的模块,并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本,如下图(来自Russ Cox):

在上述依赖中,模块 A 依赖了模块 B 和模块 C,而模块 B 依赖了模块 D,模块 C 依赖了模块 D 和 F,模块 D 又依赖了模块 E,而且同模块的不同版本还依赖了对应模块的不同版本。那么这个时候 Go modules 怎么选择版本,选择的是哪一个版本呢?

我们根据 proposal 可得知,Go modules 会把每个模块的依赖版本清单都整理出来,最终得到一个构建清单,如下图(来自Russ Cox):

我们看到 rough list 和 final list,两者的区别在于重复引用的模块 D(v1.3、v1.4),其最终清单选用了模块 D 的 v1.4 版本。

真实场景

在 Go RPC 的使用中,gRPC 的应用是非常广泛的。而 gRPC、grpc-gateway、protoc(含对应语言的 plugin)、etcd,几者的版本是会有不兼容的情况的。

例如:gRPC 本身会做一些实验性的 package,etcd 在 v3.5.0 前没有 Go modules 的良好版本管理,同时 protoc 的高版本又会对 gRPC 的版本有一定的要求,会形成各第三方库对各库版本有要求的情况。

在内部框架或应用中,我们常常会通过 go.mod 来声明所使用的版本。但在 ”最小版本选择“ 的存在下,其遵守版本化,一旦依赖的另外一个库,要求更高的 gRPC 版本,就会打破这个平衡。

最近一次见到的,就是公司内有人使用 TIDB 的库,只是使用了某一块东西,但却导致大量被依赖的版本被动升级。

最终这位同学就采取了复制一点的做法,解决了增加大量依赖的副作用。

总结

实际上 Go 的这句谚语 "A little copying is better than a little dependency",更多的是一种软件工程里的指导思想。

当你只是涉及到一个很简单的功能,那完全可以自行实现或复制核心代码。没必要直接导入一个大的第三方库,它有可能带来许多奇奇怪怪的依赖,使得你的编译构建变得缓慢,依赖管理也复杂了起来。

这是需要我们都好好思考的。

推荐内容