LOADING

正在加载

Go基础学习(拾壹)——Error接口和异常处理

壹 介绍

有的时候由于代码执行错误我们可以看见一些错误信息,那就是异常,异常并不是错误,而是因为特殊原因,例如 用户使用错误、网络,通常情况下,异常一旦出现,程序会立刻结束掉,这时我们可以通过这些信息了解到语句在哪里出现错误并可以及时处理,这就是异常处理。

哪有有人问了,系统不是都已经告诉我们错误信息了吗?为啥还有学习?学习怎么看?

看只是其中的一种,我们还可以学会自己捕抓,并及时做出处理,好像还是说不到点子上。举个例子吧!比如说电脑蓝屏(估计很多人都经历过),当蓝屏时,我们就不可以对的电脑执行除关机冲重启以外的操作了,那想想这时我们在写一些很重要的文件或者代码时,突然蓝屏了,那数据不是就丢失了吗?那有没有其他的方法改进呢?比如先保存后在蓝屏,答案是有的,就是我们可以预测可能发生异常的代码块,并进行捕抓,然后进行自己想要的处理。

异常处理的作用:

a.在出现错误时,如何处理该错误
b.保证代码的健壮性和稳定性、容错性

Go 语言中的错误处理与其他语言不太一样,它把错误当成一种值来处理,更强调判断错误、处理错误,而不是一股脑的 catch 捕获异常。Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch捕获异常的方式。

贰 Error 接口

在Go语言中默认所有操作都存在错误,所以几乎所有的函数都会有返回一个error接口类型的错误!

Go 语言中使用一个名为 error 接口来表示错误类型,error 接口只包含一个方法——Error,这个函数需要返回一个描述错误信息的字符串。

type error interface {
    Error() string
}

在Go语言中默认所有操作都存在错误,所以几乎所有的函数都会有返回error接口类型的错误!由于 error 是一个接口类型,默认零值为nil。所以我们通常将调用函数返回的错误与nil进行比较,以此来判断函数是否返回错误。例如你会经常看到类似下面的错误判断代码。

// 几乎所有的函数会返回一个err
_, err := os.Open("./xx.go")
if err != nil {
    fmt.Println("打开文件失败,err:", err) // 打开文件失败,err: open ./xx.go: The system cannot find the file specified.
    return
}

贰 使用error接口返回错误

我们一般会直接调用函数返回的err,然后进行判读,如果不为nil则输出错误信息,然后return空或者直接关闭程序。

package main

import (
    "fmt"
)

func main() {
    n, err := fmt.Println("这是一个测试")
    // 可以看到一般我们会使用err进行判断,如果不为nil则输出错误信息,然后return空或者直接关闭程序
    // 以后再写程序时,尽量加这个判断,捕获错误
    if err != nil {
        fmt.Println("[-] text() Error", err)
        return
    }
    fmt.Println("返回值:", n, "\t错误值:", err)
    // 这里只是举例子,当然以后的程序不会输出两个fmt.Println
}

叁 自定义错误

问题来了?如果我们想要一个自定义的错误呢?而不是函数返回值的错误?那么最简单的方式是使用errors 包提供的New函数创建一个错误。

函数格式如下:

func New(text string) error

它接收一个字符串参数返回包含该字符串的错误。我们可以在函数返回时快速创建一个错误。

package main

import (
    "errors"
    "fmt"
)

func text(id int) (int, error) {
    // 对ID进行判断,如果满足id<=0进入判断语句,并返回一个自定义错误,否则返回nil
    if id <= 0 {
        // 可以看到我们使用errors包提供的New函数自定义一个错误,只需要返回一个字符串
        return id, errors.New("无效的id")
    }
    return id, nil

}

func main() {
    i, err := text(-1)
    if err != nil {
        fmt.Println("[-] text函数 Error:", err)	// [-] text函数 Error: 无效的id
        return
    }
    fmt.Println(i, err)
}

或者用来定义一个错误变量,例如标准库io.EOF错误定义如下。

var err = errors.New("这是一个错误")
fmt.Printf("类型是%T,值是%v", err, err)	// 类型是*errors.errorString,值是这是一个错误

当我们需要传入格式化的错误描述信息时,使用fmt.Errorf是个更好的选择,在Go基础学习(壹)——Go简介及基础:已完成8.1.4小节。

肆 错误结构体类型

此外我们还可以自己定义结构体错误类型,实现error接口,这种类型目前个人还没有用过。。。。

package main

import (
    "fmt"
)
// OpError 自定义结构体类型
type OpError struct {
    Op string
}
// Error OpError 类型实现error接口
func (e *OpError) Error() string {
    return fmt.Sprintf("无权执行%s操作", e.Op)
}
func main() {
    err := OpError{
        Op: "text",
    }
    fmt.Println(err.Error())	// 无权执行text操作
}

伍 recover——捕获异常

在程序运行时难免会报panic错误,导致整个程序挂掉,这就不得不重新启动服务,但是我们可以通过recover函数将其运行下去。recover函数是golang中内置的函数,其作用是让进入宕机流程中的goroutine恢复过来。recover函数仅在延迟函数defer中有效,在正常的执行过程中,调用recover会返回nil并且没有其他任何效果。需要注意的是recover函数只是针对当前函数和以及直接调用的该函数可能产生的panic,它无法处理其调用产生的其它协程的panic

用法:

defer func() {
    if err := recover(); err !=nil {
        fmt.Println(err)
    }
}()

假设一个场景,我们生成三个函数分别是demo1demo2demo3,其中demo2函数运行会报错,使得整个程序中断。

package main

import "fmt"

func demo1() {
    fmt.Println("这是demo1")
}
func demo2() {
    fmt.Println("这是demo2")
    panic("运行demo2函数将退出程序!")
    fmt.Println("这是demo2结束")
}
func demo3() {
    fmt.Println("这是demo3")
}
func main() {
    demo1()
    demo2()
    demo3()
}

d1b9b5872a22480f9bae5cf373cd33a0.png
可以看到当运行到demo2时,程序就会中断报错,无法运行下面的程序了。如果我们加上recover函数呢?

package main

import "fmt"

func demo1() {
    fmt.Println("这是demo1")
}
func demo2() {
    // 预测该函数可能会存在panic
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println("这是demo2")
    panic("运行demo2函数将退出程序!")
    fmt.Println("这是demo2结束")
}
func demo3() {
    fmt.Println("这是demo3")
}
func main() {
    demo1()
    demo2()
    demo3()
}

56c233dff02740dbb4fcd9ab1d640b14.png
当我们在可能存在panic的函数内加上recover函数时,只是中断demo2这个函数,但是不影响其他函数的运行。这是在可能存在panic的函数加上recover函数,那如果我们不知道那里会出现panic,直接在主函数加panic呢?

package main

import "fmt"

func demo1() {
    fmt.Println("这是demo1")
}
func demo2() {
    fmt.Println("这是demo2")
    panic("运行demo2函数将退出程序!")

}
func demo3() {
    fmt.Println("这是demo3")
}
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    demo1()
    demo2()
    demo3()
}

858296cb59d24798af410570c15765cc.png
这时候程序会一直运行到出现panic为止,由于是在主函数使用recover函数,所以是终止main函数,并且main函数内剩余的代码将不会执行,如果有多协程运行的话,其中一个协程出现panic,并且在主函数使用recover函数,那么程序依然会报panic导致整个程序挂掉,正确的做法是在开启协程的函数里使用recover函数,所以recover函数只是针对当前函数和以及直接调用的该函数可能产生的panic,它无法处理其调用产生的其它协程的panic

avatar
小C&天天

修学储能 先博后渊


今日诗句