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