Golang 中更好的错误处理:理论和实践技巧

Python013

Golang 中更好的错误处理:理论和实践技巧,第1张

云和安全管理服务专家新钛云服 张春翻译

这种方法有几个缺点。首先,它可以对程序员隐藏错误处理路径,特别是在捕获异常不是强制性的情况下,例如在 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

}

对于新用户来说,这可能会觉得没用而且浪费行数:在其他语言中需要 3 行的函数很可能会增长到 12 行

这么多行代码!这么低效!如果您认为上述内容不优雅或浪费代码,您可能忽略了我们检查代码中的错误的全部原因:我们需要能够以不同的方式处理它们!对 API 或数据库的调用可能会被重试。

有时事件的顺序很重要:调用外部 API 之前发生的错误可能不是什么大问题(因为数据从未通过发送),而 API 调用和写入本地数据库之间的错误可能需要立即注意,因为 这可能意味着系统最终处于不一致的状态。即使我们只想将错误传播给调用者,我们也可能希望用失败的解释来包装它们,或者为每个错误返回一个自定义错误类型。

并非所有错误都是相同的,并且向调用者返回适当的错误是 API 设计的重要部分,无论是对于内部包还是 REST API

不必担心在你的代码中重复 if err != nil ——这就是 Go 中的代码应该看起来的样子。

自定义错误类型和错误包装

从导出的方法返回错误时,请考虑指定自定义错误类型,而不是单独使用错误字符串。字符串在意外代码中是可以的,但在导出的函数中,它们成为函数公共 API 的一部分。更改错误字符串将是一项重大更改——如果没有明确的错误类型,需要检查返回错误类型的单元测试将不得不依赖原始字符串值!事实上,基于字符串的错误也使得在私有方法中测试不同的错误案例变得困难,因此您也应该考虑在包中使用它们。回到错误与异常的争论,返回错误也使代码比抛出异常更容易测试,因为错误只是要检查的返回值。不需要测试框架或在测试中捕获异常

可以在 database/sql 包中找到简单自定义错误类型的一个很好的示例。它定义了一个导出常量列表,表示包可以返回的错误类型,最著名的是 sql.ErrNoRows。虽然从 API 设计的角度来看,这种特定的错误类型有点问题(您可能会争辩说 API 应该返回一个空结构而不是错误),但任何需要检查空行的应用程序都可以导入该常量并在代码中使用它不必担心错误消息本身会改变和破坏代码。

对于更复杂的错误处理,您可以通过实现返回错误字符串的 Error() 方法来定义自定义错误类型。自定义错误可以包括元数据,例如错误代码或原始请求参数。如果您想表示错误类别,它们很有用。DigitalOcean 的本教程展示了如何使用自定义错误类型来表示可以重试的一类临时错误。

通常,错误会通过将低级错误与更高级别的解释包装起来,从而在程序的调用堆栈中传播。例如,数据库错误可能会以下列格式记录在 API 调用处理程序中:调用 CreateUser 端点时出错:查询数据库时出错:pq:检测到死锁。这很有用,因为它可以帮助我们跟踪错误在系统中传播的过程,向我们展示根本原因(数据库事务引擎中的死锁)以及它对更广泛系统的影响(调用者无法创建新用户)。

自 Go 1.13 以来,此模式具有特殊的语言支持,并带有错误包装。通过在创建字符串错误时使用 %w 动词,可以使用 Unwrap() 方法访问底层错误。除了比较错误相等性的函数 errors.Is() 和 errors.As() 外,程序还可以获取包装错误的原始类型或标识。这在某些情况下可能很有用,尽管我认为在确定如何处理所述错误时最好使用顶级错误的类型。

Panics

不要 panic()!长时间运行的应用程序应该优雅地处理错误而不是panic。即使在无法恢复的情况下(例如在启动时验证配置),最好记录一个错误并优雅地退出。panic比错误消息更难诊断,并且可能会跳过被推迟的重要关闭代码。

Logging

我还想简要介绍一下日志记录,因为它是处理错误的关键部分。通常你能做的最好的事情就是记录收到的错误并继续下一个请求。

