乐趣区

关于go:人非圣贤孰能无过Go-lang118入门精炼教程由白丁入鸿儒Go-lang错误处理机制EP11

人非圣贤,孰能无过,有则改之,无则加勉。在编程语言层面,错误处理形式大体上有两大流派,别离是以 Python 为代表的异样捕捉机制(try….catch);以及以 Go lang 为代表的谬误返回机制(return error),前者是自动化流程,模式化的语法隔离失常逻辑和谬误逻辑,而后者,须要将错误处理判断编排在失常逻辑中。尽管模式化语法更容易让人了解,但从系统资源开销角度看,谬误返回机制显著更具劣势。

返回谬误

Go lang 的谬误 (error) 也是一种数据类型,谬误用内置的 error 类型示意,就像其余的数据类型的,比方字符串、整形之类,谬误的具体值能够存储在变量中,从函数中返回:

package main  
  
import "fmt"  
  
func handle() (int, error) {return 1, nil}  
  
func main() {i, err := handle()  
    if err != nil {fmt.Println("报错了")  
        return  
    }  
  
    fmt.Println("逻辑失常")  
    fmt.Println(i)  
}

程序返回:



逻辑失常  
1

这里的逻辑是,如果 handle 函数胜利执行并且返回,那么入口函数就会失常打印返回值 i,假如 handel 函数执行过程中呈现谬误,将返回一个非 nil 谬误。

如果一个函数返回一个谬误,那么实践上,它必定是函数返回的最初一个值,因为在执行阶段中可能会返回失常的值,而谬误地位是未知的,所以,handle 函数返回的值是最初一个值。

go lang 中处理错误的常见形式是将返回的谬误与 nil 进行比拟。nil 值示意没有产生谬误,而非 nil 值示意呈现谬误。在咱们的例子中,咱们查看谬误是否为 nil。如果它不是 nil,咱们会通过 fmt.Println 办法揭示用户并且从主函数返回,完结逻辑。

再来个例子:

package main  
  
import (  
    "fmt"  
    "net/http"  
)  
  
func main() {resp, err := http.Get("123123")  
    if err != nil {fmt.Println(err)  
        return  
    }  
  
    fmt.Println(resp.StatusCode)  
  
}

这回咱们应用规范库包 http 向一个叫做 123123 的网址发动申请,当然了,申请过程中有可能产生一些未知谬误,所以咱们应用 err 变量获取 Get 办法的最初一个返回值,如果 err 不是 nil,那么就阐明申请过程中报错了,这里打印具体谬误,而后从主函数中返回。

程序返回:

Get "123123": unsupported protocol scheme ""

很显著,必定报错了,因为 Go lang 并不知道所谓的 123123 到底是什么网络协议。

具体谬误类型

在 Go lang 中,谬误实质上是一个接口:



type error interface {Error() string  
}

蕴含一个带有 Error 字符串的函数。任何实现这个接口的类型都能够作为一个谬误应用。这个函数能够打印出具体谬误的阐明。

当打印谬误时,fmt.Println 函数在外部调用 Error() 办法来获取谬误的阐明:

Get "123123": unsupported protocol scheme ""

但有的时候,除了零碎级别的谬误阐明,咱们还须要针对谬误进行分类,通过不同的谬误类型的品种来决定上游的解决形式。

既然有了谬误阐明,为什么还须要谬误类型,间接通过阐明判断不就行了?这是因为零碎的谬误阐明可能会随着 go lang 版本的迭代而略有不同,而一个谬误的谬误类型则大概率不会发生变化。

通过对规范库文档的解读:https://pkg.go.dev/net/http#P…,咱们就能够对返回的谬误类型进行判断:

package main  
  
import (  
    "fmt"  
    "net"  
    "net/http"  
)  
  
func main() {resp, err := http.Get("123123")  
    if err, ok := err.(net.Error); ok && err.Timeout() {fmt.Println("超时谬误")  
        fmt.Println(err)  
  
    } else if err != nil {fmt.Println("其余谬误")  
        fmt.Println(err)  
    }  
  
    fmt.Println(resp.StatusCode)  
  
}

程序返回:

其余谬误  
Get "123123": unsupported protocol scheme ""

这里咱们把超时 (Timeout) 和其余谬误辨别开来,别离进入不同的错误处理逻辑。

定制谬误

定制谬误通过规范库 errors 为程序的谬误做个性化定制,假如某个函数的作用是做除法运算,而如果除数为 0,则返回一个谬误:

package main  
  
import (  
    "errors"  
    "fmt"  
)  
  
func test(num1 int, num2 int) (int, error) {  
    if num2 == 0 {return 0, errors.New("除数不能为 0")  
    }  
    return num1 / num2, nil  
}  
  
func main() {res, err := test(2, 1)  
    if err != nil {fmt.Println(err)  
        return  
    }  
    fmt.Println("后果是", res)  
}

程序返回:

后果是 2

但如果参数不非法:

package main  
  
import (  
    "errors"  
    "fmt"  
)  
  
func test(num1 int, num2 int) (int, error) {  
    if num2 == 0 {return 0, errors.New("除数不能为 0")  
    }  
    return num1 / num2, nil  
}  
  
