错误处理(包括日志记录)

错误处理

简单的错误处理是使用 Fprintf 和 %v 在标准错误流上输出一条消息,%v 可以使用默认格式显示任意类型的值。
为了保持示例代码简短,有时会对错误处理有意进行一定程度的忽略。明显的错误还是要处理的。但是有些出现概率很小的错误,就忽略了,不过要标记所跳过的错误检查,就是加上注释。

根据情形,将有许多可能的处理场景,接下来是5个例子。

一、将错误传递下去

最常见的情形是将错误传递下去,使得在子例程中发生的错误变为主调例程的错误。
一种是不做任何操作立即向调用者返回错误:

resp, err := http.Get(url)
if err != nil {
    return nil, err
}

还有一种,不会直接返回,因为错误信息中缺失一些关键信息:

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v\n", url, err)
}

这里格式化了一条错误消息并且返回一个新的错误值。可以为原始的错误消息不断地添加上下文信息来建立一个可读的错误描述。当错误最终被程序的 main 函数处理时,它应该能够提供一个从最根本问题到总体故障的清晰因果链、这里有一个 NASA 的事故调查的例子:

genesis: crashed: no parachute: G-switch failed: bad relay orientation

因为错误频繁地串联起来,所以消息字符串首字母不应该大写而且应该避免换行。错误结果可能会很长,但能能够使用 grep 这样的工具找到需要的信息。

需要添加的关键信息
有时候可以不用添加信息直接返回,有时候需要添加一些关键信息,因为错误信息里没有。比如 os.Open 打开文件时,返回的错误不仅仅包括错误的信息,还包含文件的名字,因此调用者构造错误消息的时候不需要包含文件的名字这类信息。具体哪些信息是缺少的关键信息需要在原始的错误消息的基础上添加?
一般地,f(x) 调用只负责报告函数的行为 f 和参数值 x,因为它们和错误的上下文相关。调用者则负责添加进一步的信息,但是 f(x) 本身并不会,并且在函数内部也没有这些信息。
比如上面的 html.Parse 返回的错误信息里不可能有 url 的信息,但是,是关键信息需要添加。而 os.Open 中,文件名字也是关键信息,但是这个正是函数的参数值,所以函数本身会返回这个信息,不需要另外添加。

二、尝试重试

对于不固定或者不可预测的错误,在短暂的间隔后对操作进行重试是合乎情理的。超出一定的重试次数和限定的时间后再报错退出。
下面给出了完整的代码,暂时只看 WaitForServer 函数:

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

// 尝试连接 url 对应的服务器
// 在一分钟内使用指数退避策略进行重试
// 所有的尝试失败后返回错误
func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil // 成功
        }
        log.Printf("server not responding (%s); retrying...", err)
        time.Sleep(time.Second << uint(tries)) // 指数退避策略
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "需要提供 url 参数\n")
        os.Exit(1)
    }
    url := os.Args[1]
    if err := WaitForServer(url); err != nil {
        fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
        os.Exit(1)
    }
}

这里的指数退避策略,以及尝试多次简单的超时退出的实现也很有意思。

三、输出日志并退出

接着看上面的代码,如果多次重试后依然不能成功,调用者能够输出错误然后优雅地停止程序,但一般这样的处理应该留给主程序部分:

if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

通常,如果是库函数,应该将错误传递给调用者,除非这个错误表示一个内部的一致性错误,这意味着库内部存在 bug。
这里还有一个更加方便的方法是通过调用 log.Fatalf 实现上面相同的效果。和所有的日志函数一样,它默认会将时间和日期作为前缀添加到错误消息前:

if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

这种带日期时间的默认格式有助于长期运行的服务器,而对于交互式的命令行工具则意义不大。
还可以自定义命令的名称作为 log 包的前缀,并且将日期和时间略去:

log.SetPrefix("wait: ")
log.SetFlags(0)

四、记录log日志

