Python 和 JavaScript 中的五级错误处理
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
介绍
几周前,我在 OpenSlava 2020 大会上发表了演讲,主要内容是关于编码中应该应用的错误处理级别。不过,我想写一篇文章,方便那些不想看视频的人参考。
以下内容涵盖了错误处理的五个层次。我称之为“层次”,是因为其理念是从最低层次开始,学习其工作原理,然后再逐步提升到更高层次。理想情况下,无论使用何种编程语言,都应该在所有类型的编码中使用第五层次的错误处理——模式匹配。如果采用这种层次,代码的可预测性会更高。当然,还有其他类型的错误处理方法,这里列举的只是我见过的最常见的几种。
错误处理技能树如下:
🏎 第一级:忽略它们,动态语言迭代速度快
⚾️ 第二级:try/catch/throw
🏭 第三级:Go/Lua 风格,函数返回值,向上传递
⛓ 第四级:管道风格,例如 JavaScript Promise
🌯 第五级:对返回类型进行模式匹配
一级:忽略它们,不进行错误处理
这个阶段的代码编写过程中完全不进行任何错误处理。即使出现错误,你也毫不在意。
例如,这里我们访问 Python 字典中的 firstName 属性:
name = person["firstName"]
这样做要么可行,要么会因为 person 对象中不存在 firstName 而抛出运行时 KeyError 异常。在 Python 和 JavaScript 中,这种做法很常见:可以直接访问字典和对象,而无需进行错误处理。
以下是 JavaScript 中一个更常见的例子,其中您需要从 API 加载一些 JSON 数据:
const result =
await fetch(url)
.then( response => response.json() )
这个例子只针对一个出了名容易出错的操作——网络调用——做了一些错误处理。虽然作者混用了 async/await 和 Promise.then 语法,并且确保了如果 response.json() 失败也能得到处理,但由于使用了 async/await,代码仍然会抛出一个未捕获的异常,因为没有 try/catch 语句包裹。或许作者当时很着急,或者不了解 JavaScript 中 Promise 的工作原理,又或者只是为了测试而复制粘贴了代码。
你可能出于各种正当理由,故意采取第一级“不在乎”的态度。
玩转创意与领域建模
第一种情况是你在探索各种想法以了解你的领域时。在编程中,“领域”指的是“你试图解决的问题范围”。它可以小到将温度从华氏度转换为摄氏度,大到构建一个在线家具购买和运输系统,甚至你可能还不清楚具体范围。在这些情况下,无论你是否事先考虑过架构,或者你只是快速思考并尝试编写代码,你通常都会以各种方式对领域的各个部分进行建模。
想想“玩蜡笔”或者“写字以避免写作瓶颈,从而真正开始写书”。一旦你理解了事物的运作方式,并在代码中看到它,你就能开始在脑海中构建出整个领域,并以你大部分可以运行的代码为指导。错误并不重要,因为这段代码还没有提交,或者它们只是你暂时不需要关注的极端情况。
主管模式
第二种方法是,你知道你运行的系统会自动为你处理这些错误。Python 和 JavaScript 都提供了多种使用 try/except 或 try/catch 来处理同步错误的方法,以及各种全局异常处理机制。但是,如果你运行的架构会自动捕获这些错误,并且代码足够简单,你可能就不需要操心这些细节了。例如AWS Lambda、AWS Step Functions、运行在ECS或EKS上的 Docker 容器。或者,你可能正在编写 Elixir/Erlang 代码,它们的理念是“允许崩溃”;Akka也秉持着同样的理念。所有这些服务和架构都鼓励你的代码崩溃,并且会处理这些崩溃,而不是由你来处理。这极大地简化了你的架构,并减少了你需要编写的代码量(具体取决于你使用的编程语言)。
学习新事物
另一个原因是你在学习。例如,假设我想学习如何在Python中进行模式匹配,而且我不想使用库。我会阅读这篇博客文章,并尝试作者给出的示例。错误可能有用,也可能没用;关键在于我的目标是学习一项技术,我并不关心代码或错误处理。
1 级最适合用来尝试各种想法,而不在乎程序是否会崩溃。
第二级:尝试/接受/提高或尝试/接受/抛出
第二级是指在 Python 中使用 try/except,在 JavaScript 中使用 try/catch 手动捕获同步错误。我也将各种异步和全局异常处理归入此类。其目标是捕获已知错误,并将无法恢复的错误记录下来,或者对可以恢复的错误执行不同的代码路径,例如使用默认值或重试失败的操作。
你做事有多彻底?
Python 和 JavaScript 都是动态语言,因此几乎语言的每个部分都可能导致崩溃。例如,Java 等语言有 `throwable` 这样的关键字,它会让编译器提示“嘿,你应该在这里添加 try/catch 语句”。尽管 Java 的类型系统并不完善,但由于类型本身的特性,在很多情况下你仍然不必担心崩溃。这意味着,对于如何在代码中使用错误处理,并没有真正意义上的规则或有效的指导。
对于那些不使用动态语言的人来说,有些人可能会质疑为什么不在一些显而易见的情况下使用动态语言。这包括所有与 I/O 相关的操作,例如我们上面提到的 HTTP REST 调用示例,或者读取文件。许多动态语言实践者的普遍共识似乎是:只要你正确地执行了操作,那么它失败的唯一原因就是外部因素提供了错误数据。
try:
result = request(url)['Body'].json()
except Exception as e:
print("failed to load JSON:", e)
对于那些到处使用它的人来说,其他人会质疑代码的性能和可读性成本。在上面我们访问 Python 字典的 firstName 示例中,如果您不使用lens,那么您唯一的办法就是检查键是否存在:
if "firstName" in person:
return person["firstName"]
return None
然而,现在有些 Python 函数期望接收一个字符串类型的None值,却收到了一个字符串类型的值,并抛出了异常。稍后会详细介绍这一点。
在 JavaScript 中,使用可选链查找嵌套属性时也会出现同样的问题:
return person.address?.street
虽然这使得访问属性更加安全,并且不会抛出运行时异常,但是下游使用该数据的方式可能会导致运行时异常,因为某些东西undefined在未预期的情况下获得了该值。
程序员的编码风格和理念各不相同,因此他们在这个层面上的精通程度实际上取决于他们的编码风格和编程语言。
是否创建错误?
第二级包括将这些错误视为类型以及使用这些类型的机制。对于可能出现多种错误的代码类型,第二级的实现方式可能是为不同的故障创建不同的错误类型……或许如此。一些使用第二级的人认为应该处理错误,而不是创建错误。另一些人则认为应该利用语言提供的功能,然后在运行时检查错误类型。对于 Python 和 JavaScript 来说,这意味着扩展某个 Error 基类。
例如,如果您想抽象出 JavaScript AJAX 函数可能出现的所有错误fetch,则需要创建 5 个类。为了简洁起见,我们不会在下面的类示例中列出您需要了解的错误详情,但假设这些类会将这些信息作为公共类属性包含在内:
class BadUrlError extends Error {}
class Timeout extends Error {}
class NetworkError extends Error {}
class BadStatus extends Error {}
class GoodStatus extends Error {}
这样,当你执行 fetch 调用时,你就能更清楚地知道哪里出了问题,并可能采取相应措施,例如记录错误或重试:
try {
const person = await loadPerson("/person/${id}")
} catch (error) {
if(error instanceof BadUrlError) {
console.log("Check '/person/${id}' as the URL because something went wrong there.")
} else if(error instanceof Timeout || error instanceof NetworkError || error instanceof BadStatus) {
retry( { func: loadPerson, retryAttempt: 2, maxAttempts: 3 })
} else {
console.log("Unknown error:", error)
throw error
}
在你的 fetch 包装类/函数中,你需要具体地throw new BadUrlError(...)解释 fetch 过程中可能出现的各种错误。对于任何你遗漏的错误,调用者会被假定为记录日志并重新抛出异常。
在 Python 中,如果作者来自 Python 语言,或者严格遵循面向对象编程风格,那么这种 Java 风格的异常处理方式就很常见:
try:
person = load_person(f'/person/{id}')
except BadUrlError:
print(f'Check /person/{id} as the URL because something went wrong there.')
except Timeout:
except NetworkError:
except BadStatus:
retry(func=load_person, retry_attempt=2, max_attempts=3)
except Exception as e:
raise e
第三级:错误作为返回值
Lua和Go处理错误的方式截然不同。Lua 并不将错误视为函数和类中独立的机制,而是让函数直接告知你程序是否成功运行。这意味着函数需要告诉你三件事:程序是否成功运行、如果成功运行,返回值是什么、如果失败,错误是什么。至少,一个函数需要返回两个值,而不是一个。
这就是 Lua 和 Go 的功能;它们允许你从函数中返回多个值。
虽然 Lua 并不强制要求这种代码风格,但这却是 Go 语言中的一种常见约定。以下是 Go 语言读取文件的方式:
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
我们将 JavaScript HTTP 示例修改为采用这种风格,使其loadPerson返回一个Object包含错误或人员信息的响应,但不会同时返回两者:
const { error, person } = await loadPerson("/person/${id}")
if(error) {
return { error }
}
Python 在这方面稍微简单一些,你可以返回一个元组,并且通过解构参数会将它们转换为变量。load_person函数会(None, person_json)分别返回成功和(the_error, None)失败的结果。
error, person = load_person(f'/person/{id}')
if error:
return (error, None)
这有利有弊。我们先来看看优点。
- 当开始同时编写多个函数时,代码会变得非常规范化,很容易理解。
- 每个函数都可能返回它所使用的函数的许多可能错误,而且它们都以相同的方式呈现;处理数据和错误的方式也相同。
- 无需将 try/catch/except 作为语言的单独部分;您不再需要担心单独的代码路径。
- 如果你愿意,你仍然可以选择忽略像 1 级错误那样的错误,只是随意修改代码,或者说这些错误无关紧要,但忽略它们不会像 1 级错误那样破坏代码。
缺点?这种风格,如果处理所有错误,很快就会变得冗长。即使使用简洁的 Python 语言,仍然会显得拖沓:
error, string = load_person_string(file_path)
if error:
return (error, None)
error, people_list = parse_people_string(string)
if error:
return (error, None)
error, names = filter_and_format_names(people_list)
if error:
return (error, None)
return (None, names)
最后一点是,并非所有函数都需要返回成功或失败。如果您知道函数不会失败、失败的可能性很低,或者不进行任何 I/O 操作,那么您可以直接返回值。例如,获取今天的日期或您正在运行的操作系统。然而,鉴于 Python 和 JavaScript 是动态的,您无法保证运行时的结果。即使使用 mypy 或 TypeScript,它们也都是非完全类型的语言,因此虽然可以显著提高成功率,但仍然无法完全保证。有时,混合方法才是最佳选择。例如, AWS Python SDK Boto3 的行为非常一致,几乎所有方法都遵循“如果成功,则返回数据;如果失败,则抛出异常”的原则。这意味着,由于这种一致性,您可以很好地利用 Python AWS SDK 实现 Level 3 的安全策略。
第四级:管道
值得庆幸的是,函数式语言已经通过管道(也称为面向铁路的编程)解决了冗长和重复的问题。管道将函数返回成功或失败状态的概念,连接成一个单一的函数。这与 Lua 和 Golang 的工作方式非常相似,但代码量更少。除了代码量更少之外,它的优势还在于你只需要在一个地方定义错误处理。就像 Level 3 一样,如果你愿意,也可以选择不定义错误处理catch。
JavaScript 异步
我们将首先介绍 JavaScript Promises,因为这是实现这种管道式错误处理的最常用方法。
fetch(someURL)
.then( response => response.json() )
.then( filterHumans )
.then( extractNames )
.then( names => names.map( name => name.toUpperCase() ) )
.catch( error => console.log("One of the numerous functions above broke:", error) )
要真正理解上述内容,你应该将其与 Golang 风格进行比较,你会发现它更易于阅读,代码量也更少。如果你只是在尝试一些想法,并且不在乎catch错误,可以删除末尾的 `@catch`。无论fetch函数是抛出 5 个可能的错误,还是response.json因为 JSON 无法解析而失败,或者函数本身response有问题,或者其他任何函数出现错误……无论什么情况,一旦出现错误,所有函数都会立即停止并跳转到 `catch` 部分。否则,一个函数的结果会自动传递给下一个函数。最后,对于 JavaScript 来说,函数是同步的还是异步的并不重要;它都能正常工作。
Python 流水线
Python 的管道机制略有不同。我们暂时忽略 Python 中的 async/await 和线程池,并假设 Python 的优点在于同步和异步代码在外观和感觉上基本相同。这使得 Python 的一个优势在于,你可以使用同步风格的函数,这些函数既适用于同步代码,也适用于异步代码。我们将介绍一些例子。
PyDash 链
让我们使用 PyDash 的链式调用重写上面的 JavaScript 示例:
chain(request(some_url))
.thru(lambda res: res.json())
.filter( lambda person: person.type == 'human' )
.map( lambda human: human['name'] )
.map( lambda name: name.upper() )
.value()
问题在于你仍然需要用 try/except 语句包裹整个代码。更好的策略是将所有函数都设为纯函数,并像第三级那样直接返回一个 Result 对象,但 PyDash 不会对返回类型做任何假设,所以这完全取决于你自己,而且很麻烦。
返回@safe和 Flow
虽然 PyDash 允许创建这些管道,但它们的工作方式与 JavaScript 不同。在 JavaScript 中,我们可以获取值或错误,并判断是需要停止并调用 catch 语句,还是继续使用最新值执行管道。这时,returns 库就派上了用场。它首先提供合适的Result类型,然后提供能够组合返回结果的函数管道的函数。
Python 中的三级函数不再返回 `null` error, data,而是返回一个 ` Result` 对象。你可以把它想象成一个基类,它有两个子类:一个Success用于处理 `null` data,另一个Failure用于处理 `Result` error。虽然该函数返回的是单个值,但这并不是重点;真正的乐趣在于,现在你可以将它们组合成一个单独的函数:
flow(
safe_parse_json,
bind(lambda person: person.type == 'human'),
lambda human: get_or('no name', 'name', human),
lambda name: name.upper()
)
Result最后会返回一个结果;要么是成功返回一个Success类型,并且你的数据就在里面;要么是返回一个错误,Failure并且错误信息在里面。如何解包取决于你。你可以调用 ` unwrapget_value` 函数,它会返回值或抛出异常。或者你可以测试它是否成功;这里有很多选择。也许你是在 Lambda 或 Docker 容器中运行,并不在意是否有错误,所以只需unwrap在最后使用 `get_value` 函数即可。或者,也许你正在与被迫使用 Python 的 Go 开发人员合作,所以使用了 Level 3,那么需要进行转换:
result = my_flow(...)
if is_successful(result) == False:
return (result.failure(), None)
return (None, result.unwrap())
事实上的管道
这是一种非常常见的模式,许多语言都内置了这种功能,并且很多语言还抽象掉了同步与否的问题。例如F#、ReScript和Elm。这里有一个使用Babel 插件的JavaScript 示例,请注意,异步或同步操作并不重要,就像Promise返回值一样:
someURL
|> fetch
|> response => response.json()
|> filterHumans
|> extractNames
|> names => names.map( name => name.toUpperCase() )
类型说明
这里简单提一下类型。虽然 JavaScript 和 Python 并不以类型著称,但最近很多 JavaScript 开发者都开始使用TypeScript,一些 Python 开发者也开始放弃内置的类型提示,转而使用mypy。为了构建这些管道,TypeScript 4.1 引入了可变参数元组,这很有帮助,而返回值则尽力支持 7 到 21 个强类型管道。如果你想知道为什么会有这种摩擦,那是因为这些语言最初的设计并没有考虑到面向铁路的编程(ROP)。
第五级:模式匹配
本文的最后一部分,模式匹配在三个方面类似于功能更强大的 switch 语句。首先,switch 语句匹配的是特定值,而大多数模式匹配允许匹配多种类型的值,包括强类型。其次,switch 语句和模式匹配都不一定总是需要返回值,但模式匹配返回值的情况更常见。第三,模式匹配隐含了一个类似 default 的捕获所有情况的选项,并且强制执行强类型,类似于 TypeScript 对 switch 语句的严格模式,确保不会遗漏任何值case。
JavaScript模式匹配
以下是使用Folktale 编写的 JavaScript 基本函数,用于验证名称。
const legitName = name => {
if(typeof name !== 'string') {
return Failure(["Name is not a String."])
}
if(name.length < 1 && name !== " ") {
return Failure(["Name is not long enough, it needs to be at least 1 character and not an empty string."])
}
return Success(name)
}
然后我们可以对结果进行模式匹配:
legitName("Jesse")
.matchWith({
Failure: ({ value }) => console.log("Failed to validate:", value),
Success: ({ value }) => console.log(value + " is a legit name.")
})
截至撰写本文时,JavaScript 提案处于第 1 阶段,但如果您喜欢冒险,如果 Folktale 不能满足您的需求,还可以使用Babel 插件或Sparkler 库。
如果用 switch 语句来写,它可能看起来像这样:
switch(legitName(value)) {
case "not legit":
console.log("Failed to validate:", getWhyInvalid(value))
break
case "legit":
console.log(value + " is a legit name.")
break
default:
console.log("Never get here.")
}
这里有几点需要注意。首先,在模式匹配中,你通常会使用某种联合类型。Python 中的字典可以添加任意数量的属性,JavaScript 中的对象也是如此,但联合类型是固定的。我们Validation上面的类型只有两个属性:` Successa` 或 `b` Failure。这意味着我们只需要匹配这两个属性。如果你使用类型系统,那么它肯定知道只有两个属性。如果你匹配三个属性,它会报错。如果你只匹配 `a` Success,它会报错说你漏掉了 `b` Failure。
相比之下,switch 语句就完全没有这种意识了。理论上你不需要 `if` default,但除非你要切换的对象是联合体(Union),否则编译器并不知道,所以你必须把它放进去,即使它永远不会被执行。真是愚蠢。
使用 Pampy 进行 Python 模式匹配
此外,以上两个示例都没有返回值,但这实际上是模式匹配的常见功能。让我们使用Pampy 库,通过 Python 将 HTTP REST 调用实现为模式匹配,并返回一个Python Union 对象,具体来说,就是一个Result 对象,它返回的是成功结果(我们将数据放入一个数组中)Success或失败结果(我们将失败原因放入一个数组中Failure)。
result = match(load_person(f'/person/{id}'),
Json, lambda json_data: Success(json_data),
BadUrl, lambda: Failure(f"Something is wrong with the url '/person/{id}'"),
Timeout, lambda: retry(func=load_person, retry_attempt=2, max_attempts=3),
NetworkError, lambda: retry(func=load_person, retry_attempt=2, max_attempts=3),
BadStatus, lambda: retry(func=load_person, retry_attempt=2, max_attempts=3)
)
对于我们的第一次尝试,如果我们成功了Json,那就太好了,一切正常,我们的result数据将拥有我们想要的 JSON 数据。
但是,如果我们遇到错误BadUrl,那就麻烦了,因为这意味着我们的代码在 URL 的编写上存在问题,或者我们可能错误地从某个我们以为存在但实际上并不存在的环境变量中读取了 URL 。此时我们只能修复代码,并通过预先进行 URL 验证并设置默认值来提高代码的健壮性。
然而,我们在这里稍微违反了 DRY(Don't Repeat Yourself,不要重复自己)原则,Timeout因为NetworkError`,` 和BadStatus`都做了同样的事情,即尝试重试。由于通常情况下,模式匹配的对象是联合体,所以你事先知道可能的状态数量(通常如此;有些语言允许你对其他具有无限空间的对象进行模式匹配。为了本文的目的,我们只关注错误)。因此,我们可以使用下划线 (_) 这个通用的捕获符。让我们重写一下:
result = match(load_person(f'/person/{id}'),
Json, lambda json_data: Success(json_data),
BadUrl, lambda: Failure(f"Something is wrong with the url '/person/{id}'"),
_, lambda: retry(func=load_person, retry_attempt=2, max_attempts=3)
)
好多了。另外需要注意的是,与 switch 语句相比,你知道下划线代表什么,而且编译器通常会提供辅助信息,而 switch 语句并不总是知道默认值是什么。上面的示例提供了数据、失败信息,如果重试成功,则可能返回成功信息;否则,在重试次数用尽后,最终会返回错误。
如果你想要比 Pampy 更 Pythonic 的东西,你可以尝试使用 Python 中的数据类进行模式匹配。
模式匹配不仅仅是错误处理
这里需要注意的一点是,模式匹配在函数式编程语言中通常只是一种语言特性。因此,你可以在任何级别的错误处理中使用它。例如,以上代码就是一级错误处理风格的“我不在乎,只是在尝试一些想法”:
result = match(load_person(f'/person/{id}'),
Json, lambda json_data: Success(json_data),
_, lambda: Success([]) # TODO: just empty Array for now, not sure why my parsing is failing, will fix later
)
再说一遍,如果你和被迫使用 Python 的 Go 开发人员一起工作,你可以追溯到第 3 级进行模式匹配:
result = match(load_person(f'/person/{id}'),
Json, lambda json_data: (None, json_data),
BadUrl, lambda: (Exception(f"Something is wrong with the url '/person/{id}'"), None),
_, lambda: retry(func=load_person, retry_attempt=2, max_attempts=3)
)
对于第四级,许多管道会假设模式匹配返回的任何内容都会被重新导入管道。例如,我们上面提到的人员解析器,如果数据来自技术债务严重的后端或包含错误数据的数据库,我们可以进行补偿。我们通过模式匹配来确保只提供默认值,而不是破坏整个管道。如果某人的姓名为空(因为DynamoDBextract_names不允许空值),这不应该导致整个流程停止。最后,由于我们知道所有可能的结果,我们会在函数中设置模式匹配,以确保函数永远不会失败,而是让使用者基于已知结果进行模式匹配。对于那些不了解 Promise 并且只使用 async/await 语法而没有使用 try/catch 的人来说,这允许他们在不损害代码库的情况下这样做。首先,我们将构建一个小型函数,用于匹配可能遇到的没有姓名的人员对象。catch
const getNameElseDefault = human =>
getNameMaybe(human).matchWith({
Nothing: () => "no name found",
Just: ({ value }) => value
})
然后,我们将她连接到下面现有的 JavaScript 管道中:(假设我们已经修改了管道response.json(),使其像第二级那样抛出自定义错误):
const getPeople = () =>
Promise.resolve(someURL)
.then( fetch )
.then( response => response.json() )
.then( filterHumans )
.then(
humans =>
humans.map(getNameElseDefault)
)
.then( names => names.map( name => name.toUpperCase() ) )
.then( uppercaseNames => Json(uppercaseNames) )
.catch(
error =>
error => error.matchWith({
FailedToParseJSON: parseError => Promise.resolve(parseError),
BadUrl: badurlError => Promise.resolve(badurlError),
_: otherError => Promise.resolve(otherError)
})
)
现在,任何使用此函数的人都可以简单地对两个值进行模式匹配:
const result = await getPeople()
result.matchWith({
Json: ({ uppercaseNames }) => console.log("Got our people names:", uppercaseNames),
_ => error => console.log("Something broke:", error)
})
图案匹配的优缺点
如果你不使用类型,那么它的优势类似于第三级,即假设所有函数都不会失败,而只是告诉你它们尝试执行的操作是否成功。当结果不再像 HTTP 响应那样只有“成功”或“失败”两种可能时,你可以创建自定义类型并进行匹配。即使某个操作有五种可能的结果,你也可以使用 catch all_来将所有错误合并为一个,或者你根本不在乎这些错误。这样就无需手动进行 try/except/catch 等错误处理。
如果使用类型,您可以确保处理所有可能的匹配项,从而避免遗漏函数返回类型。即使使用类型,_如果您只是在尝试各种想法,仍然可以将它们全部归为一类。
然而,许多语言本身并不支持这种功能。Python和JavaScript正在逐步添加这种功能。对于那些来自传统命令式或面向对象Python /JavaScript 的开发者来说,使用上述库和技术可能会比较陌生。第三级(Level 3)的概念已经很难理解,比如对别人说:“你知道我们以前是如何抛出异常的吗?如果不再有异常处理机制了怎么办?” 现在你相当于在说:“所有可能失败的函数,我们都会返回一个对象,你需要自己决定如何处理它。” 这对很多开发者来说都是难以接受的,尤其是在大多数传统编程文献都引用“是的,我们默认你只需要使用 try/catch”这种说法的情况下。
Maybe最后,如果没有类型,通常使用`and` 也能勉强应付Result,因为随着时间的推移,记住它们的两个子类型(例如 `Just/Nothing` 和 `Success/Failure`)相对容易。但是,当你创建自定义类型,或者开始将它们嵌套到复合函数中,并且完全不知道输出结果是什么时,就会变得很棘手。那些已经熟悉动态语言的人通常更愿意通过打印输出来了解这些类型,而不是使用类型化语言来让编译器帮忙。
结论
我已经解释了错误处理的 5 个级别,特别是针对动态语言的级别:
- 你无视他们
- 你可以使用 try/except/catch 和 raise/throw 等不同程度的处理方法来处理它们。
- 您采用了 Lua/Golang 的方法,通过返回多个值来表示成功或失败。
- 您可以创建管道并在一个地方处理错误,而不是像 Level 3 那样在多个地方处理。
- 您可以匹配函数可能返回的结果(例如成功或失败),或者更细致的结果(例如 HTTP),使用函数而不是像二级匹配那样使用异常匹配。
了解每个级别固然重要且有价值,每个级别也都有其用途,但对于生产代码,您应该使用级别 4 和级别 5。在学习如何解决问题时,您可以保留忽略错误并停留在级别 1 的权利。但是,当您准备真正开始编写项目代码时,请以级别 4 和级别 5 为目标。这些级别可以确保运行时异常最少,并减少单元测试的开销。
对于动态语言,开发者需要承担很大一部分责任,那就是记住字典/对象的类型和结构。第一级和第二级比较难,因为有时你只会遇到一个 ` ExceptionNone` 或 `None` Error,而其他类型的错误类型则有文档记录。它们对日志记录很有帮助,因为许多 API 和 SDK 都是这样构建的,旨在帮助你找出其抽象层内部的错误所在。然而,随着时间的推移,你会发现,除了日志记录之外,你最终总是会陷入“要么成功,要么失败”的怪圈,并开始放弃记录异常处理堆栈。你永远无法与你的团队或你自己就 `try/except` 的使用量达成共识。你也很难看到创建自定义异常类的投资回报。
一旦达到第三级,即使不使用 Go 语言,你也会喜欢上更少的代码量,以及只对你认为有风险的函数返回错误信息的自由。然而,如果没有编译器,你仍然会遇到与第二级相同的问题,并且永远无法真正确定怎样的错误处理才算足够。
Python 提供了多种管道选项,甚至 JavaScript 也有类似的替代方案,Promise例如RxJS。然而,你会发现,如果无法轻松地与其他类进行比较,错误类的概念就没什么用处。因此,5 级模式匹配更适合管道工作流,它既可以减少 3 级错误检查所需的样板代码,又能让你轻松地在管道中的任何位置注入。许多模式匹配文档会涵盖你可以匹配的各种对象,例如简单的数字和列表,但对于错误处理,文档通常假定存在某种数据类或类型。虽然像 JavaScript Promise 这样的管道会输出数据或抛出异常,但最好将它们视为返回成功/失败值的 3 级函数,并以此为基础进行后续处理。