Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error 类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息,使用 errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
}
在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了 non-nil 的error对象,将此对象与nil比
较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误。
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
综合案例:
package main
import (
"fmt"
)
// 定义一个DivideError结构
type DivideError struct {
dividee int
divider int
}
// 实现error接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义int类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
// 100/10 = 10
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
# 程序输出
100/10 = 10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
等价于:
result, errorMsg := Divide(100,10)
if errorMsg == ""{
fmt.Println("100/10 = ", result)
}
result, errorMsg = Divide(100,0)
if errorMsg != ""{
fmt.Println("errorMsg is: ", errorMsg)
}
这里介绍一下 panic 与 recover,一个用于主动抛出错误,一个用于捕获 panic 抛出的错误。
panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,panic 用于主动抛出错
误,recover 用来捕获 panic 抛出的错误。
引发 panic 有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。
发生 panic 后,程序会从调用 panic 的函数位置或发生 panic 的地方立即返回,逐层向上执行函数的 defer
语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数。
panic 不但可以在函数正常流程中抛出,在 defer 逻辑里也可以再次调用 panic 或抛出 panic。defer 里面
的 panic 能够被后续执行的 defer 捕获。
recover 用来捕获 panic,阻止 panic 继续向上传递。recover() 和 defer 一起使用,但是 defer 只有在后
面的函数体内直接被调用才能捕获 panic 来终止异常,否则返回 nil,异常继续向外传递。
以下捕获失败:
defer recover()
defer fmt.Prinntln(recover)
defer func(){
func(){
// 无效,嵌套两层
recover()
}()
}()
以下捕获有效:
defer func(){
recover()
}()
func except(){
recover()
}
func test(){
defer except()
panic("runtime error")
}
多个 panic 只会捕捉最后一个:
package main
import "fmt"
func main() {
// 结果:three
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
fmt.Println("three")
panic("three")
}()
defer func() {
fmt.Println("two")
panic("two")
}()
fmt.Println("one")
panic("one")
}
# 程序输出
one
two
three
three
使用场景,一般情况下有两种情况用到:
fmt.Println 打印结构体的时候,会把其中的 error 的返回的信息打印出来。
package main
import "fmt"
type User struct {
username string
password string
}
func (p *User) init(username string, password string) (*User, string) {
if "" == username || "" == password {
return p, p.Error()
}
p.username = username
p.password = password
return p, ""
}
func (p *User) Error() string {
return "Usernam or password shouldn't be empty!"
}
func main() {
var user User
user1, _ := user.init("", "")
fmt.Println(user1)
}
# 程序输出
Usernam or password shouldn't be empty!
总结几点 panic,defer 和 recover:
1、panic 在没有用 recover 前以及在 recover 捕获那一级函数栈,panic 之后的代码均不会执行;一旦被
recover 捕获后,外层的函数栈代码恢复正常,所有代码均会得到执行;
2、panic 后,不再执行后面的代码,立即按照逆序执行 defer,并逐级往外层函数栈扩散;defer 就类似
finally;
3、利用 recover 捕获 panic 时,defer 需要在 panic 之前声明,否则由于 panic 之后的代码得不到执行,因
此也无法 recover;
package main
import (
"fmt"
)
func main() {
fmt.Println("外层开始")
defer func() {
fmt.Println("外层准备recover")
if err := recover(); err != nil {
// err已经在上一级的函数中捕获了,这里没有异常,只是例行先执行defer,然后执行后面的代码
fmt.Printf("%#v-%#v\n", "外层", err)
} else {
fmt.Println("外层没做啥事")
}
fmt.Println("外层完成recover")
}()
fmt.Println("外层即将异常")
f()
fmt.Println("外层异常后")
defer func() {
fmt.Println("外层异常后defer")
}()
}
func f() {
fmt.Println("内层开始")
defer func() {
fmt.Println("内层recover前的defer")
}()
defer func() {
fmt.Println("内层准备recover")
if err := recover(); err != nil {
// 这里err就是panic传入的内容
fmt.Printf("%#v-%#v\n", "内层", err)
}
fmt.Println("内层完成recover")
}()
defer func() {
fmt.Println("内层异常前recover后的defer")
}()
panic("异常信息")
defer func() {
fmt.Println("内层异常后的defer")
}()
//recover捕获的一级或者完全不捕获这里开始下面代码不会再执行
fmt.Println("内层异常后语句")
}
# 程序输出
外层开始
外层即将异常
内层开始
内层异常前recover后的defer
内层准备recover
"内层"-"异常信息"
内层完成recover
内层recover前的defer
外层异常后
外层异常后defer
外层准备recover
外层没做啥事
外层完成recover
package main
import (
"fmt"
)
// 自定义错误信息结构
type DIV_ERR struct {
etype int // 错误类型
v1 int // 记录下出错时的除数、被除数
v2 int
}
// 实现接口方法 error.Error()
func (div_err DIV_ERR) Error() string {
if 0 == div_err.etype {
return "除零错误"
} else {
return "其他未知错误"
}
}
// 除法
func div(a int, b int) (int, *DIV_ERR) {
if b == 0 {
// 返回错误信息
return 0, &DIV_ERR{0, a, b}
} else {
// 返回正确的商
return a / b, nil
}
}
func main() {
// 正确调用
// (1)succeed: 50
v, r := div(100, 2)
if nil != r {
fmt.Println("(1)fail:", r)
} else {
fmt.Println("(1)succeed:", v)
}
// 错误调用
// (2)fail: 除零错误
// 因为重写了Error()方法,所以输出结构体的时候会自动输出错误信息
v, r = div(100, 0)
if nil != r {
fmt.Println("(2)fail:", r)
} else {
fmt.Println("(2)succeed:", v)
}
}
# 程序输出
(1)succeed: 50
(2)fail: 除零错误
Go函数里提供了 defer 关键字,可以注册多个延迟调用,这些调用以先进后出(FILO)的顺序在函数返回前被执行。
这有点类似于Java 语言中异常处理中的 finaly 子句,defer 常用于保证一些资源最终一定能够得到回收和释放。
package main
func main() {
//先进后出
defer func() {
println("first")
}()
defer func() {
println("second")
}()
println("function body")
}
# 程序输出
function body
second
first
defer 后面必须是函数或方法的调用,不能是语句,否则会报如下错误:
expression in defer must be function call
defer 函数的实参在注册时通过值拷贝传递进去。下面示例代码中,实参 a 的值在 defer 注册时通过值拷贝传递
进去,后续语句 a++ 并不会影响 defer 语句最后的输出结果。
package main
import "fmt"
func f() int {
a := 0
defer func(i int) {
println("defer i=", i)
}(a)
a++
return a
}
func main() {
result := f()
fmt.Println(result)
}
# 程序输出
1
defer i= 0
package main
import "fmt"
func test() int {
i := 0
defer func() {
fmt.Println("defer1")
}()
defer func() {
i += 1
fmt.Println("defer2")
}()
return i
}
func main() {
fmt.Println("return", test())
}
# 程序输出
defer2
defer1
return 0
返回值并没有被修改,这是由于 Go 的返回机制决定的,执行 return 语句后,Go 会创建一个临时变量保存返回
值,因此,defer 语句修改了局部变量 a,并没有修改返回值。
对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 a,即对返
回值产生了影响。
package main
import "fmt"
func test() (i int) {
i = 0
defer func() {
i += 1
fmt.Println("defer2")
}()
return i
}
func main() {
fmt.Println("return", test())
}
# 程序输出
defer2
return 1
defer 语句必须先注册后才能执行,如果 defer 位于 return 之后,则 defer 因为没有注册,不会执行。
package main
func main() {
defer func() {
println("first")
}()
a := 0
println(a)
return
defer func() {
println("second")
}()
}
# 程序输出
0
first
package main
func main() {
defer func() {
println("first")
}()
a := 0
println(a)
defer func() {
println("second")
}()
}
# 程序输出
0
second
first
主动调用 os.Exit(int) 退出进程时, defer 将不再被执行(即使 defer 经提前注册)。
package main
import "os"
func main() {
defer func() {
println("defer")
}()
println("func body")
os.Exit(1)
}
# 程序输出
func body
defer 的好处是可以在一定程度上避免资源泄漏,特别是在有很多 return 语句,有多个资源需要关闭的场景中,
很容易漏掉资源的关闭操作。
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
fmt.Printf("%v",err)
return
}
defer file.Close()
}
package main
import (
"io"
"os"
)
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return
}
defer dstFile.Close()
w, err = io.Copy(srcFile, dstFile)
return
}
在打开资源无报错后直接调用 defer 关闭资源,一旦养成这样的编程习惯,则很难会忘记资源的释放。
defer 语句的位置不当,有可能导致 panic,一般 defer 语句放在错误检查语句之后。
defer 也有明显的副作用:defer 会推迟资源的释放,defer 尽量不要放到循环语句里面,将大函数内部的
defer 语句单独拆分成一个小函数是一种很好的实践方式。另外,defer 相对于普通的函数调用需要间接的数据
结构的支持,相对于普通函数调用有一定的性能损耗。
defer 中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果。
本节主要介绍 panic 和 recover 两个内置函数,这两个内置函数用来处理Go的运行时错误。
panic 用来主动抛出错误,recover 用来捕获 panic 抛出的错误。
panic 和 recover 的函数签名如下:
panic(i interface{})
revover() interface{}
包中 init 函数引发的 panic 只能在 init 函数中捕获,在 main 中无法被捕获,原因是 init 函数先于 main 执
行。函数并不能捕获内部新启动的goroutine 所抛出的 panic。
package main
import (
"fmt"
"time"
)
func do() {
// do函数里无法捕捉go da()抛出的异常
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err)
}
}()
go da()
go db()
time.Sleep(3 * time.Second)
}
func da() {
panic("panic da")
for i := 0; i < 10; i++ {
fmt.Println("da", i)
}
}
func db() {
for i := 0; i < 10; i++ {
fmt.Println("db", i)
}
}
func main() {
do()
}
# 程序输出
db 0
db 1
db 2
db 3
db 4
db 5
db 6
panic: panic da
goroutine 19 [running]:
main.da()
什么情况下主动调用 panic 函数抛出 panic?
一有两种情况:
(1)、程序遇到了无法正常执行下去的错误,主动调用 panic 函数结束程序运行。
(2)、在调试程序时,通过主动调用 panic 实现快速退出,panic 打印出的堆栈能够更快地定位错误。
为了保证程序的健壮性,需要主动在程序的分支流程上使用 recover() 拦截运行时错误。
Go 提供了两种处理错误的方式,一种是借助 panic 和 recover 的抛出捕获机制,另一种是使用 error 错误类
型。
// 不捕获异常
package main
import (
"errors"
"fmt"
)
func main() {
panic(errors.New("异常"))
fmt.Println("执行")
}
# 程序输出
panic: 异常
goroutine 1 [running]:
main.main()
// 捕获异常
package main
import (
"errors"
"fmt"
)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常", err)
}
}()
panic(errors.New("异常"))
fmt.Println("执行")
}
# 程序输出
捕获异常 异常
上面程序虽然捕获了异常,但是后续的程序确因为异常而终止,没有执行相关业务,下面进行处理。
package main
import (
"errors"
"fmt"
)
func main() {
func() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常", err)
}
}()
panic(errors.New("异常"))
}()
fmt.Println("执行")
}
# 程序输出
捕获异常 异常
执行
开启线程捕获异常:
package main
import (
"errors"
"fmt"
)
func main() {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常", err)
}
}()
panic(errors.New("异常"))
}()
fmt.Println("执行")
}
# 输出
执行
捕获异常 异常
进行封装:
package main
import (
"errors"
"fmt"
)
func try(tryFunc func(), catchFunc func(interface{})) {
defer func() {
if err := recover(); err != nil {
catchFunc(err)
}
}()
tryFunc()
}
func main() {
try(func() {
panic(errors.New("异常"))
}, func(err interface{}) {
fmt.Println("捕获异常", err)
})
fmt.Println("执行")
}
# 程序输出
捕获异常 异常
执行
本节讨论defer带来的副作用:第一个副作用是对返回值的影响,第二个副作用是对性能的影响。
defer中如果引用了函数的返回值,则因引用形式不同会导致不同的结果,这些结果往往给初学者造成很大的困
惑,我们先来看一下如下三个函数的执行结果:
package main
func f1() (r int) {
defer func() {
r++
}()
return 0
}
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
func main() {
// f1= 1
println("f1=", f1())
// f2= 5
println("f2=", f2())
// f3= 1
println("f3=", f3())
}
f1、f2、f3 三个函数的共同点就是它们都是带命名返回值的函数,返回值都是变量。
(1)、函数调用方负责开辟栈空间,包括形参和返回值的空间。
(2)、有名的函数返回值相当于函数的局部变量,被初始化为类型的零值。
综上所述,对于带 defer 的函数返回整体上有三个步骤:
(1)、执行 return 的值拷贝,将 return 语句返回的值复制到函数返回值栈区(如果只有一个return ,不带任何变
量或值,则此步骤不做任何动作)。
(2)、执行 defer 语句,多个 defer 按照 FILO 顺序执行。
(3)、执行调整阻RET指令。
package main
func f4() int {
r := 0
defer func() {
r++
}()
return r
}
func f5() int {
r := 0
defer func(i int) {
i++
}(r)
return 0
}
func main() {
// f4= 0
println("f4=", f4())
// f5= 0
println("f5=", f5())
}
不管 defer 如何操作,都不会改变函数的 return 的值,这是一种好的编程模式。
go中定义异常有两种方式,一种是errors.New()、另一种是定义一个结构体实现Error方法。
News函数的源码:
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
其实也是定义了一个结构体实现了Error方法。
package main
import (
"errors"
"fmt"
)
func main() {
result1, err := Divide(10, 2)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result1)
}
result2, err := Divide(10, 0)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result2)
}
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("The dividend cannot be 0 ")
} else {
return a / b, nil
}
}
# 程序输出
5
The dividend cannot be 0
package main
import (
"fmt"
)
func main() {
result1, err := Divide(10, 2)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result1)
}
result2, err := Divide(10, 0)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result2)
}
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("Divide failed, Dividend id %d, Divisor is %d, The dividend cannot be 0", a, b)
} else {
return a / b, nil
}
}
# 程序输出
5
Divide failed, Dividend id 10, Divisor is 0, The dividend cannot be 0
package main
import (
"fmt"
)
type DivideError struct {
ErrStr string
Dividend int
Divisor int
}
// 实现error接口
func (de *DivideError) Error() string {
return fmt.Sprintf("%s, Dividend id %d, Divisor is %d", de.ErrStr, de.Dividend, de.Divisor)
}
func main() {
result1, err := Divide(10, 2)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result1)
}
result2, err := Divide(10, 0)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result2)
}
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideError{"Divide failed, The dividend cannot be 0", a, b}
} else {
return a / b, nil
}
}
# 程序输出
5
Divide failed, The dividend cannot be 0, Dividend id 10, Divisor is 0
我们可以在 DivideError 结构体中添加更多的字段,返回更多的信息。
package main
import (
"fmt"
)
type DivideError struct {
ErrStr string
Dividend int
Divisor int
Negative bool
}
// 实现error接口
func (de *DivideError) Error() string {
return fmt.Sprintf("%s, Dividend id %d, Divisor is %d", de.ErrStr, de.Dividend, de.Divisor)
}
// 除数是否是负数
func (de *DivideError) IsNegative() bool {
return de.Negative
}
func main() {
result, err := Divide(-10, 0)
if err != nil {
if err, ok := err.(*DivideError); ok {
if err.IsNegative(){
fmt.Printf("%s\n","The Dividend is negative number!")
}
}
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", result)
}
}
func Divide(a, b int) (int, error) {
divideError := DivideError{}
if a < 0 {
divideError.Negative = true
}
if b == 0 {
divideError.ErrStr = "Divide failed, The dividend cannot be 0"
divideError.Dividend = a
divideError.Divisor = b
return 0, ÷Error
} else {
return a / b, nil
}
}
# 程序输出
The Dividend is negative number!
Divide failed, The dividend cannot be 0, Dividend id -10, Divisor is 0