从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

🏠 » 📄文章 » 内容

 欢迎来访!

Go(Golang) 编程之异常处理 panic、recover

🕗2024-06-15👁️1

Go 中的异常处理简介

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误

panic、recover 参数类型为 interface{},因此可抛出任何类型对象。

 
func panic(v interface{})
 
func recover() interface{}

处理流程:方法体重抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

关于 panic:

  触发运行时错误:panic 用于立即停止当前函数的执行,并开始回溯调用栈直到程序终止或遇到 recover。
  传递错误信息:panic 可以接受任何类型的参数,通常传递字符串或错误接口实例,方便错误信息的传递和处理。
  易错点:随意使用 panic 处理非严重错误是不推荐的,其主要用于处理不可恢复的运行时错误,对于可处理的错误,应通过返回错误值的方式传递给调用者。

关于 recover:

  捕获 panic:recover 只能在 defer 语句中调用,用于捕获当前 goroutine 发生的 panic,如果没有 panic 发生,则返回 nil。
  recover 处理异常后:逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  易错点:recover 只能捕获同一 goroutine 内发生的 panic,对于其他 goroutine 引发的 panic 无能为力。

Go 语言推荐使用错误返回码而非异常机制来处理错误,通过 error 接口返回错误信息,这是一种更灵活且不会破坏程序执行流程的方法。在实际开发中,建议优先使用错误处理机制,谨慎使用 panic 和 recover,以编写出更加稳定和高效的 Go 程序。

一、异常捕获简单测试

1.1 简单的捕获 panic

如下代码,直接触发 panic,在 defer 中通过 recover 捕获,并转换成 string 输出:

package main
 
func main() {
    test()
}
 
func test() {
    defer func() {
        if err := recover(); err != nil {
            println("输出:", err.(string)) // 将 interface{} 转型为具体类型 string
            // 输出: panic error!
        }
    }()
    panic("panic error!")
}

再来个示例,往已关闭的通道中发送数据,会引发异常:

package main
 
import (
    "fmt"
)
 
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err) // 输出:send on closed channel
        }
    }()
 
    var ch chan int = make(chan int, 10)
    close(ch) // 关闭通道
    ch <- 1   // 继续往通道中发送值,就会引发异常
}

1.2 多个异常时 recover 如何捕获?

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

如下代码,只有 defer 中的 panic 会被捕获,另一个异常将会漏掉:

package main
 
import "fmt"
 
func test() {
    defer func() {
        fmt.Println(recover())
    }()
    defer func() {
        panic("defer panic")
    }()
    panic("test panic")
}
 
func main() {
    test()
}
// 输出:
// defer panic

因此,需要再第二个 defer 中针对 test panic 进行处理。


1.3 一个异常在多层 defer 如何捕获?

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。

如下代码,在第一层进行了延迟调用,然后第〇层就未获取到 panic:

package main
 
import "fmt"
 
func test() {
    defer func() {
        fmt.Println("第〇层:", recover()) // 无效
    }()
    defer func() {
        fmt.Println("第一层:", recover()) // 有效
    }()
    defer fmt.Println("第二层:", recover()) // 无效!
    defer fmt.Println("第三层:", recover()) // 无效!
    defer func() {
        func() {
            println("第四层:defer inner")
            fmt.Println("第四层:", recover()) // 无效!
        }()
    }()
    panic("test panic")
}
 
func main() {
    test()
}
// 输出:
// 第四层:defer inner
// 第四层: <nil>
// 第三层: <nil>
// 第二层: <nil>
// 第一层: test panic
// 第〇层: <nil>

1.4 通过 error 类型的错误对象来表示函数调用状态

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

 
 
type error interface {
 
    Error() string
 
}
 

标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

package main
 
import (
    "errors"
    "fmt"
)
 
var ErrDivByZero = errors.New("division by zero") // 定义错误类型 ErrDivByZero
 
func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }
    return x / y, nil
}
 
func main() {
    defer func() {
        fmt.Println(recover()) // 捕获 panic,无 panic 就打印 <nil>
    }()
    switch z, err := div(10, 0); err { // div(10, 0) 返回 ErrDivByZero
    case nil:
        println(z)
    case ErrDivByZero: // 触发 panic
        panic(err)
    }
}
 
// 输出:
// division by zero

1.5 通过将代码块重构成匿名函数来实现 try-catch 的效果

将代码块重构成匿名函数,并包含异常处理,如此可确保后续代码被执行

如下代码,当被除数为 0 时会报错,在匿名函数中被捕获并记录,然后不影响正常输出:

package main
 
import "fmt"
 
func test(x, y int) {
    var z int
    func() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("err:", err)
                z = 0
            }
        }()
        z = x / y
    }()
    fmt.Printf("x / y = %d\n", z)
}
 
func main() {
    test(2, 0)
}
// 输出:
// err: runtime error: integer divide by zero
// x / y = 0

1.6 实现类似 try-catch 的异常处理

package main
 
import "fmt"
 
func Try(fun func(), handler func(interface{})) {
    defer func() {
        if err := recover(); err != nil {
            handler(err) // 捕获异常后,执行异常处理逻辑
        }
    }()
    fun() // 直接执行处理逻辑
}
 
func main() {
    Try(
        func() {
            // 程序处理逻辑。。。
            panic("test panic") // 抛出异常
        },
        func(err interface{}) {
            // 异常处理逻辑。。。
            fmt.Println(err)
        },
    )
}