error 处理与包装

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: error 处理与包装

在该系列中


错误处理的话题足够有趣,为了保持零件的数量,我决定将其分成几个部分:


  • error处理
  • error包装


错误处理


我如何在Go中尝试捕捉异常?


在go中,error 是数值。


这意味着,error 不是"抛出",而是一个返回值,就像其他的值一样。例如,要打开一个文件:


f, err := os.Open("config.yml")


Open函数返回两个值:文件描述符和 error。在成功时,错误为nil,否则它有一些值。


if err != nil {
    // ... do something
    return err
}


通常的错误处理模式是检查错误是否为空。你可能会说,这样的明确性伴随着冗长的语言。是的,Golang代码中经常散布着if err != nil


哦,拜托,我是不是每次都要检查err !=nil?


在Go的bufio.Scanner中可以找到一个巧妙的例子,避免不断检查错误是否为nil。


const input = "The quick brown fox jumps over lazy dog."
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(bufio.ScanWords)
count := 0
// We're not checking the error, just iterate
for scanner.Scan() {
    count++
}
// See if there was any error in the end
if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading input:", err)
}


scanner.Scan()在有匹配的情况下返回true,在没有匹配或有错误的情况下返回false。任何在扫描过程中可能发生的错误都会被记录下来。在扫描循环结束后,将推迟检查:不需要每次迭代都检查。


实际上"error"是什么?


它可以是任何东西。一般来说,它是任何实现了简单的、单一方法的错误接口的类型:


type error interface {
    Error() string
}


开箱即用,Go提供了内置的Error.New构造函数。


errors.New("missing ID parameter")


或更灵活的fmt.Errorf():


return fmt.Errorf("Unsupported message type: %q", msgType)


返回error


这很容易,因为Go允许多个返回值。把错误类型作为最后一个。如果一切顺利,只需返回nil。


func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divide by zero prohibited")
    }
    return a/b, nil
}

自定义 error 类型


在绝大多数情况下,你对内部是字符串的错误没有意见。最终,它们只是被记录在某个地方。


如果你需要根据返回的错误做出决定,不要试图解析其字符串值。为了给错误提供更多的背景,你可以创建你自己的类型。


想象一下,你想从HTTP服务器处理函数中返回带有状态码的错误:


type HttpError struct {
    StatusCode int
    Err        error
}
func (h HttpError) Error() string {
    return fmt.Sprintf("HTTP error: %v (%d)", h.Err, h.StatusCode)
}


error 是值,所以要像其他结构一样创建和返回:


func handlerFunc(r http.Request) error {
    // ...
    // something went wrong:
    return HttpError{
        StatusCode: 404,
        Err:        errors.New("product not found"),
    }
    // ...
}


然后使用类型断言将一般的 error 转换为我们特定的HttpError


he, ok := err.(HttpError)
if ok {
    log.Printf("HTTP error with status = %d", he.StatusCode)
}


或者使用推荐的、更安全的方式:


var he HttpError
if errors.As(err, &he) {
        log.Printf("HTTP error with status = %d", he.StatusCode)
}


日志


提示:在堆栈深处处理错误,在顶部记录。


这种方法有助于防止重复的日志。使用包装技术来增加上下文和调试时有用的信息。流行的软件包https://pkg.go.dev/github.com/pkg/errors 也允许在日志中保留堆栈跟踪。


error 包装


让我们考虑一个例子:main()调用readConfig(),后者又调用readFromFile()。这是一个简化的例子,说明代码中出现的模式:嵌套调用和错误发生在低层。


让我们使用谷歌错误包中的errors.Wrap()


func readFromFile() (string, error) {
    data, err := os.ReadFile("wrong file name")
    if err != nil {
        return "", errors.Wrap(err, "readFromFile")
    }
    return string(data), nil
}
func readConfig() (string, error) {
    data, err := readFromFile()
    if err != nil {
        return "", errors.Wrap(err, "readConfig")
    }
    // ...
    return data, nil
}
func main() {
    conf, err := readConfig()
    if err != nil {
        log.Printf("Cannot read: %v", err)
    }
}


output:


2022/03/16 00:37:54 Cannot read: readConfig: readFromFile: open wrong file name: no such file or directory


在每个包裹着错误的层次,你可以添加一条信息。一个原因,也可能只是一个函数名称或其他抽象层次的标识。

检索根本原因


如果你需要跟踪的最底层错误,使用


errors.Cause(err)


来检索堆栈中的第一个错误。该函数是安全的,所以当提供一个没有包装的错误时,它将返回错误。


内置包装


第二种错误包装的方式,允许有一点灵活性,就是使用:


err = fmt.Errorf("read file: %w", err)


注意%w格式指定符,它用错误的文本值来代替。请记住,这种形式不保留堆栈痕迹。

stack trace


