七叶笔记 » golang编程 » 比Python还方便的Go语言要出2.0了,你想怎么设计?

比Python还方便的Go语言要出2.0了,你想怎么设计?

机器之心整理,机器之心编辑部。

在昨天的 Go contributor 年度峰会上,与会者对错误处理和泛型的设计草案有了一个初步的了解。Go 2 的开发项目是去年宣布的,今天谷歌公布了这一语言的更新。

作为 Go 2 设计进程的一部分,谷歌发布了这些设计草案,以激发社区关于以下三个话题的讨论:泛型(generics)、错误处理和错误值语义(error value semantics)。

这些设计草案不算 Go 提案流程意义上的提案。它们只是激发讨论的引子,最终目的是给出足够好的设计并将其转变为实际提案。每种设计草案都附带一个「问题概述」,其作用是:(1)提供语境;(2)为包含更多设计细节的实际设计文档做准备;(3)推动关于设计框架和说明的讨论。问题概述会提供背景、目标、非目标、设计约束、设计的简要总结、对重点关注领域的简短讨论以及与先前方法的比较。

再次重申,这些只是设计草案,不是官方提案。现在没有相关提案事宜。谷歌希望 Go 的所有用户都能够帮助其改进草案并将草案完善为 Go 提案。为此,谷歌创建了一个 wiki 页面来收集并组织关于每个话题的反馈。谷歌希望用户帮助其更新这些页面,包括添加用户自己的反馈链接。

简介

本概览及附带的细节草案是《Go 2 设计草案》(Go 2 Draft Designs)文档的一部分。Go 2 的总体目标是为 Go 无法扩展到大型代码库和大量开发人员这一问题提供最重要的解决方式。

Go 编程无法成功扩展的一大原因在于错误检查和错误处理代码的编写。总体来看,Go 编程代码检查错误太多,但处理这些错误的代码却非常不足(下文将给出解释)。该设计草案旨在通过引入比当前惯用的「赋值和 if 语句」(assignment-and-if-statement)组合更轻量级的错误检查语法来解决这个问题。

作为 Go 2 的一部分,谷歌还考虑对错误值的语义进行更改,这是一个单独的关注点,但是本文档仅涉及错误检查和处理。

在 Go 开源之前,Go 团队成员——尤其是 Ian Lance Taylor——就一直在研讨「泛型」的可能设计(即参数多态,parametric polymorphism)。谷歌从 C++ 和 Java 的经验中得知,这一话题非常丰富、复杂,要想考虑透彻并设计出一个良好的解决方案将花费很长时间。谷歌一开始并没有尝试这一做法,而是将时间花在了更直接适用于 Go 网络系统软件(现在的「云软件」)这一初始目标的功能上,例如并发性、可扩展构建和低延迟垃圾收集。

