前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >18.Go语言-错误与异常

18.Go语言-错误与异常

原创
作者头像
面向加薪学习
发布2022-09-04 11:19:02
3400
发布2022-09-04 11:19:02
举报
文章被收录于专栏:面向加薪学习面向加薪学习

第 18 章 错误与异常

18.1 错误

18.1.1 内建错误

在 Go 中, 错误 使用内建的 error 类型表示。error 类型是一个接口类型,它的定义如下:

代码语言:go
复制
type error interface {
    Error() string
}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

下面的例子演示了程序尝试打开一个不存在的文件导致的报错:

代码语言:go
复制
package main

import (
    "fmt"
    "os"
)

func main() {
    // 尝试打开文件
    file, err := os.Open("/a.txt")
    // 如果打开文件时发生错误 返回一个不等于 nil 的错误
    if err != nil {
        fmt.Println(err)
        return
    }
    // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
    fmt.Println(file.Name(), "opened successfully")
}

我们这里没有存在一个文件 a.txt ,所以尝试打开文件将会返回一个不等于 nil 的错误。

代码语言:go
复制
open /a.txt: The system cannot find the file specified.

18.1.2 自定义错误

使用 errors 包中的 New 函数可以创建自定义错误。下面是 errors 包中 New 函数的实现代码:

代码语言:go
复制
package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

errorString 是一个结构体类型,只有一个字符串字段 s 。它使用了 errorString 指针接受者,来实现 error 接口的 Error() string 方法。New 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。

下面是一个简单的自定义错误例子,该例子创建了一个计算矩形面积的函数,当矩形的长和宽两者有一个为负数时,就会返回一个错误:

代码语言:go
复制
package main

import (
    "errors"
    "fmt"
)

func area(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, errors.New("计算错误, 长度或宽度,不能小于0.")
    }
    return a * b, nil
}
func main() {
    a := 100
    b := -10
    r, err := area(a, b)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", r)
}

运行上面的程序会报出自定义的错误:

代码语言:go
复制
计算错误, 长度或宽度,不能小于0.

18.1.3 给错误添加更多信息

上面的程序能报出我们自定义的错误,但是没有具体说明是哪个数据出了问题,所以下面就来改进一下这个程序,我们使用 fmt 包中的 Errorf 函数,规定错误格式,并返回一个符合该错误的字符串。

代码语言:go
复制
package main

import (
    "fmt"
)

func area(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, fmt.Errorf("计算错误, 长度%d或宽度%d,不能小于0", a, b)
    }
    return a * b, nil
}
func main() {
    a := 100
    b := -10
    area, err := area(a, b)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", area)
}

运行上面的程序,我们可以看到输出的错误中打印了长度和宽度的具体值:

代码语言:go
复制
计算错误, 长度100或宽度-10,不能小于0

当然,给错误添加更多信息还可以 使用结构体类型和字段 实现。下面还是通过改进上面的程序来讲解这种方法的实现:

首先创建一个表示错误的结构体类型,一般错误类型名称都是以 Error 结尾,上面的错误是由于面积计算中长度或宽度错误导致的,所以这里把结构体命名为 areaError

代码语言:go
复制
package main

import (
    "fmt"
)

type areaError struct {
    // 错误信息
    err string
    // 错误有关的长度
    length int
    // 错误有关的宽度
    width int
}

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
    // 打印长度和宽度以及错误的描述
    return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
}

