• go 1.13的错误处理


    go 1.13的错误处理

    原文链接

    本文有些难懂,建议看完这篇博客再看.

    把错误当初数值的方式在过去的十年给我们提供许多便利,但是标准库中对错误的支撑却很少,比,只有errors.Newfmt.Errorf这两个函数,他们创造只包含一条信息的错误,内置的error接口允许go程序员添加他们想要的错误类型。只需要实现error方法。

    type QueryError struct {
        Query string
        Err   error
    }
    
    func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
    

    像这类的error 类型无所不在,并且他们所储存的信息十分广泛,从时间戳到文件名到服务器IP。通常,这个信息还包括其他信息,比如较为低级的错误信息,以便提供额外的上下文信息。在一个错误中包含另一个错误,这样的模式在go中普遍存在,所以在经过了广泛的讨论后,go 1.13决定对其添加显式的支持。本文描述了三个errors包的新函数和一个fmt.Errorf的新动词。

    在讨论这些改变之前,我们先复习一下,在之前的版本中,我们是如何去检测和构建一个错误的。

    go 1.13 前的错误

    检测错误

    go的错误是数值,在多数情况下,程序基于这些数值做出决策。最常见的方法是把错误和nil相比较,去判断一个人操作是否失败。

    if err != nil {
        // something went wrong
    }
    

    有时候,我们会把错误和一个哨兵值相对比,从而判断是否发生了特定的错误。

    var ErrNotFound = errors.New("not found")
    
    if err == ErrNotFound {
        // something wasn't found
    }
    

    一个错误类型可能是任何满足了语言定义的error接口的类型,程序可以使用类型断言和类型转换去更详细的观察错误。

    type NotFoundError struct {
        Name string
    }
    
    func (e *NotFoundError) Error() string { return e.Name + ": not found" }
    
    if e, ok := err.(*NotFoundError); ok {
        // e.Name wasn't found
    }
    

    额外信息

    在函数向调用堆栈添加信息是,通常会把错误向上传递给调用堆栈,例如一段简单的信息,描述了什么时候发生的错误。一个简单的实现方式是构造一个包含了之前错误的错误。

    if err != nil {
        return fmt.Errorf("decompress %v: %v", name, err)
    }
    

    fmt.Errorf创建一个错误会丢失掉原始错误中除了内容之外的东西。正如我们上文所看到的QueryError,我们想要定义一个错误,他包含了之前的错误,以便在代码中我们能检查它。

    type QueryError struct {
        Query string
        Err   error
    }
    

    程序可以通过观察 *QueryError内部,基于表面下的错误做出决策,这种情况有事被称为错误展开。(unwrapping)

    if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
        // query failed because of a permission problem
    }
    

    os.PathError 在标准库中也是另一个例子,一个错误包含着另一个错误。

    go 1.1中的错误

    展开方法

    go 1.13 引进了新特性给errorfmt标准库,以简化错误中包含着错误的工作。其中最重要的一点说是改变倒不如说是约定:包含着其他错误的错误可以实现Unwrap方法,它返回内层错误。如果e1.Unwrap()返回e2,那么我们说e1包含e2,或者展开e1得到e2。

    遵循以下约定,我们可以得到QueryError 类型,并且实现Unwrap方法,返回其内层方法。

    func (e *QueryError) Unwrap() error { return e.Err }
    

    将错误展开后,我们可能也会得到一个包含着另一个有着Unwrap方法的错误。我们将这种情况称之为重复展开错误链 而产生了一个错误序列。

    用IS和AS检测错误

    在go1.13中,error包有了两个新函数以检测错误,一个是Is一个是As

    errors.Is函数比较错误与值。

    //类似以下情况
    //   if err == ErrNotFound { … }
    if errors.Is(err, ErrNotFound) {
        // something wasn't found
    }
    
    

    As函数检查一个错误是否为特定的类型

    // 类似以下情况
    //   if e, ok := err.(*QueryError); ok { … }
    var e *QueryError
    if errors.As(err, &e) {
        // err is a *QueryError, and e is set to the error's value
    }
    
    

    在最简单的情况下,errors.Is 函数的作用类似于与哨兵值比较,而 errors.As 函数的行为更接近于类型声明。对包裹过的错误进行操作时,这些函数会考虑链中的所有错误。我们再使用QueryError来说明两个新函数的使用。

    if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
        // 由于权限问题,查询失败
    }
    
    

    使用errors.Is函数后

    if errors.Is(err, ErrPermission) {
        // err 本身或者 err 包裹的错误,是一个权限问题
    }
    
    

    errors包内有一个新的Unwrap方法,他的返回值是调用错误的Unwrap方法返回的结果,如果错误没有Unwrap方法,就会返回nil,最好还是使用errors.Iserrors.As,这些方法都会在一次调用中检查整条错误链。

    用 %w 包装错误

    正如之前提到的,通常方法是用fmt.Errorf函数去添加额外信息。

    if err != nil {
        return fmt.Errorf("decompress %v: %v", name, err)
    }
    
    

    在 Go 1.13 中,fmt.Errorf 函数支持新的 %w 动词。当存在该动词时,fmt.Errorf 返回的错误将具有 Unwrap 方法,该方法返回 %w 的参数,该参数必须是错误。在其他方面,%w %v 相同。

    if err != nil {
        // Return an error which unwraps to err.
        return fmt.Errorf("decompress %v: %w", name, err)
    }
    
    

    %w包装一个错误,使得它可以使用errors.Iserrors.As

    err := fmt.Errorf("access denied: %w", ErrPermission)
    ...
    if errors.Is(err, ErrPermission) ... 
    
    

    是否需要包装错误

    向错误中添加额外上下文的时候,使用fmt.Errorf或者通过实现自定义类型。你需要决定是否需要包装之前的错误。这个问题没有一个答案。它取决于创建新错误的上下文。包装错误可以将原始错误暴露给调用者。当不希望暴露细节的时候请不要包装错误。

    译注:细节指错误信息附带的其他敏感信息,比如路径信息等,当用户调用错误时,系统不希望用户通过错误信息得知数据库等地址

    举个例子来说,一个Parse函数,他读取复杂的数据结构从io.Reader中,如果一个错误发生,我们希望它能报告出发生错误的行和列,如果从 io.Reader 读取时发生错误,我们将希望包装该错误以允许检查基本问题。由于调用者向函数提供了 io.Reader,因此暴露由它产生的错误是有意义的。

    相反,对数据库进行多次调用的函数不应该返回将展开后的错误。如果该函数使用的数据库是实现细节,那么暴露这些错误就违背了抽象的原则。例如,如果你编写的 LookupUser 函数使用 Go 的 database / sql 包,则它可能会遇到 sql.ErrNoRows 错误。如果你使用 fmt.Errorf(“ accessing DB:%v”,err) 返回该错误,则调用者无法在程序内部查找 sql.ErrNoRows。但是如果函数反而返回 fmt.Errorf(“ accessing DB:%w”,err),则调用者可以这样编写代码:

    err := pkg.LookupUser(...)
    if errors.Is(err, sql.ErrNoRows) …
    
    

    此时,即使你不希望中断客户端,即使切换到其他数据库包,该函数也必须始终返回sql.ErrNoRows。 换句话说,包装错误会使该错误成为API的一部分。 如果您不想将来将错误作为API的一部分来支持,则不应包装该错误。

    请务必记住,无论是否包装错误,错误文本都将相同。 将以两种方式获得相同的信息。 包装的选择是关于是否给程序提供更多信息,以便他们可以做出更明智的决定,还是保留该信息以保留抽象层。

    使用IsAs定制错误

    errors.Is将会检测每个错误是否与目标错误相同,默认情况下,当两个错误相等时,他们相匹配。除此之外,也可以通过实现Is方法来声明和某个目标值是相等的。

    例如,受Upspin 错误包错误包启发的,该包将错误与模板进行比较,仅考虑模板中非零的字段:

    type Error struct {
        Path string
        User string
    }
    
    func (e *Error) Is(target error) bool {
        t, ok := target.(*Error)
        if !ok {
            return false
        }
        return (e.Path == t.Path || t.Path == "") &&
               (e.User == t.User || t.User == "")
    }
    
    if errors.Is(err, &Error{User: "someuser"}) {
        // err's 的用户定义字段是 "someuser".
    }
    
    

    error.As类似。

    错误和包API

    返回错误的包(大多数都是这样),应该描述程序员可能依赖的那些错误的属性。一个设计良好的包还可以避免返回带有不应该依赖的属性的错误。

    最简单的规范是说操作成功或失败,分别返回nil或nonnil错误值。 在许多情况下,不需要进一步的信息。

    如果我们希望函数返回一个可识别的错误条件,例如“item not found”,我们可能需要返回一个包装了标记的错误。

    var ErrNotFound = errors.New("not found")
    
    // FetchItem 返回指定名称的项目
    //
    // 如果指定名称项目不存在,则返回包装了的错误
    func FetchItem(name string) (*Item, error) {
        if itemNotFound(name) {
            return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
        }
        // ...
    }
    

    还有其他现有的提供错误的模式,可以由调用方进行语义检查,例如直接返回哨兵值,特定类型或可以使用谓词函数检查的值。

    在所有类型中,我们都要注意是否需要将细节暴露给用户,正如我们上文讨论过的”是否需要包装“,当你从另一个包中返回错误时,应该将错误转换为不暴露基本错误的形式,除非你承诺返回该特定错误。

    f, err := os.Open(filename)
    if err != nil {
        // 由 os.Open 返回的 *os.PathError 属于内部细节
        // 为了避免将其暴漏给调用方,将其重新包裹为
        // 一个具有相同文字信息的错误。这里使用 %v 格式化谓词,
        // 因为 %w 有内部 *os.PathError 被展开的可能。
        return fmt.Errorf("%v", err)
    }
    
    

    如果将函数定义为返回包装某些标记或类型的错误,请不要直接返回基础错误。

        var ErrPermission = errors.New("permission denied")
    
        // DoSomething 会在用户没有做某些事群的许可时
        // 返回一个包裹  ErrPermission 的错误
        func DoSomething() error {
            if !userHasPermission() {
                // 如果直接返回 ErrPermission,那调用方就可能
                // 依赖确切的返回值,然后写出这样的代码
                //
                //     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
                //
                // 这样的话如果我们以后想要给错误加入其他上下文,
                // 是会出问题的。为了避免这种事情,我们使用错误包裹
                // 哨兵,这样用户每次都必须将其展开:
                //
                //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
                return fmt.Errorf("%w", ErrPermission)
            }
            // ...
        }
    
    
    
  • 相关阅读:
    Chrome调试工具常用功能
    把读取sql的结果写入到excel文件
    Android逆向破解:Android Killer使用
    鸭子类型和猴子补丁
    Scrapy同时启动多个爬虫
    命令注入
    理解RESTful架构
    程序员需要谨记的九大安全编码规则
    10条建议分享:帮助你成为与硅谷工程师一样优秀的程序员
    代码审计:是安全专家都应该掌握的技能
  • 原文地址:https://www.cnblogs.com/Jun10ng/p/12746347.html
Copyright © 2020-2023  润新知