Go 1 发布之后,谷歌继续探索泛型的多种可能设计。2016 年 4 月,谷歌发布了这些早期设计(#)。作为 Go 2 再次进入「设计模式」的一部分,Go 团队再次尝试探索泛型的设计,希望泛型能与 Go 语言融合,为用户提供足够的灵活性和表达性。

在 2016 和 2017 年的 Go 用户调查中,某种形式的泛型是最迫切的两个功能需求之一(另一个是包管理)。Go 社区维护一份「Go 泛型讨论摘要」(Summary of Go Generics Discussions)文档。

许多人错误地以为 Go 团队的立场是「Go 永远不会有泛型」。但这并非事实,谷歌知道泛型的潜力,它能让 Go 更加灵活、强大、复杂。如果要增加泛型,谷歌想在尽量不增加 Go 复杂度的前提下努力提高其灵活度,并使其更加强大。

错误处理:问题概览

为了扩展至大型代码库,Go 程序必须是轻量级的,没有不适当的重复,且具备稳健性,能够优雅地处理出现的错误。

在 Go 的设计中,我们有意识地选择使用显性的错误结果和错误检查。而 C 语言通常主要使用对隐性错误结果的显性检查,而很多语言(包括 C++、C#、Java 和 Python)中都出现的异常处理表示对隐性结果的隐性检查。

目标

对于 Go 2,我们想使错误检查更加轻量级,减少用于错误检查的 Go 程序文本量。我们还想更加方便地写处理错误的程序,提高编程人员处理错误的可能性。

错误检查和错误处理必须是显性的,即在程序文本中可见。我们不想重复异常处理的缺陷。

现有代码必须能够继续运行,且和现在一样有效。任何改变都必须能够实现对现有代码的互操作。

如前所述,该设计的目标不是改变或增强错误的语义。

错误值:问题概览

大程序必须能够以编程的方式测试错误和作出反应,还要报告这些错误。

由于错误值是实现 error 接口的任意值,Go 程序中有四种测试特定错误的传统方式。一,程序可以使用 sentinel error(如 io. EOF )测试它们的等价性。二,程序能够使用 Type assertions 或 type switch 检查错误实现类型。三,点对点检查(如 os.IsNotExist)检查特定种类的错误,进行有限的解包。四,由于当错误被封装进额外的上下文中时,这些方法通常都不奏效,因此程序通常在 err.Error() 报告的错误文本中进行子字符串搜索。很明显,最后一种方法最不可取,即使是在出现任意封装的情况下,支持前三种方法更好。

目标

我们有两个目标,分别对应两个主要问题。一,我们想使检查程序错误的过程更加简单,出现的错误更少,从而改善错误处理和真实程序的稳健性。二,我们想以标准格式打印出具备额外细节的错误。

任何解决方案必须能够使现有代码正常运行,且适合现有的源树。尤其是,必须保留使用 error sentinel(如 io.ErrUnexpectedEOF)对比是否相等以及测试特定种类的错误这些概念。必须继续支持现有的 error sentinel,现有代码不必改变成返回不同错误类型。即扩展函数(如 os.IsPermission)来理解任意封装而不是固定集是可行的。

在考虑打印额外错误细节的解决方案时,我们偏好于使用 golang.org/x/text/message 使定位和翻译错误成为可能,或至少避免不可能。

包必须继续轻松定义其错误类型。定义新的通用「真实错误实现」是不可接受的,且使用这种实现需要所有代码。对错误实现添加很多额外要求也是不可接受的,这些错误实现只涉及到几个包。错误还必须能够高效创建。错误并非异常。在程序运行期间,生成、处理、丢弃错误都是很平常的事。

很多年前,谷歌一个用基于异常(exception-based)的语言写的程序被发现一直生成异常。最后发现,深层嵌套堆栈上的函数尝试打开文件路径固定列表中的每个路径去寻找配置文件。每个失败的打开操作就会导致一个异常;异常的生成浪费了大量时间记录这个深层执行堆栈;之后调用器丢弃了所有这些工作,继续进行循环。在 Go 代码中错误的生成必须保持固定的开销,不管堆栈深度或其他语境如何。(延迟的处理程序在堆栈解开之前运行也是由于同样的原因:关心堆栈上下文的处理程序能够检查活跃的堆栈,无需昂贵的 snapshot 操作。)

泛型:问题概览

为了推广 Go 语言的大型代码库和开发者的贡献,提高代码的复用性就显得非常重要。实际上,Go 语言早期的关注点只是确保能快速构建包含很多独立软件包的程序,因此代码的复用成本并不是很高。Go 语言的关键特征之一是它的接口方式,这种方式同样也直接定位于提高代码复用性。具体来说,这种接口可以写一个算法的抽象实现,从而消除不必要的细节。例如,container/heap 在 heap.Interface 操作上以普通函数的方式提供了堆维护(heap-maintenance)的算法,这使得 container/heap 适用于任何备用储存,而不仅仅只是一些值。接口的这些属性令 Go 非常强大。

与此同时,大多数希望获取优先级序列的 编程器 并不希望为算法实现底层存储,然后再调用堆算法。这些编程器更愿意让实现自行管理它的数组,但是 Go 不允许以 type-safe 的方式表达它。最接近的是创建 interface{} 值的优先序列,并在获取每一个元素后使用类型断言。

多态 变成不仅仅是数据容器。我们可能希望将许多通用算法实现为朴素的函数,它们能应用各种类型,但是我们现在在 Go 中写的函数都只能应用于单个类型。泛型函数的示例可能为如下:

// Keys returns the keys from a map.

func Keys(m map [K]V) []K

// Uniq filters repeated elements from a channel,

// returning a channel of the filtered data.

func Uniq(<-chan T) <-chan T

// Merge merges all data received on any of the channels,

// returning a channel of the merged data.

func Merge(chans …<-chan T) <-chan T

// SortSlice sorts a slice of data using the given comparison function.

func SortSlice(data []T, less func(x, y T) bool )

目标

谷歌的目标是通过带有类型参数的参数多态性来解决 Go 语言库的编写问题,这些问题抽象出了不必要的类型细节(如上所述)。

除了预料之中的容器类型外,谷歌还希望能编写有用的库来操作任意的 map 和 channel 值,理想的方案是编写能在 []byte 和 string 值上运算的多态函数。

允许其它类型的参数化并不是谷歌的目标,例如通过常数值进行参数化等。此外允许多态定义的专有化实现也不是目标,例如使用比特包装(bit-packing)定义一个通用的 vector <T> 和特定的 vector<bool>。

我们希望能从 C++和 Java 的泛型问题中学习经验。为了支持软件工程,Go 语言的泛型必须明确记录对类型参数的约束,以作为调用者和实现之间的明确强制协议。但调用者不满足这些约束或实现本身就超出了约束时,编译器报告明确的错误也非常重要。

在没有棘手的特殊情况和没有暴露实现细节的前提下,Go 语言里的多态性必须平滑地适应到环境语言中。例如,将类型参数限制到机器表征为单个指针或单个词汇的情况中是不可接受的。还有另一个例子,一旦以上考虑的通用 Keys(map[K]V) []K 函数被初始化为 K=int 和 V=String,它必须和手写的非泛型函数在语义上同等地处理。特别是,它必须可分配给类型变量 func(map[int]string) []int。

Go 语言中的多态性应该要在编译时和运行时实现,因此用于实现策略的决策还可以用于编译器,并与其它任何编译器优化一视同仁。这种灵活性将解决泛型困境。Go 语言在很大程度上都是一种直观且易于理解的语言,如果我们要添加多态性,就必须保留这一点。

相关文章