除非您正在构建简单的命令行工具或个人项目,否则您的应用程序应该使用结构化的日志库,该库可以为日志添加时间戳,并提供对日志级别的控制。最后一部分特别重要,因为它将允许您突出显示应用程序记录的所有错误和警告。通过帮助将它们与信息级日志分开,这将为您节省无数时间。

微服务架构还应该在日志行中包含服务的名称以及机器实例的名称。默认情况下记录这些时,程序代码不必担心包含它们。您也可以在日志的结构化部分中记录其他字段,例如收到的错误(如果您不想将其嵌入日志消息本身)或有问题的请求或响应。只需确保您的日志没有泄露任何敏感数据,例如密码、API 密钥或用户的个人数据!

对于日志库,我过去使用过 logrus 和 zerolog,但您也可以选择其他结构化日志库。如果您想了解更多信息,互联网上有许多关于如何使用这些的指南。如果您将应用程序部署到云中,您可能需要日志库上的适配器来根据您的云平台的日志 API 格式化日志 - 没有它,云平台可能无法检测到日志级别等某些功能。

如果您在应用程序中使用调试级别日志(默认情况下通常不记录),请确保您的应用程序可以轻松更改日志级别,而无需更改代码。更改日志级别还可以暂时使信息级别甚至警告级别的日志静音,以防它们突然变得过于嘈杂并开始淹没错误。您可以使用在启动时检查以设置日志级别的环境变量来实现这一点。

原文:https://levelup.gitconnected.com/better-error-handling-in-golang-theory-and-practical-tips-758b90d3f6b4

如何使用Google日志库 (glog)

介绍

Google glog是一个应用层的库. 它提供基于C++风格的流和多种宏接口.例如:

#include <glog/logging.h>

int main(int argc, char* argv[]) {

// Initialize Google's logging library.

google::InitGoogleLogging(argv[0])

// ...

LOG(INFO) <<"Found " <<num_cookies <<" cookies"

}

Google glog定义了一系列的宏处理普通的日志工作. 你可以分级记录, control loggingbehavior from the command line, 按条件记录, abort theprogram when expected conditions are not met, introduce your ownverbose logging levels, 及其它. 文本只是简单的介绍了常用的glog功能而不是所有的细节.没介绍到的,自己读代码去吧.

严重等级

严重等级包括(按严重程度排序): INFO, WARNING,ERROR,和FATAL.记录一个FATAL级的错误后会自动终止程序执行(其实就是_asm int 3).一个严重等级高的记录不但记录在它自己的文件,也记录在比它低等的文件里.例如, 一个FATAL级的记录将会记在FATAL, ERROR,WARNING, 和INFO等级的日志里.

在debug状态下FATAL级记录也可以用DFATAL(当没定义NDEBUG宏的时候),发给客户的程序最好不要用FATAL小心客户对你使用_asm int 3,这时ERROR是个不错的选择.

glog默认把日志文件命名为"/tmp/...log...."(例如, "/tmp/hello_world.example.com.hamaji.log.INFO.20080709-222411.10474").glog默认把ERROR 和 FATAL 级的记录发送一份到stderr.

设置标志

标志可以改变glog的输出行为.如果你安装了Googlegflags library, configure 脚本 (具体请参考它的INSTALL文件)会自动检测并使用标志,这时你可以通过命令行使用标志.例如, 若你想发送 --logtostderr 标志,像下面这样:

./your_application --logtostderr=1

如果你没装Google gflags library,你就只能用带GLOG_的环境变量实现,例如.

GLOG_logtostderr=1 ./your_application(貌似在Windows里不能这样用吧?)

常用的标志有:

logtostderr (bool, default=false)

写日志到stderr而不是日志文件.

Note: you can set binary flags to true by specifying1, true, or yes (caseinsensitive).Also, you can set binary flags to false by specifying0, false, or no (again, caseinsensitive).

stderrthreshold (int, default=2, whichis ERROR)

将某级及以上级别的记录同时发送到stderr和日志文件. 严重等级INFO, WARNING, ERROR, FATAL 分别对应 0, 1, 2, 3.

minloglevel (int, default=0, whichis INFO)

