【Golang笔记03】error、panic、fatal错误处理学习笔记
Golang笔记:错误处理学习笔记
一、进阶学习
1.1、错误(异常处理)
Go
语言中也有和Java
中的异常处理相关的机制,不过,在Go
里面不叫异常,而是叫做:错误
。错误分为三类,分别是:
error
:部分流程错误,需要程序员进行处理,这种级别的错误,不会导致整个程序停止运行。panic
:严重错误,等程序执行完成之后,会立即退出运行。fatal
:致命错误,整个程序立即停止运行。
在Go
语言中,不存在try..catch
相关的语句,Go
中的错误是通过返回值的形式返回的,例如:在调用一个方法的时候,这个方法可以增加一个返回值,表示是否存在错误信息,然后通过if
条件语句判断,是否能够正常执行后续代码。
上面这种错误的处理方式,虽然简化了try...catch
的使用,但是,引入了if
条件语句,也会导致代码里面出现一大堆的条件判断,所以,从本质上来说,还是没有简化的,只不过是采用另一种方式处理错误而已。
1.1.1、error错误
Go
中有一个error
接口,接口里面只有一个Error()
方法,返回值是一个string
字符串,这个字符串表示错误信息。Go
中创建error
错误,可以通过下面两种方式:
- 第一种方式:使用
errors
包下的New()
函数。 - 第二种方式:使用
fmt
包下的Errorf()
函数。
package mainimport ("errors""fmt"
)// 返回值第二个参数是 error 错误
func div(a int, b int) (int, error) {if b == 0 {// 定义 error 级别的错误返回return -1, errors.New("分母不能为0")// 或者使用下面这种方式// return -1, fmt.Errorf("分母不能为0")}c := a / breturn c, nil
}func main() {i, err := div(1, 0)if err != nil {fmt.Println(err)return}fmt.Println("结果=", i)
}
1.1.2、自定义error错误
Go
中也允许程序开发人员自定义error
级别的错误。自定义错误很简单,只需要对应的类型,实现了Error()
方法,那么这个类型就将被看作是error
接口的实现类,也就属于error
类型的错误了。
自定义error
错误的步骤,如下所示:
- 第一步:自定义一个类型。
- 第二步:实现
Error()
方法,返回string
错误字符串信息。 - 第三步:创建自定义类型对象,作为
error
错误即可。
案例代码,如下所示:
package mainimport ("fmt"
)// 第一步:MyCustomError 自定义错误类型
type MyCustomError struct {code intmsg string
}// 第二步:实现 error 接口的 Error() 方法
func (e MyCustomError) Error() string {// 自定义错误的返回格式return fmt.Sprintf("自定义错误:code=%d, msg=%s", e.code, e.msg)
}func main() {// 第三步:创建自定义错误customError := MyCustomError{code: 1,msg: "分母不能为0",}fmt.Println(customError)
}
1.1.3、Unwrap解包错误
Go
语言中,允许错误嵌套,一个错误嵌套一个错误,从而形成一个错误链表。要想从一个错误中,获取它包含的错误,可以调用errors
中提供的Unwrap()
方法。
Unwrap()
方法:从当前error
错误中,获取其内部包含的error
错误,如果内部不存在error
错误,则返回nil
;如果存在,则返回内部的error
错误。
需要注意的是,Unwrap()
方法获取到的error
错误,有可能也是嵌套的error
错误,如果想要获取指定的error
错误,可以使用递归的方式进行判断。
package mainimport ("errors""fmt"
)// MyCustomError 自定义错误类型
type MyCustomError struct {code intmsg string
}// 实现 error 接口的 Error() 方法
func (e MyCustomError) Error() string {// 自定义错误的返回格式return fmt.Sprintf("自定义错误:code=%d, msg=%s", e.code, e.msg)
}func main() {// 创建自定义错误customError := MyCustomError{code: 1,msg: "分母不能为0",}// 嵌套错误,形成一个错误链err := fmt.Errorf("错误:%w", customError)fmt.Println(err)// 解包错误err = errors.Unwrap(err)fmt.Println(err)// 解包错误err = errors.Unwrap(err)fmt.Println(err)
}
// 执行结果
错误:自定义错误:code=1, msg=分母不能为0
自定义错误:code=1, msg=分母不能为0
<nil>
从上面可以看到,errors.Unwrap()
方法的作用就是从错误链中解包内部错误。
1.1.4、检查错误
Go
语言中,判断某个error
错误是否为指定类型,可以使用errors.Is()
方法,这个方法的作用是:判断当前error
是否为目标target
类型的错误,如果是目标类型的错误,则返回true
,否则返回false
。
errors.Is(err, targer error) bool
方法:判断是否为目标错误类型。
package mainimport ("errors""fmt"
)var originalErr = errors.New("this is an error")// 包裹原始错误
func wrap1() error {return fmt.Errorf("wrapp error %w", wrap2())
}// 原始错误
func wrap2() error {return originalErr
}func main() {err := wrap1()// 如果使用if err == originalErr 将会是falseif errors.Is(err, originalErr) {fmt.Println("original")}
}
另外,Go
中还提供了一个errors.As(err, target Any) bool
方法,这个方法的作用是:在当前错误链中寻找第一个匹配target
的错误类型,并且将找到的错误赋值给传入的err
变量。
errors.As(err, target Any) bool
方法:用于检查一个错误是否可以转换成目标,如果可以就会将目标错误赋值给第一个参数err
。
package mainimport ("errors""fmt""time"
)// TimeError 自定义error
type TimeError struct {Msg string// 记录发生错误的时间Time time.Time
}// 实现 error 接口方法
func (m TimeError) Error() string {return m.Msg
}// NewMyError 定义自定义错误函数,并且使用 & 符号,返回一个 结构体指针 类型
func NewMyError(msg string) error {// 使用 & 符号,返回一个 结构体指针 类型,即:返回内存地址return &TimeError{Msg: msg,Time: time.Now(),}
}// 包裹原始错误
func wrap1() error {return fmt.Errorf("wrapp error %w", wrap2())
}// 原始错误
func wrap2() error {return NewMyError("original error")
}func main() {// 定义一个 结构体指针 变量var myerr *TimeError// 获取错误err := wrap1()// 检查错误链中是否有 *TimeError 类型的错误if errors.As(err, &myerr) { // 输出TimeError的时间fmt.Println("original", myerr.Time)}
}
1.2、panic
前面学的error
错误,是不严重的错误类型,如果不在后面写return
语句,那么程序还是会继续往后执行下去的。Go
语言中还提供了一个panic
错误。
panic
类型是一种比error
更加严重的错误类型,当出现panic
之后,后续的代码时不会继续执行
,即使没有使用return
关键字,代码也不会执行panic
后面的代码。
注意:
panic
你可以理解成是Java
语言中的使用throw new
抛出异常的模式。
1.2.1、创建panic
Go
中提供了一个panic
函数,通过这个函数可以创建panic
错误。
func panic(v any)
我对panic
的理解是,panic
就相当于是简写了Java
语言中的throw new Exception("错误信息")
的语句。例如:
// 在Java语言中,抛出异常
throw new Exception("错误异常信息")// 而在Go里面,只需要写一个 panic 函数即可
panic("错误异常信息")
使用panic
的时候,需要注意的是,panic
后面的代码是不会执行的。
1.2.2、panic善后
当发生panic
错误之前,如果存在defer
延迟函数,那么程序首先会依次执行defer
延迟函数,执行完成之后,才会触发panic
。
package mainimport "fmt"func mockPanic() {fmt.Println("开始执行panic...")panic("模拟panic错误...")fmt.Println("panic执行结束...")
}
func demo01() {fmt.Println("执行demo01()函数...")
}
func demo02() {fmt.Println("执行demo02()函数...")
}
func demo03() {fmt.Println("执行demo03()函数...")
}func main() {fmt.Println("Hello")defer demo01()defer demo02()mockPanic()defer demo03()fmt.Println("World")
}// 程序运行结果
Hello
开始执行panic...
执行demo02()函数...
执行demo01()函数...
panic: 模拟panic错误...goroutine 1 [running]:
main.mockPanic()D:/environment/GoWorks/src/go-study/Hello.go:7 +0x59
main.main()D:/environment/GoWorks/src/go-study/Hello.go:24 +0x7d
当发生panic
时,会立即停止当前函数的执行,然后执行善后的一些操作,例如:执行defer
延迟函数;执行完成之后,会将panic
向上层函数传递,上层函数也会进行相应的善后操作,直到main
函数终止运行。
1.2.3、恢复执行
当使用panic
之后,程序会终止运行,如果我们想恢复程序的执行,那么可以使用recover()
内置函数来实现。需要注意的是,recover()
函数必须在defer
延迟函数里面使用。另外,recover()
函数的返回值是panic
函数中的错误信息内容。
package mainimport ("fmt"
)func demo() {fmt.Println("C")defer func() {fmt.Println("D")// 恢复程序执行,就相当于是取消panicok := recover()if ok != nil {fmt.Println("恢复程序执行")}fmt.Println("E")}()// 发生 panic 则 demo 方法终止执行panic("模拟panic错误...")fmt.Println("F")
}func main() {fmt.Println("A")demo()fmt.Println("B")
}// 执行结果
A
C
D
恢复程序执行
E
B
从上面代码中,可以发现,正常情况下,发生panic
之后,如果没有使用recover()
函数恢复执行,那么最终的输出结果将只有下面这些内容:
A
C
D
E
panic: 模拟panic错误...goroutine 1 [running]:
main.demo()D:/environment/GoWorks/src/go-study/Hello.go:19 +0x76
main.main()D:/environment/GoWorks/src/go-study/Hello.go:25 +0x4f
但是,我们在demo()
函数中,定义了一个defer
延迟函数,并且在里面使用recover()
函数恢复程序的执行,也就相当于是捕获处理了panic
,那么demo()
函数中的panic就不会向上传递到main
函数里面,所以main
函数中的代码将正常执行。
理解这一点很重要,因为如果把上面代码改成下面执行顺序,如下所示:
package mainimport ("fmt"
)func demo() {fmt.Println("C")// 发生 panic 则 demo 方法终止执行panic("模拟panic错误...")fmt.Println("F")
}func main() {fmt.Println("A")// 在main函数里面定义deferdefer func() {fmt.Println("D")// 恢复程序执行,就相当于是取消panicok := recover()if ok != nil {fmt.Println("恢复程序执行")}fmt.Println("E")}()demo()fmt.Println("B")
}// 执行结果
A
C
D
恢复程序执行
E
上面代码中,从输出结果来看,程序没有执行demo()
函数之后的代码,这是为什么呢???
- 我是这么理解的,当
demo()
函数中,发生panic
之后,由于demo()
函数没有进行处理,所以会向上传递到main()
函数里面。 main()
函数发现,此时发生了panic
,所以就不会执行后面的代码。- 此时触发
panic
的善后相关代码,例如:执行defer
函数。 - 首先,会从
demo()
函数中,依次开始执行defer
函数,然后向上传递到main
函数里面,开始执行main
函数中的defer
函数。 defer
函数执行完成之后,此时整个程序就结束运行了。- 所以,最终
demo()
函数之后的代码,也就不会再执行了。
下面举个例子,来看看发生panic之后,程序的输出结果分别是多少,如下所示:
package mainimport ("fmt"
)func demo() {fmt.Println("B1")defer fmt.Println("C")defer func() {fmt.Println("E1")// 恢复程序执行,就相当于是取消panicok := recover()if ok != nil {fmt.Println("恢复程序执行")}fmt.Println("E2")}()demo02()fmt.Println("B2")
}func demo02() {fmt.Println("D1")panic("demo02函数发生panic...")fmt.Println("D2")
}func main() {fmt.Println("A1")demo()fmt.Println("A2")
}// 输出结果
// A1 B1 D1 E1 恢复... E2 C A2
看到这里,你学会了吗???
使用recover()
函数,有四个注意事项:
- 必须在
defer
函数中使用recover()
函数。 - 多次使用
recover()
函数,只会恢复一个panic
。 - 闭包结构中使用
recover()
函数,不能恢复外部函数的panic
。 panic
参数禁止使用nil
。
可以将recover()
函数理解成是Java
中的try...catch
的功能,也就是捕获panic
错误,然后能够让程序继续正常执行。
1.3、fatal
fatal
是Go
语言中的致命错误,这种错误一旦发生,那么整个程序就会立即停止运行,fatal
是没有办法进行善后操作的,也就是说,发生fatal
之后,程序都来不及执行defer
函数。
package mainimport ("fmt""os"
)func main() {fmt.Println("A1")// 模拟 fatal 错误os.Exit(1)fmt.Println("A2")// A1 B1 D1 E1 恢复 E2 C A2
}
Go
语言中一般不会显示的声明fatal
错误,发生fatal
错误一般都是程序自主发生的,不是人为干预的。
注意:
Go
语言中一般使用os.Exit(1)
代码来实现fatal
错误。
以上,就是Go
语言中错误处理相关的知识点。