Go 中异常处理
主要掌握 一下几个方面:
- 掌握error接口
- 掌握defer延迟
- 掌握panic及recover
error接口
error是指程序中出现不正常的情况,从而导致程序无法正常运行;
go中为错误的类型提供了简单的错误处理机制
go中error的源码:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
可以看到接口中定义了Error方法,我们可以实现这个方法已实现error接口;
error本质上是一个接口类型,其中包含一个Error()方法,错误值可以存储在变量中,通过函数返回。它必须是函数返回的最后一个值。
在go中处理错误的方式通常是以下的形式:
if err!=nil{
fmt.println(err)
}
代码Demo
func Divide(a,b float64)(float64,error){
if b==0{
return -1 ,errors.New("by zero")
}else {
return a/b, nil
}
}
func main() {
divide, err := Divide(10, 0)
if err!=nil{
fmt.Println(err)
}else {
fmt.Println(divide)
}
}
创建error对象
由于error是一个接口,因此只要实现该接口中的方法就可以:
结构体只要实现了Error() string这种格式的方法,就代表实现了该错误接口,返回值为错误的具体描述
在这之前我们先看一下go中提供了哪些关于error的方法:
简单看一下Join()的源码:
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil { //如果传进来的err不是 nil
n++//n自增
}
}
if n == 0 {
return nil
}
e := &joinError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
简单来讲就是err的拼接
func main() {
r, err := errorR(121)
if err!=nil {
fmt.Println(err)
fmt.Printf("err的类型%T\n",err)
}else {
fmt.Println(r)
}
}
func errorR(age int) (string, error) {
if age < 0 || age > 120 {
err := fmt.Errorf("输入的年龄%d不符合统计要求", age)
return "", err
} else {
str := "您输入的年龄" + strconv.FormatInt(int64(age), 10) + "符合要求"
return str, nil
}
}
自定义错误
自定义错误的实现步骤如下。
- • 定义一个结构体,表示自定义错误的类型。
- • 让自定义错误类型实现error接口:Error()string。
- • 定义一个返回error的函数。根据程序实际功能而定。
代码demo
//定义结构体
type errorDefine struct {
reason string
}
//实现Error()方法
func (e errorDefine)Error()string {
return "除数为0,不符合要求/"+e.reason
}
func Divide2(a,b int)(float64,error){
defineerr := errorDefine{reason: "by Zero"}
if b==0{
return -1,defineerr
}else{
return float64(a / b),nil
}
}
func main() {
divide2, err := Divide2(100, 0)
if err!=nil {
fmt.Println(err)
}else {
fmt.Println(divide2)
}
}
defer 关键字
defer 是go中一个关键字,用于延迟一个函数或者方法(或者当前创建的匿名函数)的执行;
defer只能出现在函数或者方法的内部
defer的使用
在函数中可以添加多个defer语句。如果有很多调用defer,当函数执行到最后时,这些defer语句会按照逆序执行(报错的时候也会执行),最后该函数返回。
代码Demo
func DeferA(){
println("这是deferA")
}
func DeferB(){
println("这是deferB")
}
func DeferC(){
println("这是deferC")
}
func main() {
defer DeferA()
DeferB()
defer DeferC()
fmt.Println("打印一些东西")
}
函数(整体)
defer 经常被用于**处理成对的操作,**如 打开-关闭连接,加锁解锁,等
func Divide(a, b float64) (float64, error) {
if b == 0 {
return -1, errors.New("by zero")
} else {
return a / b, nil
}
}
func DeferB() {
println("这是deferB")
}
func DeferC() {
println("这是deferC")
}
func DeferFinish() {
fmt.Println("运算结束")
}
func main() {
divide, err := Divide(1024, 2)
if err != nil {
errors.New("发生了错误,请检查参数")
}else{
fmt.Printf("计算结果是%f\n",divide)
}
defer DeferFinish()
DeferC()
DeferB()
}
运行多次发现,defer 定义的方法DeferFinish 只能保证运行在Divide之后;
函数调用
以上defer是应用在函数内部,但是defer的使用 并不局限于函数,延迟一个方法的调用用也是可以的;
例如下面的例子:
func (f Fruit) FruitInfo() {
fmt.Println(f.name, "--", f.color, "--", f.weight)
}
func main() {
fruit := Fruit{
name: "樱桃",
color: "红",
weight: 1.5,
}
defer fruit.FruitInfo()
fmt.Println("我要吃樱桃")
divide, _ := Divide(1024, 3)
defer DeferFinish()
fmt.Println(divide)
}
函数参数
延迟函数的参数在执行延迟语句时已被执行 而不是在执行实际的函数调用执行 时在执行;
什么意思呢,就是该函数的执行结果只是被延迟显示,换句话讲就是函数的参数已经被传递过来了;
看个例子就知道了;
func addNum(a, b int, flag rune) {
if flag == 1 {
fmt.Printf("延迟执行的函数 参数的值%d --%d ,和为 %d\n", a, b, a+b)
} else {
fmt.Printf("未延迟执行的函数 参数的值%d --%d ,和为 %d\n", a, b, a+b)
}
}
func main() {
a := 10
b := 18
flag := '1'
//延迟调用addNum
defer addNum(a, b, flag)
//此时修改a b 的值
a=100
b=234
flag='0'
//再调用函数,看看被defer的函数的参数是否会受到再次赋值的影响
addNum(a,b,flag)
}
看结果------
被延迟执行的参数 依照就近原则,在被defer定义时就已经确定了;
堆栈
当一个函数有多个延迟调用时,他们会被添加到一个堆栈中,按照LIFO的 顺序执行
例如我们要实现一个字符串的逆序输出:
一般的写法如下:
func ReversStr(str string) {
//转换字节切片
bytes := []byte(str)
for i := len(bytes)-1; i >=0 ; i-- {
fmt.Print(string(bytes[i]))
}
}
func ReversStr2(str string){
//转换字节切片
r := []rune(str)
for i := 0; i < len(r); i++ {
defer fmt.Print(string(r[i]))
}
println()
}
func main() {
ReversStr("hello world")
ReversStr2("hello world")
}
panic和recover机制
go中异常处理机制跟java中不一样,go在设计时,设计者认为将异常于流程控制混在一起会让代码变得混乱;
go中panic()是一个内建的函数,可以中断原有的流程就像java中抛出异常一样;
代码demo
func PanicA() {
fmt.Println("正常打印")
}
func PanicB() {
fmt.Println("正常打印B")
//参数是any 类型的
panic("发生了异常中断了Panic")
}
func PanicC() {
fmt.Println("我想要被正常打印")
}
func main() {
PanicA()
PanicB()//
PanicC()
}
运行结果:
正常打印
正常打印B
panic: 发生了异常中断了Panic
goroutine 1 [running]:
main.PanicB(...)
D:/GolandData/main.go:902
main.main()
D:/GolandData/main.go:917 +0xa8
Process finished with the exit code 2
panic与error
通常情况下我们使用error就可以了,但是当遇到一些不可恢复的错误状态的时候,比如数组下标越界,空指针引用等,这些会引起panic,
不应通过调用panic()函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式
func SliceAA(i int){
var s [10]int
s[i]=100
}
func main() {
PanicA()
PanicC()
SliceAA(11)
}
运行结果:
正常打印
我想要被正常打印
panic: runtime error: index out of range [11] with length 10
goroutine 1 [running]:
main.SliceAA(...)
D:/GolandData/main.go:912
main.main()
D:/GolandData/main.go:918 +0xa5
Process finished with the exit code 2
recover
panic一旦被引发就会导致程序崩溃,那怎么拦截呢?
go为开发者提供了内建函数recover(),该函数可以让goroutine恢复过来并重新获得流程控制权;
recover()让程序恢复,必须在延迟函数中执行,即 recover()只在延迟函数中生效;
在正常的程序运行过程中,调用 recover()会返回nil,并且没有其他任何效果。
如果当前的Goroutine陷入恐慌,调用recover()可以捕获panic()的输入值,使程序恢复正常运行。
recover使用方式
代码Demo
1,自己的panic自己处理,不向上级抛
func PanicA() {
fmt.Println("正常打印")
}
func PanicB() {
//自己内部处理
defer func(){
if msg:=recover();msg!=nil{
fmt.Println("恢复中,获取的recover返回值:",msg)
}
}()
fmt.Println("正常打印B")
for i := 0; i < 10; i++ {
fmt.Println(i)
if i==5 {
panic("有致命panic发生")
}
}
//参数是any 类型的
//panic("发生了异常中断了Panic")
}
func PanicC() {
defer func(){
fmt.Println("延迟执行函数")
msg:=recover()
fmt.Println("recover返回值",msg)
}()
fmt.Println("我想要被正常打印")
panic("发生了panic")
}
func main() {
PanicA()
PanicB()
PanicC()
fmt.Println("over--over")
}
2,发生panic,谁调用谁处理
func PanicD(){
fmt.Println("开始打印")
for i := 0; i < 10; i++ {
fmt.Println("i--",i)
if i==5 {
panic("i=5时发生致命错误")
}
}
}
func main() {
defer func(){
if msg:=recover();msg!=nil{
fmt.Println("恢复程序执行,recover()返回值--",msg)
}
}()
PanicD()
}
注意:这里的恢复执行是指的程序不中断,而不是指可以继续打印5之后的数字;
小结:recover()要在延迟函数中执行,可以专门定义一个延迟函数 来捕获panic 或者使用匿名函数来捕获;
一般情况下我们只需要考虑error,判断err是否为nil 就可以了,很少涉及到panic
代码中尽量少有或者没有panic异常,这是底线呀