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