只记录某级及以上级别的记录.

log_dir (string, default="")

指定日志保存的目录.

v (int, default=0)

Show all VLOG(m) messages for m less orequal the value of this flag. Overridable by --vmodule.See the section about verbose logging for moredetail.

vmodule (string, default="")

Per-module verbose level. The argument has to contain acomma-separated list of =.is a glob pattern (e.g., gfs* for all modules whose namestarts with "gfs"), matched against the filename base(that is, name ignoring .cc/.h./-inl.h). overrides any value given by --v.See also the section about verbose logging.

还有一些其它的标志,自己去logging.cc里面搜索"DEFINE_",爷就不多说了.

你也可以在程序里修改FLAGS_* 开头的全局变量. 通常设置FLAGS_*开头的变量会立即生效,和日志文件名或路径相关的变量例外.例如FLAGS_log_dir就不能放在google::InitGoogleLogging后面.例子代码:

LOG(INFO) <<"file"

// Most flags work immediately after updating values.

FLAGS_logtostderr = 1

LOG(INFO) <<"stderr"

FLAGS_logtostderr = 0

// This won't change the log destination. If you want to set this

// value, you should do this before google::InitGoogleLogging .

FLAGS_log_dir = "/some/log/directory"

LOG(INFO) <<"the same file"

条件/ Occasional Logging

下面的宏用于根据条件写日志:

LOG_IF(INFO, num_cookies >10) <<"Got lots of cookies"

只有当num_cookies >10的时候才会记录"Got lots of cookies"

如果有的代码执行非常频繁,你肯定不会希望每次都写入日志.这时,你可以使用下面的方法:

LOG_EVERY_N(INFO, 10) < Got the googleCOUNTER th cookiepre>

上面的代码只有当第1次,第11次,第21次....执行时才写入日志.Note that the specialgoogle::COUNTER value is used to identify which repetition ishappening.

条件和计次可以混合使用,例如:

LOG_IF_EVERY_N(INFO, (size >1024), 10) <<"Got the " <<google::COUNTER

<<"th big cookie"

除了间隔N次输出,还可以只输出前M次,例如:

LOG_FIRST_N(INFO, 20) < Got the googleCOUNTER th cookiepre>

只输出前20次.

Debug mode支持

"debug mode"的宏只在调试模式有效,其他模式不生效.上栗子:

DLOG(INFO) <<"Found cookies"

DLOG_IF(INFO, num_cookies >10) <<"Got lots of cookies"

DLOG_EVERY_N(INFO, 10) <<"Got the " <<google::COUNTER <<"th cookie"

CHECK宏

经常检查是否会出错而不是等着程序执行出错是个不错的习惯.CHECK宏就是干这个滴,它的功能和assert一样,条件不符合时终止程序.

与assert不同的是它*不*受NDEBUG的限制,无论是不是debug编译它都执行.所以下面的fp-<Write(x)会被执行:

CHECK(fp->Write(x) == 4) <<"Write failed!"

有各种各样的宏用于CHECK相等/不相等,包括: CHECK_EQ,CHECK_NE, CHECK_LE, CHECK_LT,CHECK_GE,和CHECK_GT.它们会记录一个FATAL级的记录到日志,牛X的是他们还会把对比的两个值也一起记录.(那两个值必须定义了 operator<ostreamcode>).例如:

CHECK_NE(1, 2) <<": The world must be ending!"

We are very careful to ensure that each argument is evaluated exactlyonce, and that anything which is legal to pass as a function argument islegal here. In particular, the arguments may be temporary expressionswhich will end up being destroyed at the end of the apparent statement,for example:

CHECK_EQ(string("abc")[1], 'b')

The compiler reports an error if one of the arguments is apointer and the other is NULL. To work around this, simply static_castNULL to the type of the desired pointer.

CHECK_EQ(some_ptr, static_cast(NULL))

还有个好办法:用CHECK_NOTNULL宏:

CHECK_NOTNULL(some_ptr) some_ptr-<DoSomething()

这宏也常用于构造函数中.

struct S {

S(Something* ptr) : ptr_(CHECK_NOTNULL(ptr)) {}

Something* ptr_

}

