Go语言学习四——异常处理

幸运草
幸运草
幸运草
896
文章
3
评论
2020年4月13日18:59:02 评论 201
程序就像汽车,一旦上路,难免会有各种大大小小的突发情况:路上突然闯出一个行人、没油了、零部件故障甚或剐蹭追尾、车毁人亡。现代的汽车已经针对各种情况作出了不同的预案:鸣笛警示行人、将近没油时进行提示、汽车启动时检查核心部件,在最糟糕的情况下,也会弹出安全气囊尽量减少损伤。更智能的无人驾驶技术甚至可以自动减速、主动避让行人。

好的程序语言就像好的汽车,为我们提供了多层次的安全预案与安全保障。早期的C语言并没有直接提供语言层面的异常处理,更多需要程序员自己判断异常情况并返回errorCode,或者使用goto语句跳转到错误处理逻辑进行错误处理;经典的面向对象语言如Java通常提供了更完备的错误处理机制:明确不同的错误类型,并提供语言级别的异常捕获(try-catch-finally)机制,在很大程度上减轻了程序员的负担,提供了程序的健壮性。

作为一门更年轻也非常有个性的编程语言,Go语言借助自己函数多返回值、函数式编程以及灵活的接口等特性,提供了非常优雅的异常处理方式。

一. 异常接口

Go语言内置了基础的error数据类型,定义如下:

type error interface {
    Error() string
}

可以看出,error类型是一个接口,其中声明了一个Error方法,用来获取错误信息。

基于这个接口,我们可以声明自己的error类型,如下:

type MyError struct {
    modle string
    err error
}

func (e *MyError) Error() string {
    return fmt.Sprintf("[%s]%s", e.modle, e.err.Error())
}

基于error扩展的错误类型可以包含更多的错误信息,便于更好的分析、定位问题。

有了error类型之后,我们在遇到错误时,就可以返回error了,由于Go语言中的函数支持返回多个值,因此,通常函数设计时都会连同正常结果与错误信息一起返回,如下:

func readFile(file string) (string, error) {
    byteArray, err := ioutil.ReadFile(file)
    if err != nil { // 读取文件错误
        return "", &MyError{"FILEREAD", err}
    } else { // 读取文件正常
        return string(byteArray), nil
    }
}

二. defer

程序中一些特殊的操作需要在处理完成后执行清理,例如打开文件、网络连接后需要关闭,但是一旦程序在执行过程中出错,这些清理操作很有可能得不到执行,造成文件句柄得不到关闭,网络连接得不到释放的情况,对系统整体的性能及稳定性都有很大影响。为了解决这个问题,Go引入了defer关键字,在作用上有点类似于java中的finally。我们直接看一下上面代码中用到的ioutil.ReadFile方法代码:

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close() // 确保文件被正常关闭
    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {
        if size := fi.Size() + bytes.MinRead; size > n {
            n = size
        }
    }
    return readAll(f, n)
}

函数中在打开文件后声明了defer语句用于关闭文件,则即使函数在后面执行过程中出错,该语句也会被执行。每当声明一个defer语句时,相当于向函数注册一个延迟执行命令,等函数执行完毕或者异常退出时将执行这些指令。

此外,当一个函数中定义了多个defer语句时,函数退出时将按照从后往前的顺序执行这些panic语句。我们再来看一个例子:

func testDefer() {
    defer fmt.Println("testDefer 1")
    var err MyError
    defer fmt.Println("testDefer 2")
    fmt.Println(err.Error())
    defer fmt.Println("testDefer 3")
}

func main() {
    testDefer()
}

我们在testDefer方法中故意写了一个空指针异常,运行结果如下:

testDefer 2
testDefer 1
panic: runtime error: invalid memory address or nil pointer dereference
……

"testDefer 3"没有得到执行,因为在执行到fmt.Println(err.Error())时就以及异常了,只会执行之前已经注册的defer语句。

三. panic

当程序遇到严重的系统问题,需要停止运行时,可以使用系统内置的panic函数来报告错误。这通常可以应用在程序启动时执行初始化的阶段,因为一旦初始化失败,系统应当停止运行,否则错误初始化的程序将可能导致更多未知的错误。

当程序执行panic时,正常的执行流程将立即结束,但是函数中声明的defer语句仍将继续执行,之后将按照调用链条逐层向上执行panic流程。

一个简单的示例:

func print(s interface{}) {
    // 获取s的类型并判断
    switch sType := s.(type) {
    case string:
        fmt.Println(s.(string))
    case int:
        fmt.Println(s.(int))
    default:
        panic(fmt.Sprint("unknown type:", sType))
    }
}

func main() {
    print("hello")
    print(10)
    print(true)
}

运行结果如下:

hello
panic: unknown type:true
10

goroutine 1 [running]:
main.print(0x109d060, 0x1129721)
    /Users/Gyz/peng/workspace/test/src/test/main.go:21 +0x212
main.main()
    /Users/Gyz/peng/workspace/test/src/test/main.go:28 +0x7b

Process finished with exit code 2

四. recover

通常,在程序中遇到错误并执行了panic方法后,程序会在执行完defer语句后退出,但是有时我们并不希望程序退出,而是希望能捕获到异常信息,打印日志或者触发报警,但程序依然继续运行。recover方法就是用于这个目的。recover方法通常放在defer语句中,以保证一定会得到执行,如下:

func print(s interface{}) {
    defer func() {
        if r := recover(); r!= nil {
            fmt.Println("catch exception:", r)
        }
    }()
    switch sType := s.(type) {
    case string:
        fmt.Println(s.(string))
    case int:
        fmt.Println(s.(int))
    default:
        panic(fmt.Sprint("unknown type:", sType))
    }
}

func main() {
    print(true)
    print("hello")
    print(10)
}

我们在print方法的最前面声明了defer函数,并在其中用recover方法捕获异常,这样,即使捕获到异常,也只是打印一条错误信息,不会导致程序退出。执行结果如下:

catch exception: unknown type:true
hello
10

五. 小结

以上,我们列举了Go语言在异常处理方面的一些特性,通过这些简单的测试代码,我们可以大致看出Go语言的异常处理方式:

  • 通过内置的错误类型error来记录已识别的异常信息,并返回给调用函数
  • 通过recover()方法来捕捉未识别的异常信息,并结束异常,使程序继续运行
  • 通过panic()主动结束程序
  • 通过defer语句强制程序在退出前执行必要的清理

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

转载请注明:{{title}}-变化吧
  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 赞助本站
  • 支付宝扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...