Go的错误处理和泛型

有段时间没有写Go的代码了,前些日子需要做一些Dump文件分析,写了一个小工具auto_bugcheck,发现以前的Go语言保持了很好的兼容性,也特别适合写一些命令行工具,很快的完成了任务。

背景

2015年左右我在大量用Go的时候应该还是Go1.1~Go1.2版本,近4年来最大的变化可能就是对第三方包的依赖管理(go module)的支持,这是对三方引用的一个元数据,类似于.NET下的packages.xml,这一块我以前是通过git submodule来引用三方包,其实还是有些使用上的问题的,现在语言内置支持解决的更加优雅。

其实社区讨论最多的,目前还没有实现莫过于Go的错误处理和泛型了。对于错误处理,从.NET/Java/Python带重型运行时的语言背景来说,没有类Exception似的错误处理简直不可接受,这点有点像用Visual Studio写C#写惯了,直接用Vim编写Ruby/Python等没有智能提示无法接受一样,居然写代码没有智能提示——这样能写出代码来吗?其实不仅能,而且能写出更加优秀的代码。因为没有智能提示,你会阅读标准库,三方库的手册,这会强迫你了解标准库是怎么组织他的对外接口,或是三方库是怎么组织对外接口的,不仅能更加了解你要使用的API本身,而且你对库的设计更加了解,使用上也更加的合理——选择更加适合的接口。而对于智能提示来说,你只能靠感觉,其实猜的成分更重一点,比如在.NET中你想排序一个列表你会一个点,然后输入Sort,不是说不对,而是这更多是直觉上的事情。

更喜欢现在的错误处理方式

对于Go的错误处理类似,其实写了越多的代码,你会发现影响代码可读性的往往不是代码的绝对长度。而是代码对现实理解的映射,这映射复杂度往往决定了代码本身的可读性。Go的if err != nil错处处理本身并不会增加复杂度,对错误处理的复杂度对于严肃应用来说是必要的,不可省略的复杂度。不是说你来一个大大的try...catch就是降低了复杂度,而是你将问题掩藏了,出现错误时候程序的行为更加不可预知,同时也给排错增加了难度。Go的错误优势体现在两点:

在实践中,采用这方式的代码最:

排错时间通常不是花在使用什么样的方案去fix,而是找到在哪里出错。

Go目前提的两种错误方案我自己觉得都不像Go,一点也不工程化。我们来看看问题在哪里:

我还会在以后的代码中优先使用传统错误处理方式。

泛型 - 不能脱离数据结构讨论算法

上学的时候搞不清楚为什么数据结构书中讲的东西其实是算法。其实数据结构和算法是紧密相关的,我们不能脱离数据结构单独讲算法。Golang的官方blog的why generics很好说明了这点。数据结构决定了可以对数据施加的操作。比如数组可以按索引来存取,无论是动态数组还是静态数组都可以取到长度属性。从这点来讲其实想把通用算法留下来,我们必须留下算法,把类型信息剥离(factor out)出去。而这必然存在一个问题,既然类型信息剥离出去了,我们怎么能保证传入的参数支持特定的操作呢?即类型可以剥离出去,但是可以对类型施加的操作确实支撑算法实现本身的基础。那现在问题变成了,你不可能将类型完全剥离出去(factor out),你能剥离的是类型,而不是可以对这个类型施加的操作。所以:

泛型 = 通用算法+数据可施的加操作
类型 = 数据+数据可施加的操作

怎么样描述数据可施加的操作是泛型实现的难点所在了,你需要将这部分对着类型看着很自然的东西,人为的剥离开来,这部分简单不了。在.NET中使用where关键字指定数据可施加的(公共)操作。Go语言的泛型实现的Draft中阐述的contract也是解决一样的问题,即表述通用算法适用的前提——数据支持特定的操作。

这点Go和.NET的实现上并没有根本的不同,但是.NET利用了现有的接口和类型继承,并没有给语言本身添加新概念。而Go并没有利用他的非侵入式接口和组合性,而是添加了新的contract概念其实是增加了语言的学习成本的。目前看唯一的好处是这个概念对语言本身来说是正交的,可以任意的和现有的类型进行"组合"。

这种把对类型可施加操作利用contract剥离出来的做法目前我还没有实践,也不知道哪种更加工程化,直觉上还是喜欢.NET的泛型实现多一点。

@ 2019-08-01 23:28

Comments:

Sharing your thoughts: