编程语言应该如何处理错误?例如,打开具有给定名称的文件并将其读入缓冲区的函数可能由于多种原因而失败:文件可能不存在,打开它的程序可能没有打开它的权限,或者它可能太大到适合缓冲区;大多数语言都使用异常:抛出的异常向上传播调用堆栈,直到在try-catch块中处理的级别。异常模型将错误视为特殊情况,与程序的正常返回值流分开。这种方法有几个缺点。首先,它可以向程序员隐藏错误处理路径,特别是在捕获异常不是强制性的情况下,例如在Python中。即使在具有必须处理的Java样式检查异常的语言中,如果在与原始调用不同的级别处理错误,则从何处引发错误并不总是很明显。我们都见过用try-catch块包裹的长代码块。在这种情况下,catch块实际上充当goto语句,这通常被认为是有害的(奇怪的是,关键字在C中被认为是可接受的少数用例之一是错误后清理,因为该语言没有Golang-样式延迟语句)。如果你确实在源头捕获了异常,你最终会得到一个不太优雅的Go错误模式版本。这可能会解决混淆代码的问题,但它会遇到另一个问题:性能。在Java这样的语言中,抛出异常可能比函数的常规返回慢数百倍。Java中最大的性能成本是由打印异常的堆栈跟踪引起的,这是昂贵的,因为正在运行的程序必须检查编译它的源代码。仅仅进入try块也不是空闲的,因为需要保留CPU内存寄存器的先前状态,因为在抛出异常的情况下可能需要恢复它们。如果您将异常视为通常不会发生的异常情况,那么异常的缺点并不重要。这可能是传统单体应用程序的情况,其中大部分代码库不必进行网络调用-对格式良好的数据进行操作的函数不太可能遇到错误(错误情况除外)。一旦将I/O添加到代码中,无错误代码的梦想就破灭了:您可以忽略错误,但不能假装它们不存在!try{doSometing()}catch(IOExceptione){//ignoreit}withbig与大多数其他编程语言不同,Golang认为错误是不可避免的。如果在单体时代还不是这样的话,那么在今天的模块化后端服务中,服务通常与外部API调用、数据库读写以及其他服务进行通信。上述所有方法都可能失败,解析或验证从它们接收到的数据(通常在无模式JSON中)也是如此。Golang使可以从这些调用返回的错误显式化,与普通返回值处于同一级别。这是由从函数调用中返回多个值的能力支持的,这在大多数语言中通常是不可能的。Golang的错误处理系统不仅仅是一种语言怪癖,它还是一种将错误视为替代返回值的完全不同的方式!Repeatingiferr!=nil对Go错误处理的常见批评是被迫重复以下代码块:res,err:=doSomething()iferr!=nil{//Handleerror}对于新用户来说,这可能感觉毫无用处以及行的浪费:在其他语言中需要3行的函数很可能会增长到12行:这么多行代码!太低效了!如果您认为上面的代码不够优雅或浪费代码,您可能错过了我们检查代码错误的全部原因:我们需要能够以不同的方式处理它们!可能会重试正确的API或数据库调用。有时事件的顺序很重要:调用外部API之前的错误可能不是什么大问题(因为数据永远不会发送),而API调用和写入本地数据库之间的错误可能需要立即引起注意因为这可能意味着系统最终处于不一致状态。即使我们只想将错误传播给调用者,我们也可能希望用失败的解释来包装它们,或者为每个错误返回自定义错误类型。并非所有错误都是一样的,向调用者返回适当的错误是API设计的一个重要部分,无论是对于内部包还是RESTAPI。不要担心在代码中重复iferr!=nil-Go中的代码应该是这样的。自定义错误类型和错误包装器从导出的方法返回错误时,请考虑指定自定义错误类型而不是单独使用错误字符串。字符串在意想不到的代码中很好,但在导出的函数中,它们成为函数公共API的一部分。更改错误字符串将是一个重大更改-如果没有明确的错误类型,需要检查返回的错误类型的单元测试将不得不依赖原始字符串值!事实上,基于字符串的错误也会使私有方法中针对不同错误情况的测试变得困难,因此您也应该考虑在您的包中使用它们。回到错误与异常的争论,返回错误也比抛出异常更容易测试代码,因为错误只是要检查的返回值。无需测试框架或在测试中捕获异常。可以在database/sql包中找到简单自定义错误类型的一个很好的示例。它定义了一个导出常量列表,表示包可以返回的错误类型,最值得注意的是sql.ErrNoRows。虽然从API设计的角度来看这种特定的错误类型有点问题(您可能会争辩说API应该返回一个空结构而不是错误),但任何需要检查空行的应用程序都可以导入此常量并在Use中使用它它在您的代码中,而不必担心错误消息本身会更改和破坏您的代码。对于更复杂的错误处理,您可以通过实现返回错误字符串的Error()方法来定义自定义错误类型。自定义错误可以包括元数据,例如错误代码或原始请求参数。如果您想表示错误类,它们很有用。DigitalOcean的本教程展示了如何使用自定义错误类型来表示一类可以重试的临时错误。通常,错误通过用更高级别的解释包装低级错误来向上传播程序的调用堆栈。例如,数据库错误可能会以以下格式记录在API调用处理程序中:调用CreateUser端点时出错:查询数据库时出错:pq:检测到死锁。这很有用,因为它可以帮助我们在错误通过系统传播时跟踪错误,向我们展示根本原因(数据库事务引擎中的死锁)及其对更广泛系统的影响(调用者无法创建新用户)。自Go1.13以来,此模式具有特殊语言支持,带有错误包装器。通过在创建字符串错误时使用%w动词,可以使用Unwrap()方法访问底层错误。除了比较错误是否相等的函数errors.Is()和errors.As()之外,程序还可以获得原始类型或包装错误的标识。这在某些情况下很有用,尽管我认为在确定如何处理所述错误时最好使用顶级错误的类型。恐慌不要恐慌()!长时间运行的应用程序应该优雅地处理错误而不是恐慌。即使在不可恢复的情况下(例如在启动时验证配置),最好记录错误并优雅地退出。恐慌比错误消息更难诊断,并且可能会跳过被推迟的重要关闭代码。日志记录我还想简要介绍一下日志记录,因为它是处理错误的关键部分。通常你能做的最好的事情就是记录收到的错误并继续下一个请求。除非您正在构建简单的命令行工具或个人项目,否则您的应用程序应该使用结构化的日志记录库,为日志添加时间戳并提供对日志级别的控制。最后一部分特别重要,因为它可以让您突出显示应用程序记录的任何错误和警告。这有助于将它们与INFO级别的日志分开,从而为您节省无数时间。微服务架构还应在日志行中包含服务名称和机器实例名称。虽然默认情况下会记录这些,但程序代码不必担心包含它们。您还可以在日志的结构化部分记录其他字段,例如收到的错误(如果您不想将它们嵌入日志消息本身)或有问题的请求或响应。只要确保您的日志没有泄露任何敏感数据,例如密码、API密钥或用户的个人数据!对于日志库,我过去使用过logrus和zerolog,但您也可以选择其他结构化的日志库。如果您想了解更多信息,互联网上有很多关于如何使用它们的指南。如果将应用程序部署到云中,您可能需要在日志库上使用一个适配器来根据云平台的日志记录API格式化日志——没有它,云平台可能无法检测某些功能,例如日志级别。如果您在应用程序中使用调试级别日志记录(默认情况下通常不记录),请确保您的应用程序可以轻松更改日志级别而无需更改代码。更改日志级别还可以暂时使信息级别甚至警告级别的日志静音,以防它们突然变得过于嘈杂并开始淹没错误。您可以使用在启动时检查的环境变量来设置日志级别。原文:https://img.ydisp.cn/news/20220902/j0iabpi0tfx
