go语言调取包会先找vendor下的包 ,这个错说明vendor下有sirupsen这个包
github.com/Sirupsen/logrus" and "github.com/sirupsen/logrus,
直接把Sirupsen换为sirupsen就可以使用了
这个问题说来话长,我先表达一下我的观点,Go语言从语法层面提供区分错误和异常的机制是很好的做法,比自己用单个返回值做值判断要方便很多.
上面看到很多知乎大牛把异常和错误混在一起说,有认为Go没有异常机制的,有认为Go纯粹只有异常机制的,我觉得这些观点都太片面了.
具体对于错误和异常的讨论,我转发一下前阵子写的一篇日志抛砖引玉吧.
============================
最近连续遇到朋友问我项目里错误和异常管理的事情,之前也多次跟团队强调过错误和异常管理的一些概念,所以趁今天有动力就赶紧写一篇Go语言项目错误和异常管理的经验分享.
首先我们要理清:什么是错误、什么是异常、为什么需要管理.然后才是怎样管理.
错误和异常从语言机制上面讲,就是error和panic的区别,放到别的语言也一样,别的语言没有error类型,但是有错误码之类的,没有panic,但是有throw之类的.
在语言层面它们是两种概念,导致的是两种不同的结果.如果程序遇到错误不处理,那么可能进一步的产生业务上的错误,比如给用户多扣钱了,或者进一步产生了异常;如果程序遇到异常不处理,那么结果就是进程异常退出.
在项目里面是不是应该处理所有的错误情况和捕捉所有的异常呢?我只能说,你可以这么做,但是估计效果不会太好.我的理由是:
如果所有东西都处理和记录,那么重要信息可能被淹没在信息的海洋里.
不应该处理的错误被处理了,很容易导出BUG暴露不出来,直到出现更严重错误的时候才暴露出问题,到时候排查就很困难了,因为已经不是错误的第一现场.
所以错误和异常最好能按一定的规则进行分类和管理,在第一时间能暴露错误和还原现场.
对于错误处理,Erlang有一个很好的概念叫速错,就是有错误第一时间暴露它.我们的项目从Erlang到Go一直是沿用这一设计原则.但是应用这个原则的前提是先得区分错误和异常这两个概念.
错误和异常上面已经提到了,从语言机制层面比较容易区分它们,但是语言取决于人为,什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现全部靠异常来做错误处理的情况,似乎Java项目特别容易出现这样的设计.
这里我先假想有这样一个业务:游戏粉丝通过购买按钮,用铜钱购买宝石.
在这种结构下,铜钱不足就变成了业务逻辑范围内的一种失败情况,但不能提升为异常,否则铜钱不足的用户一点购买按钮都会出错掉线.
所以,异常和错误在不同程序结构下是互相转换的,我们没办法一句话的给所有类型所有结构的程序一个统一的异常和错误分类规则.
所以我们可以这样来归类异常和错误:不会终止程序逻辑运行的归类为错误,会终止程序逻辑运行的归类为异常.
错误和异常的分类需要通过一定的思维训练来强化分类能力,就类似于面向对象的设计方式一样的,技术实现就摆在那边,但是要用好需要不断的思维训练不断的归类和总结,以上提到的归类方式希望可以作为一个参考,期待大家能发现更多更有效的归类方式.
此时此刻呢我们讲一下速错和Go语言里面怎么做到速错.
速错我最早接触是在做的时候就体验到的,当然跟Erlang的速错不完全一致,那时候也没有那么高大上的一个名字,但是对待异常的理念是一样的.
在.NET项目开发的时候,有经验的程序员都应该知道,不能随便re-throw,就是catch错误再抛出,原因是异常的第一现场会被破坏,堆栈跟踪信息会丢失,因为外部最后拿到异常的堆栈跟踪信息,是最后那次throw的异常的堆栈跟踪信息;其次,不能随便try catch,随便catch很容易导出异常暴露不出来,升级为更严重的业务漏洞.
到了Erlang时期,大家学到了速错概念,简单来讲就是:让它挂.只有挂了你才会第一时间知道错误,但是Erlang的挂,只是Erlang进程的异常退出,不会导致整个Erlang节点退出,所以它挂的影响层面比较低.
在Go语言项目中,虽然有类似Erlang进程的Goroutine,但是Goroutine如果panic了,并且没有recover,那么整个Go进程就会异常退出.所以我们在Go语言项目中要应用速错的设计理念,就要对Goroutine做一定的管理.
第一类Goroutine典型的有:处理各个粉丝请求的Goroutine,因为每个粉丝连接各自有一个Goroutine,所以挂掉了只会影响单个粉丝,不会影响整体业务进行.
第二类Goroutine典型的有:数据库同步用的Goroutine,如果它挂了,数据就无法同步到数据库,游戏如果继续运行下去只会导致数据回档,还不如让整个游戏都异常退出.
这样一分类,就可以比较清楚哪些Goroutine该做recover处理,哪些不该做recover处理了.
为此,我们还特地设计了一个库,用来格式化输出堆栈跟踪信息和对象信息,项目地址:funny/debug - GitHub
通篇写下来发现比我预期的长很多,所以这里我做一下归纳总结,帮组大家理解这篇文章所要表达的:
错误和异常需要分类和管理,不能一概而论
错误和异常的分类可以以是否终止业务过程作为标准
错误是业务过程的一部分,异常不是
不要随便捕获异常,更不要随便捕获再重新抛出异常
Go语言项目需要把Goroutine分为两类,区别处理异常
在捕获到异常时,需要尽可能的保留第一现场的关键数据
以上仅为一家之言,抛砖引玉,希望大家如果觉得本站发布的文章不错,请转发分享给您身边的朋友,您的支持是我们最大的动力.
云和安全管理服务专家新钛云服 张春翻译
这种方法有几个缺点.首先,它可以对程序员隐藏错误处理路径,特别是在捕获异常不是强制性的情况下,例如在 Python 中.即使在具有必须处理的 Java 风格的检查异常的语言中,如果在与原始调用不同的级别上处理错误,也并不总是很明显错误是从哪里引发的.
我们都见过长长的代码块包装在一个 try-catch 块中.在这种情况下,catch 块实际上充当 goto 语句,这通常被认为是有害的(奇怪的是,C 中的关键字被认为可以接受的少数用例之一是错误后清理,因为该语言没有 Golang- 样式延迟语句).
如果你确实从源头捕获异常,你会得到一个不太优雅的 Go 错误模式版本.这可能会解决混淆代码的问题,但会遇到另一个问题:性能.在诸如 Java 之类的语言中,抛出异常可能比函数的常规返回慢数百倍.
Java 中最大的性能成本是由打印异常的堆栈跟踪造成的,这是昂贵的,因为运行的程序必须检查编译它的源代码 .仅仅进入一个 try 块也不是空闲的,因为需要保存 CPU 内存寄存器的先前状态,因为它们可能需要在抛出异常的情况下恢复.
如果您将异常视为通常不会发生的异常情况,那么异常的缺点并不重要.这可能是传统的单体应用程序的情况,其中大部分代码库不必进行网络调用——一个操作格式良好的数据的函数不太可能遇到错误(除了错误的情况).一旦您在代码中添加 I/O,无错误代码的梦想就会破灭:您可以忽略错误,但不能假装它们不存在!
try {
doSometing()
} catch (IOException e) {
// ignore it
}
与大多数其他编程语言不同,Golang 接受错误是不可避免的. 如果在单体架构时代还不是这样,那么在今天的模块化后端服务中,服务通常和外部 API 调用、数据库读取和写入以及与其他服务通信 .
以上所有方法都可能失败,解析或验证从它们接收到的数据(通常在无模式 JSON 中)也可能失败.Golang 使可以从这些调用返回的错误显式化,与普通返回值的等级相同.从函数调用返回多个值的能力支持这一点,这在大多数语言中通常是不可能的.Golang 的错误处理系统不仅仅是一种语言怪癖,它是一种将错误视为替代返回值的完全不同的方式!
重复 if err != nil
对 Go 错误处理的一个常见批评是被迫重复以下代码块:
res, err := doSomething()
if err != nil {
// Handle error
这么多行代码!这么低效!如果您认为上述内容不优雅或浪费代码,您可能忽略了我们检查代码中的错误的全部原因:我们需要能够以不同的方式处理它们!对 API 或数据库的调用可能会被重试.
有时事件的顺序很重要:调用外部 API 之前发生的错误可能不是什么大问题(因为数据从未通过发送),而 API 调用和写入本地数据库之间的错误可能需要立即注意,因为 这可能意味着系统最终处于不一致的状态.即使我们只想将错误传播给调用者,我们也可能希望用失败的解释来包装它们,或者为每个错误返回一个自定义错误类型.
并非所有错误都是相同的,并且向调用者返回适当的错误是 API 设计的重要部分,无论是对于内部包还是 REST API .
不必担心在你的代码中重复 if err != nil ——这就是 Go 中的代码应该看起来的样子.
自定义错误类型和错误包装
从导出的方法返回错误时,请考虑指定自定义错误类型,而不是单独使用错误字符串.字符串在意外代码中是可以的,但在导出的函数中,它们成为函数公共 API 的一部分.更改错误字符串将是一项重大更改——如果没有明确的错误类型,需要检查返回错误类型的单元测试将不得不依赖原始字符串值!事实上,基于字符串的错误也使得在私有方法中测试不同的错误案例变得困难,所以呢您也应该考虑在包中使用它们.回到错误与异常的争论,返回错误也使代码比抛出异常更容易测试,因为错误只是要检查的返回值.不需要测试框架或在测试中捕获异常 .
可以在 database/sql 包中找到简单自定义错误类型的一个很好的示例.它定义了一个导出常量列表,表示包可以返回的错误类型,最著名的是 sql.ErrNoRows.虽然从 API 设计的角度来看,这种特定的错误类型有点问题(您可能会争辩说 API 应该返回一个空结构而不是错误),但任何需要检查空行的应用程序都可以导入该常量并在代码中使用它不必担心错误消息本身会改变和破坏代码.
对于更复杂的错误处理,您可以通过实现返回错误字符串的 Error() 方法来定义自定义错误类型.自定义错误可以包括元数据,例如错误代码或原始请求参数.如果您想表示错误类别,它们很有用.DigitalOcean 的本教程展示了如何使用自定义错误类型来表示可以重试的一类临时错误.
通常,错误会通过将低级错误与更高级别的解释包装起来,从而在程序的调用堆栈中传播.例如,数据库错误可能会以下列格式记录在 API 调用处理程序中:调用 CreateUser 端点时出错:查询数据库时出错:pq:检测到死锁.这很有用,因为它可以帮助我们跟踪错误在系统中传播的过程,向我们展示根本原因(数据库事务引擎中的死锁)以及它对更广泛系统的影响(调用者无法创建新用户).
Panics
不要 panic()!长时间运行的应用程序应该优雅地处理错误而不是panic.即使在无法恢复的情况下(例如在启动时验证配置),最好记录一个错误并优雅地退出.panic比错误消息更难诊断,并且可能会跳过被推迟的重要关闭代码.
Logging
我还想简要介绍一下日志记录,因为它是处理错误的关键部分.通常你能做的最好的事情就是记录收到的错误并继续下一个请求.
除非您正在构建简单的命令行工具或个人项目,否则您的应用程序应该使用结构化的日志库,该库可以为日志添加时间戳,并提供对日志级别的控制.最后一部分特别重要,因为它将允许您突出显示应用程序记录的所有错误和警告.通过帮助将它们与信息级日志分开,这将为您节省无数时间.
微服务架构还应该在日志行中包含服务的名称以及机器实例的名称.默认情况下记录这些时,程序代码不必担心包含它们.您也可以在日志的结构化部分中记录其他字段,例如收到的错误(如果您不想将其嵌入日志消息本身)或有问题的请求或响应.只需确保您的日志没有泄露任何敏感数据,例如密码、API 密钥或用户的个人数据!
对于日志库,我过去使用过 logrus 和 zerolog,但您也可以选择其他结构化日志库.如果您想了解更多信息,互联网上有许多关于如何使用这些的指南.如果您将应用程序部署到云中,您可能需要日志库上的适配器来根据您的云平台的日志 API 格式化日志 - 没有它,云平台可能无法检测到日志级别等某些功能.
如果您在应用程序中使用调试级别日志(默认情况下通常不记录),请确保您的应用程序可以轻松更改日志级别,而无需更改代码.更改日志级别还可以暂时使信息级别甚至警告级别的日志静音,以防它们突然变得过于嘈杂并开始淹没错误.您可以使用在启动时检查以设置日志级别的环境变量来实现这一点.
原文: