Go语言错误处理

Go语言错误处理

1、Go错误处理

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

使用场景,一般情况下有两种情况用到:

  • 程序遇到无法执行下去的错误时,抛出错误,主动结束运行。
  • 在调试程序时,通过 panic 来打印堆栈,方便定位错误。

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: 除零错误

1.1 defer

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 中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果。

1.2 panic和recover

本节主要介绍 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("执行")
}
# 程序输出
捕获异常 异常
执行

1.3 defer陷阱

本节讨论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 的值,这是一种好的编程模式。

1.4 自定义错误

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
使用Errorf向error中添加更多信息
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, &divideError
	} else {
		return a / b, nil
	}
}
# 程序输出
The Dividend is negative number!
Divide failed, The dividend cannot be 0, Dividend id -10, Divisor is 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值