在一些错误情况下,只记录下错误信息然后程序继续运行。同样地,可以选择使用 log 包来增加日志的常用前缀:

if err := Ping(): err != nil {
    log.Printf("Ping failed: %v; networking disabled", err)
}

所有 log 函数都会为缺少换行符的日志补充一个换行符。
或者是,直接输出到标准错误流:

if err := Ping(): err != nil {
    fmt.Fprintf(os.Stderr, "Ping failed: %v; networking disabled\n", err)
}

没有用 log 函数,所以没有时间日期,当然也不需要。上面说了,对于交互式的命令工具意义不大。

五、忽略错误

在某些罕见的情况下,还可以直接安全地忽略掉整个日志:

dir, err := ioutil.TempDir("", "scratch")
if err != nil {
    return fmt.Errorf("failed to create temp dir: %v", err)
}
// 使用临时的目录
os.RemoveAll(dir)  // 忽略错误,$TMPDIR 会被周期性删除

调用 os.RemoveAll 可能会失败,但程序忽略了这个错误,原因是操作系统会周期性地清理临时目录。在这个例子中,有意的抛弃了错误,但程序的逻辑看上去就和忘记去处理一样了。要习惯考虑到每一个函数调用可能发生的出错情况,当有意忽略一个错误的时候,要清楚地注释一下你的意图。

error 接口

之前已经使用过 error 类型了,实际上它是一个接口类型,包含一个返回错误消息的方法:

type error interface {
    Error() string
}

errors 包

构造 error 最简单的方法是调用 errors.New,它会返回一个包含指定错误消息的新 error 实例。
完整的 errors 包其实只有如下的4行代码:

package errors

func New(text string) error { return &errorString{text} }

type errorString struct { s string }

func (e *errorString) Error() string { return e.s }

底层的 errorString 类型是一个结构体,而不是像其他包里那样定义字符串的别名类型。这主要是为了保护它所表示的错误值无意间的(或者也可能是故意的)更新。
定义的 Error 方法是指针方法,而不是值方法。这样每次 New 分配的 error 实例都互不相等,即使是同样的错误值,也是不同的地址:

fmt.Println(errors.New("TEST") == errors.New("TEST")) // false

这样可以避免比如像 io.EOF 这样重要的错误,与仅仅只是包含同样错误消息的一个错误相等。

fmt.Errorf

直接调用 errors.New 的情况比较少,只在直接能取得错误值的字符串信息的时候使用:

func startCPUProfile(w io.Writer) error {
    if w == nil {
        return errors.New("nil File")
    }
    return pprof.StartCPUProfile(w)
}

更多的情况是会得到一个错误值 err,而我们可以在这个错误值之上做一点包装,还需要做字符串格式化。有一个更易用的封装函数 fmt.Errorf,它额外还提供了字符串格式化的功能,所以一般都是用这个:

doc, err := html.Parse(resp.Body)
if err != nil {
    return fmt.Errorf("parseing %s as HTML: %v", url, err)
}

原文地址:https://blog.51cto.com/steed/2390099

时间: 2024-11-10 01:05:43

错误处理(包括日志记录)的相关文章

PHP 错误与异常的日志记录