func main() {res, err := test(2, 0)  
    if err != nil {fmt.Println(err)  
        return  
    }  
    fmt.Println("后果是", res)  
}

程序返回:

除数不能为 0 

假如,出于某种原因,咱们对除数有定制化需要,比方不能为 0 或者为 1,但条件变成了多条件,此时须要将除数显性的展现在谬误阐明中,以便更具象化的揭示用户:

package main  
  
import ("fmt")  
  
func test(num1 int, num2 int) (int, error) {if (num2 == 0) || (num2 == 1) {return 0, fmt.Errorf("除数为 %d, 除数不能为 0 或者 1", num2)  
    }  
    return num1 / num2, nil  
}  
  
func main() {res, err := test(2, 1)  
    if err != nil {fmt.Println(err)  
        return  
    }  
    fmt.Println("后果是", res)  
}

程序返回:

除数为 1, 除数不能为 0 或者 1 

这里应用 fmt 包的 Errorf 函数依据一个格局说明器格式化谬误,并返回一个字符串作为值来满足谬误。

此外,还能够应用应用构造体和构造体中的属性提供对于谬误的更多信息:

type testError struct {  
    err string  
    num int  
}

这里定义构造体 testError,外面两个属性,别离是谬误阐明和除数值。

随后,咱们应用一个指针接收器区域谬误来实现谬误接口的 Error() string 办法。这个办法打印出谬误的除数值和谬误阐明:

func (e *testError) Error() string {return fmt.Sprintf("除数 %d:%s", e.num, e.err)  
}

接着通过构造体寻址调用:

func test(num1 int, num2 int) (int, error) {if (num2 == 0) || (num2 == 1) {return 0, &testError{"除数非法", num2}  
    }  
    return num1 / num2, nil  
}

残缺代码:

package main  
  
import ("fmt")  
  
type testError struct {  
    err string  
    num int  
}  
  
func (e *testError) Error() string {return fmt.Sprintf("除数 %d:%s", e.num, e.err)  
}  
  
func test(num1 int, num2 int) (int, error) {if (num2 == 0) || (num2 == 1) {return 0, &testError{"除数非法", num2}  
    }  
    return num1 / num2, nil  
}  
  
func main() {res, err := test(2, 1)  
    if err != nil {fmt.Println(err)  
        return  
    }  
    fmt.Println("后果是", res)  
}

程序返回:

除数 1: 除数非法

通过构造体的定义,谬误阐明更加规整,并且更易于保护。

异样(panic/recover)

异样的概念是,原本不应该呈现问题的中央呈现了问题,某些状况下,当程序产生异样时,无奈持续运行,此时,咱们会应用 panic 来终止程序。当函数产生 panic 时,它会终止运行,在执行完所有的提早函数后,程序返回到该函数的调用方,这样的过程会始终继续上来,直到以后协程的所有函数都返回退出,而后程序会打印出 panic 信息,接着打印出堆栈跟踪,最初程序终止:

package main  
  
import "fmt"  
  
func main() {panic("panic error")  
  
    fmt.Println("上游逻辑")  
  
}

程序返回:

panic: panic error

能够看到,panic 办法执行后,程序上游逻辑并未执行,所以 panic 应用场景是,当上游依赖上游的操作,而上游的问题导致上游机关用尽的时候,应用 panic 抛出异样。

但提早执行是个例外:

package main  
  
import "fmt"  
  
func myTest() {defer fmt.Println("defer myTest")  
    panic("panic myTest")  
}  
func main() {defer fmt.Println("defer main")  
    myTest()}

程序返回:

defer myTest  
defer main  
panic: panic myTest

这里当函数产生 panic 时,它会终止运行,在执行完所有的提早函数后,程序返回到该函数的调用方,这样的过程会始终继续上来,直到以后协程的所有函数都返回退出,而后程序会打印出 panic 信息,接着打印出堆栈跟踪,最初程序终止。

此外,recover 办法能够捕捉异样的异样,从而打印异样信息后,继续执行上游逻辑:

package main  
  
import "fmt"  
  
func outOfArray(x int) {defer func() {// recover() 能够将捕捉到的 panic 信息打印  
        if err := recover(); err != nil {fmt.Println(err)  
        }  
    }()  
    var array [5]int  
    array[x] = 1  
}  
func main() {outOfArray(20)  
  
    fmt.Println("上游逻辑")  
}

程序返回:

runtime error: index out of range [20] with length 5  
上游逻辑

结语

综上,Go lang 的错误处理,属实不太优雅,大多数状况下会有很多反复代码:if err != nil,这在肯定水平上影响了代码的可读性和可维护性,同时容易失落底层谬误类型,且定位谬误时,很难失去谬误链,也就是在肯定水平上妨碍了谬误的追根溯源,但反过来想,谬误原本就是业务的一部分,从业务角度上看,Golang 这种返回谬误的形式更贴合业务逻辑,你能够用多返回值蕴含 error 解决业务异样,用 recover 解决零碎异样。业务异样,能够定义为不会引起零碎解体上游瘫痪的异样;零碎异样能够定义为会引起零碎解体上游瘫痪的异样。所以,归根结底,一套功夫的威力,真的不在于其招式的设计,而在于使用功夫的那个人是否施展这套文治的全副后劲。

退出移动版