这宏的缺点是不能跟C++流一起用.这时就得用CHECK_EQ了.

如果你要对比一个C字符串(char *)可以用:CHECK_STREQ, CHECK_STRNE,CHECK_STRCASEEQ,和CHECK_STRCASENE.带CASE的区分大小写. 这个宏参数可以用NULL指针.NULL与non-NULL比不等.两个NULL是相等的.

Note that both arguments may be temporary strings which aredestructed at the end of the current "full expression"(e.g., CHECK_STREQ(Foo().c_str(), Bar().c_str()) whereFoo and Bar return C++'sstd::string).

CHECK_DOUBLE_EQ 用于对比浮点数,会有小小的误差.CHECK_NEAR 可以接受一个浮点数作为误差.

详细日志

用VLOG宏,你还可以定义自己的严重等级. The --v command line option controlswhich verbose messages are logged:

VLOG(1) <<"I'm printed when you run the program with --v=1 or higher"

VLOG(2) <<"I'm printed when you run the program with --v=2 or higher"

With VLOG, the lower the verbose level, the morelikely messages are to be logged. For example, if--v==1, VLOG(1) will log, butVLOG(2) will not log. This is opposite of the severitylevel, where INFO is 0, and ERROR is 2.--minloglevel of 1 will log WARNING andabove. Though you can specify any integers for both VLOGmacro and --v flag, the common values for them are smallpositive integers. For example, if you write VLOG(0),you should specify --v=-1 or lower to silence it. Thisis less useful since we may not want verbose logs by default in mostcases. The VLOG macros always log at theINFO log level (when they log at all).

Verbose logging can be controlled from the command line on aper-module basis:

--vmodule=mapreduce=2,file=1,gfs*=3 --v=0

will:

a. Print VLOG(2) and lower messages from mapreduce.{h,cc}

b. Print VLOG(1) and lower messages from file.{h,cc}

c. Print VLOG(3) and lower messages from files prefixed with "gfs"

d. Print VLOG(0) and lower messages from elsewhere

The wildcarding functionality shown by (c) supports both '*'(matches 0 or more characters) and '?' (matches any single character)wildcards. Please also check the section about command line flags.

There's also VLOG_IS_ON(n) "verbose level" conditionmacro. This macro returns true when the --v is equal orgreater than n. To be used as

if (VLOG_IS_ON(2)) {

// do some logging preparation and logging

// that can't be accomplished with just VLOG(2) <<...

}

Verbose level condition macros VLOG_IF,VLOG_EVERY_N and VLOG_IF_EVERY_N behaveanalogous to LOG_IF, LOG_EVERY_N,LOF_IF_EVERY, but accept a numeric verbosity level asopposed to a severity level.

VLOG_IF(1, (size >1024))

<<"I'm printed when size is more than 1024 and when you run the "

"program with --v=1 or more"

VLOG_EVERY_N(1, 10)

<<"I'm printed every 10th occurrence, and when you run the program "

"with --v=1 or more. Present occurence is " <<google::COUNTER

VLOG_IF_EVERY_N(1, (size >1024), 10)

<<"I'm printed on every 10th occurence of case when size is more "

" than 1024, when you run the program with --v=1 or more. "

"Present occurence is " <<google::COUNTER

Failure Signal Handler

The library provides a convenient signal handler that will dump usefulinformation when the program crashes on certain signals such as SIGSEGV.The signal handler can be installed bygoogle::InstallFailureSignalHandler(). The following is an example of outputfrom the signal handler.

*** Aborted at 1225095260 (unix time) try "date -d @1225095260" if you are using GNU date ***

*** SIGSEGV (@0x0) received by PID 17711 (TID 0x7f893090a6f0) from PID 0stack trace: ***

PC: @ 0x412eb1 TestWaitingLogSink::send()

@ 0x7f892fb417d0 (unknown)

@ 0x412eb1 TestWaitingLogSink::send()

@ 0x7f89304f7f06 google::LogMessage::SendToLog()

@ 0x7f89304f35af google::LogMessage::Flush()

@ 0x7f89304f3739 google::LogMessage::~LogMessage()

@ 0x408cf4 TestLogSinkWaitTillSent()

@ 0x4115de main

@ 0x7f892f7ef1c4 (unknown)

@ 0x4046f9 (unknown)

By default, the signal handler writes the failure dump to the standarderror. You can customize the destination by InstallFailureWriter().

Miscellaneous Notes

Performance of Messages

The conditional logging macros provided by glog (e.g.,CHECK, LOG_IF, VLOG, ...) arecarefully implemented and don't execute the right hand sideexpressions when the conditions are false. So, the following checkmay not sacrifice the performance of your application.

CHECK(obj.ok) <<obj.CreatePrettyFormattedStringButVerySlow()

User-defined Failure Function

FATAL severity level messages or unsatisfiedCHECK condition terminate your program. You can changethe behavior of the termination byInstallFailureFunction.

void YourFailureFunction() {

// Reports something...

exit(1)

}

int main(int argc, char* argv[]) {

google::InstallFailureFunction(&YourFailureFunction)

}

By default, glog tries to dump stacktrace and makes the programexit with status 1. The stacktrace is produced only when you run theprogram on an architecture for which glog supports stack tracing (asof September 2008, glog supports stack tracing for x86 and x86_64).

Raw Logging

The header file can beused for thread-safe logging, which does not allocate any memory oracquire any locks. Therefore, the macros defined in thisheader file can be used by low-level memory allocation andsynchronization code.Please check src/glog/raw_logging.h.in for detail.

Google Style perror()

PLOG() and PLOG_IF() andPCHECK() behave exactly like their LOG* andCHECK equivalents with the addition that they append adescription of the current state of errno to their output lines.E.g.

PCHECK(write(1, NULL, 2) <= 0) < Write NULL failedpre>

This check fails with the following error message.

F0825 185142 test.cc:22] Check failed: write(1, NULL, 2) <= 0 Write NULL failed: Bad address [14]

