人非圣贤,孰能无过,有则改之,无则加勉。在编程语言层面,错误处理形式大体上有两大流派,别离是以 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 解决零碎异样。业务异样,能够定义为不会引起零碎解体上游瘫痪的异样;零碎异样能够定义为会引起零碎解体上游瘫痪的异样。所以,归根结底,一套功夫的威力,真的不在于其招式的设计,而在于使用功夫的那个人是否施展这套文治的全副后劲。