使用errors.Wrap()函数包装的错误会保留调用堆栈。要打印它,请使用%+v格式指定符:


log.Printf("Cannot read: %+v", err)


output:


2022/07/22 18:51:54 Cannot read: open wrong file name: no such file or directory
readFromFile
awesomeProject/learnWrapping.readFromFile
        /Code/learn/go/awesomeProject/learnWrapping/wrapping.go:13
awesomeProject/learnWrapping.readConfig
        /Code/learn/go/awesomeProject/learnWrapping/wrapping.go:19
awesomeProject/learnWrapping.Main
        /Code/learn/go/awesomeProject/learnWrapping/wrapping.go:28
main.main
        /Users/tomek/Code/learn/go/awesomeProject/main.go:6


堆栈跟踪可以通过另一种有点隐晦的方式来检索。包含堆栈跟踪的错误实现了私有的stackTracer接口(它是私有的,因为名字以小写字母开头)。但是,这是Go,你可以在你的代码中重新声明这个接口:


type stackTracer interface {
        StackTrace() errors.StackTrace
}


并单独访问每个堆栈框架:


if sterr, ok := err.(stackTracer); ok {
    log.Printf("Stack trace:")
    for n, f := range sterr.StackTrace() {
        fmt.Printf("%d: %s %n:%d\n", n, f, f, f)
    }
}


output:


2022/07/23 15:11:17 Stack trace:
0: wrapping.go readConfig:21
1: wrapping.go Main:32
2: main.go main:6
3: proc.go main:250
4: asm_arm64.s goexit:1259


在堆栈深处处理错误,在顶部记录


包裹是保存上下文信息的有用技术,有助于避免重复记录语句。


让我们重温一下上面的例子:在第四行,用黑体字突出显示,文件系统读取错误被记录下来。然后它又被main()函数记录下来,导致双重日志。


func readFromFile() (string, error) {
    data, err := os.ReadFile("wrong file name")
    if err != nil {
        log.Printf("readFromFile failed")
        return "", errors.Wrap(err, "readFromFile")
    }
    return string(data), nil
}
func readConfig() (string, error) {
    data, err := readFromFile()
    if err != nil {
        return "", errors.Wrap(err, "readConfig")
    }
    // ...
    return data, nil
}
func main() {
    conf, err := readConfig()
    if err != nil {
        log.Printf("Cannot read: %v", err)
    }
}


结果我们会在日志中看到重复的语句:


2022/09/13 14:22:41 Cannot read file: "dummyfile.txt"
2022/09/13 14:22:41 Cannot read: readConfig: readFromFile: open dummyfile.txt: no such file or directory


在上面的例子中,包装好的错误组合提供了足够的上下文来确定错误的原因。在第一行,我们得到了断章取义的声明,这说明了什么。


当然,你可以将Context下游传递给每一个方法,但我不认为这样做的好处会超过污染造成的不清晰。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5月前
|
JSON 前端开发 JavaScript
ES6类的使用和定义.Json.Promise对象的使用
ES6类的使用和定义.Json.Promise对象的使用
55 0
|
1天前
|
Java
Error和Exception的关系以及区别
文章解释了Java中Error和Exception的区别和关系,其中Error表示严重的系统级问题,通常不可被程序捕获处理,而Exception表示可以被捕获和处理的异常,分为已检查和未检查两种类型。
6 0
|
2月前
|
小程序
[渲染层错误] [jsbridge] invoke remoteDebugInfo fail: too eayly.
这篇文章分享了在小程序开发过程中遇到的一个渲染层错误,原因是路径错误,特别是图片后缀名的遗漏,通过手动修正后问题得到解决。
[渲染层错误] [jsbridge] invoke remoteDebugInfo fail: too eayly.
|
Web App开发 安全 Windows
解决:对COM 组件的调用返回了错误 HRESULT E_FAIL
  调用SHDOCVW(web浏览器) COM组件的时候,返回了错误 HRESULT E_FAIL。总结如下:     1. 在控制面板--->管理工具--->服务 中,开启Distributed Transaction Coordinator 服务。
6261 0
|
11月前
|
JSON Java API
优雅地进行全局异常处理、统一返回值封装、自定义异常错误码——Graceful-Response推荐
Graceful Response是一个Spring Boot体系下的优雅响应处理器,提供一站式统一返回值封装、全局异常处理、自定义异常错误码等功能,使用Graceful Response进行web接口开发不仅可以节省大量的时间,还可以提高代码质量,使代码逻辑更清晰。
327 0
|
C++
【C++】function包装器
【C++】function包装器
41 0
|
5月前
|
存储 编译器 C++
C++ 包装器—function
C++ 包装器—function
|
5月前
|
存储 C++
【C++11特性篇】玩转C++11中的包装器(function&bind)
【C++11特性篇】玩转C++11中的包装器(function&bind)
|
前端开发
手动封装call
手动封装call
58 0