Syslog

SYSLOG, SYSLOG_IF, andSYSLOG_EVERY_N macros are available.These log to syslog in addition to the normal logs. Be aware thatlogging to syslog can drastically impact performance, especially ifsyslog is configured for remote logging! Make sure you understand theimplications of outputting to syslog before you use these macros. Ingeneral, it's wise to use these macros sparingly.

Strip Logging Messages

Strings used in log messages can increase the size of your binaryand present a privacy concern. You can therefore instruct glog toremove all strings which fall below a certain severity level by usingthe GOOGLE_STRIP_LOG macro:

If your application has code like this:

#define GOOGLE_STRIP_LOG 1// this must go before the #include!

#include <glog/logging.h>

The compiler will remove the log messages whose severities are lessthan the specified integer value. SinceVLOG logs at the severity level INFO(numeric value 0),setting GOOGLE_STRIP_LOG to 1 or greater removesall log messages associated with VLOGs as well asINFO log statements.

对于Windows用户

glog的ERROR级错误与windows.h冲突. You can make glog not defineINFO, WARNING, ERROR,and FATAL by definingGLOG_NO_ABBREVIATED_SEVERITIES beforeincluding glog/logging.h . Even with this macro, you canstill use the iostream like logging facilities:

#define GLOG_NO_ABBREVIATED_SEVERITIES

#include <windows.h>

#include <glog/logging.h>

// ...

LOG(ERROR) <<"This should work"

LOG_IF(ERROR, x >y) <<"This should be also OK"

However, you cannotuse INFO, WARNING, ERROR,and FATAL anymore for functions definedin glog/logging.h .

#define GLOG_NO_ABBREVIATED_SEVERITIES

#include <windows.h>

#include <glog/logging.h>

// ...

// This won't work.

// google::FlushLogFiles(google::ERROR)

// Use this instead.

google::FlushLogFiles(google::GLOG_ERROR)

If you don't need ERROR definedby windows.h, there are a couple of more workaroundswhich sometimes don't work:

#define WIN32_LEAN_AND_MEAN or NOGDIbefore you #include windows.h .

#undef ERRORafter you #include windows.h .

See this issue for more detail.