提到 Nginx + PHP 服务的错误日志,我们通常能想到的有 Nginx 的 access 日志.error 日志以及 PHP 的 error 日志.虽然看起来是个很简单的问题,但里面其实又牵扯到应用配置以及日志记录位置的问题,如果是在 ubuntu 等系统下使用 apt-get 的方式来安装,其自有一套较为合理的的配置文件可用.再者运行的应用程序中的配置也会影响到日志记录的方式及内容. 错误与异常的区别 关于错误与异常,我们可以用一个简单的例子来理解: <?php try { 1 / 0;

解决Apache的错误日志巨大的问题以及关闭Apache web日志记录

调整错误日志的级别 这几天 apache错误日志巨大 莫名其妙的30G  而且 很多都是那种页面不存在的  网站太多了  死链接相应的也很多于是把错误警告调低了 因为写日志会给系统带来很大的损耗.关闭日志以后,甚至最高可以提高整体性能近40%(粗略估计)那么如何关闭日志呢? 可以通过降低log级别的办法来减少日志读写. 这里要提醒的是,这么做将给"入侵检测"以及其他基于日志分析的工作带来麻烦.所以请谨慎使用.网上相关文章很多,但说的都不详细,擦边而过,下面详细说一下具体操作步骤. 编辑

错误和日志记录

错误和日志记录 值 常量 说明 备注 1 E_ERROR 致命的运行时错误.这类错误一般是不可恢复的情况,例如内存分配导致的问题.后果是导致脚本终止不再继续运行. 2 E_WARNING 运行时警告 (非致命错误).仅给出提示信息,但是脚本不会终止运行. 4 E_PARSE 编译时语法解析错误.解析错误仅仅由分析器产生. 8 E_NOTICE 运行时通知.表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知. 16 E_CORE_ERROR 在PHP初始化启动过程

自定义错误日志记录类

引言 这是一个简单的自定义的错误日志记录类,这里我主要用于API接口开发中,APP移动端的入参记录 日志参数 /// <summary> /// 日志参数 /// </summary> public static class LogReq { /// <summary> /// 入参 /// </summary> public static string LogReqStr = ""; /// <summary> /// 加密

asp.net MVC自定义错误页,并记录错误日志

只需要在Global.asax文件中添加以下代码,则可以在出错后友好的展示错误页,也不需要在很多地方写记录错误日志的代码 protected void Application_Error(object sender, EventArgs e) { if (HttpContext.Current.IsCustomErrorEnabled) { return; } var exception = Server.GetLastError(); var httpException = new HttpE

PHP的日志记录-错误与异常记录

提到 Nginx + PHP 服务的错误日志,我们通常能想到的有 Nginx 的 access 日志.error 日志以及 PHP 的 error 日志.虽然看起来是个很简单的问题,但里面其实又牵扯到应用配置以及日志记录位置的问题,如果是在 ubuntu 等系统下使用 apt-get 的方式来安装,其自有一套较为合理的的配置文件可用.再者运行的应用程序中的配置也会影响到日志记录的方式及内容. 错误与异常的区别 关于错误与异常,兄弟连来给大家举一个简单的例子来理解: <?php try { 1 /

路径问题 Global文件中写入错误日志记录

“~”表示Web 应用程序根目录,“/”也是表示根目录,“../”表示当前目录的上一级目录,“./”表示当前目录 1  throw抛出异常     2 执行OnActionExecuted 方法   3执行 Global  中的 Application_Error 方法写入日志 global文件中写入  错误日志记录 protected void Application_Error(object sender,EventArgs e) { Exception lastError = Server

.Net Core中间件和过滤器实现错误日志记录

1.中间件的概念 ASP.NET Core的处理流程是一个管道,中间件是组装到应用程序管道中用来处理请求和响应的组件. 每个中间件可以: 选择是否将请求传递给管道中的下一个组件. 可以在调用管道中的下一个组件之前和之后执行业务逻辑. 中间件是一个请求委托( public delegate Task RequestDelegate(HttpContext context) )的实例,所以中间件的本质就是一个方法,方法的参数是HttpContext,返回Task.传入的HttpContext参数包含

asp.net Web项目中使用Log4Net进行错误日志记录

使用log4net可以很方便地为应用添加日志功能.应用Log4net,开发者可以很精确地控制日志信息的输出,减少了多余信息,提高了日志记录性能.同时,通过外部配置文件,用户可以不用重新编译程序就能改变应用的日志行为,使得用户可以根据情况灵活地选择要记录的信息. 那么我们如何在Web项目中使用Log4Net呢? 一.基本配置 1.下载Log4Net,地址如下:http://logging.apache.org/log4net/download_log4net.cgi,如下图所示: 2.下载到本地后