func rectangleArea(a, b int) (int, error) {
    if a < 0 || b < 0 {
        return 0, &areaError{"length or width is negative", a, b}
    }
    return a * b, nil
}
func main() {
    a := 100
    b := -10
    area, err := rectangleArea(a, b)
    // 检查了错误是否为 nil
    if err != nil {
        // 断言 *areaError 类型
        if err, ok := err.(*areaError); ok {
            // 如果错误是 *areaError 类型
            // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
            fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", area)
}

运行该程序输出如下:

代码语言:go
复制
length 100 or width -10 is less than zero

当然,我们还可以使用 结构体类型的方法 来给错误添加更多信息。下面我们继续完善上面的程序,让程序更加精确的定位是长度引发的错误还是宽度引发的错误。

首先,我们还是跟上面一样创建一个表示错误的结构体:

代码语言:go
复制
package main

import (
    "fmt"
)

type areaError struct {
    // 错误信息
    err string
    // 长度
    length int
    // 宽度
    width int
}

// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
func (e *areaError) Error() string {
    return e.err
}

// 长度为负数返回 true
func (e *areaError) lengthNegative() bool {
    return e.length < 0
}

// 宽度为负数返回 true
func (e *areaError) widthNegative() bool {
    return e.width < 0
}

func area(length, width int) (int, error) {
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += " and width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

func main() {
    length := 100
    width := -10
    area, err := area(length, width)
    // 检查了错误是否为 nil
    if err != nil {
        // 断言 *areaError 类型
        if err, ok := err.(*areaError); ok {
            // 如果错误是 *areaError 类型
            // 如果长度为负数 打印错误长度具体值
            if err.lengthNegative() {
                fmt.Printf("error: 长度 %d 小于0\n", err.length)
            }
            // 如果宽度为负数 打印错误宽度具体值
            if err.widthNegative() {
                fmt.Printf("error: 宽度 %d 小于0\n", err.width)
            }
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Println("Area =", area)
}

还是使用之前的例子中的参数,但我们这次报错结果更加具体,运行该程序输出如下:

代码语言:go
复制
error: width -10 is less than zero

18.2 异常

错误和异常是两个不同的概念,非常容易混淆。错误指的是可能出现问题的地方出现了问题;而异常指的是不应该出现问题的地方出现了问题。

18.2.1 panic

在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

我们应该尽可能地使用错误,而不是使用 panicrecover 。只有当程序不能继续运行的时候,才应该使用 panicrecover 机制。

panic 有两个合理的用例:

  • 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic ,因为如果不能绑定端口,啥也做不了。
  • 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic ,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

18.2.2 触发 panic

下面是内建函数 panic 的签名:

代码语言:go
复制
func panic(v interface{})

当程序终止时,会打印传入 panic 的参数。

代码语言:go
复制
package main

func main() {
    panic("panic error")
}

运行上面的程序,会打印出传入 panic 函数的信息,并打印出堆栈跟踪:

代码语言:go
复制
panic: panic error

18.2.3 发生 panic 时的 defer

上面已经提到了,当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。下面通过一个简单的例子看看是不是这样:

代码语言:go
复制
package main

import "fmt"

func myTest() {
    defer fmt.Println("defer myTest")
    panic("panic myTest")
}
func main() {
    defer fmt.Println("defer main")
    myTest()
}

运行该程序后输出如下:

代码语言:go
复制
defer myTest
defer main
panic: panic myTest

18.2.4 recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。下面是内建函数 recover 的签名:

代码语言:go
复制
func recover() interface{}

recover 必须在 defer 函数中才能生效,在其他作用域下,它是不工作的。在延迟函数内调用 recover ,可以取到 panic 的错误信息,并且停止 panic 续发事件,程序运行恢复正常。下面是网上找的一个例子:

代码语言:go
复制
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() {
    // 故意制造数组越界 触发 panic
    outOfArray(20)
    // 如果能执行到这句 说明 panic 被捕获了
    // 后续的程序能继续运行
    fmt.Println("main...")
}

虽然该程序触发了 panic ,但由于我们使用了 recover() 捕获了 panic 异常,并输出 panic 信息,即使 panic 会导致整个程序退出,但在退出前,有 defer 延迟函数,还是得执行完 defer 。然后程序还会继续执行下去:

代码语言:go
复制
runtime error: index out of range [20] with length 5
main...

这里要注意一点,只有在相同的协程中调用 recover 才管用, recover 不能恢复一个不同协程的 panic

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第 18 章 错误与异常
    • 18.1 错误
      • 18.1.1 内建错误
      • 18.1.2 自定义错误
      • 18.1.3 给错误添加更多信息
    • 18.2 异常
      • 18.2.1 panic
      • 18.2.2 触发 panic
      • 18.2.3 发生 panic 时的 defer
      • 18.2.4 recover
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档