【go每日一题】golang异常、错误 {源码、实践、总结}
错误与异常在golang中区分
Go 的错误处理设计与其他语言的异常不同。Go 中的 error 就是一个普通的值对象,而其他语言如 Java 中的 Exception 将会造成程序控制流的终止和其他行为,Exception 与普通的值不同。虽然 Go 也有类似的异常机制 —— panic,但它仅用于报告完全无法预料的错误(可能有 Bug),而不应该是一个健壮程序应该返回的程序错误(这一点与 Java 等语言不同)。
- 错误是业务的一部分,而不同与异常。例如:开一个文件:文件正在被占用,可知的。
错误error
- Go中的错误也是一种类型。错误用内置的error类型表示。就像其他类型的,如int, float64。
1. 在built-in 包中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
}
在我们最常用的函数返回值类型就是这个接口类型
Go 将 error 设计为一个接口,只需要实现 Error() string 方法,返回有意义、简练的错误描述信息即可。这也使得我们可以以任何的方式来自定义错误。
2. golang内部对该接口的 实现1:errors.New(text string)error
源码中定义了一个结构体类型errorString,这个结构体实现了error接口定义的Error()方法,因此业务中直接 errors.New("err msg")
就可以使用了
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} // 这个结构体实现了error接口中的方法,因此返回的结构体也是error类型
}// errorString is a trivial implementation of error.
type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}
3. golang内部对该接口的 实现2:fmt.Errorf(format string, a …any) error
fmt 包下有一个Errorf,使用也十分简单,返回值也是error类型。
err := fmt.Errorf("this is an err| code: %d, msg: %s", 404, "IER")
源码中,返回的可能是不同实现error接口的结构体类型
比如case 0 返回的就是上面的errorString 结构体类型,case 1 是wrapError结构体类型,该类型也实现了error接口,典型的多态
func Errorf(format string, a ...any) error {p := newPrinter()p.wrapErrs = truep.doPrintf(format, a)s := string(p.buf)var err errorswitch len(p.wrappedErrs) {case 0:err = errors.New(s)case 1:w := &wrapError{msg: s}w.err, _ = a[p.wrappedErrs[0]].(error)err = wdefault:if p.reordered {slices.Sort(p.wrappedErrs)}var errs []errorfor i, argNum := range p.wrappedErrs {if i > 0 && p.wrappedErrs[i-1] == argNum {continue}if e, ok := a[argNum].(error); ok {errs = append(errs, e)}}err = &wrapErrors{s, errs}}p.free()return err
}
异常
与错误处理不同,Go语言中的异常处理是通过panic和recover关键字来实现的。panic用于表示程序中的严重错误,它会导致程序中断执行;recover用于捕获panic,并恢复程序的正常执行流程。
panic
- panic后面的代码不会被执行,包括之后的defer
- 如果panic之前有defer语句,先执行defer语句(LIFO)。panic之后的defer语句不被执行
- 如果有panic发生,我们尽可能接收它,并处理
recover
- recover:接收panic异常并处理,一般是recover结合defer处理 panic 恐慌
- recover必须在defer语句中调用,才能捕获到panic。defer语句会延迟函数的执行,直到包含它的函数即将返回时,才执行defer语句中的函数。
- recover会返回panic的参数,直接接收即可
- 即便使用了recover,panic后面的代码依然不会被执行
- 当前函数的panic被recover之后,表示当前函数直接被执行完毕,正常执行下一个函数
func main() {//defer语句绑定的匿名函数defer func() {r := recover()if r != nil {fmt.Println("捕获到异常:", r)}}()panic("发生了异常")fmt.Println("这行代码不会被执行")
}
尝试下面的输出顺序:
func TestRecovery1(t *testing.T) {defer fmt.Println("defer1")defer fmt.Println("defer2")fmt.Println("main")panicAndREVFun()normalFunc()
}
func normalFunc() {fmt.Println("后面的函数正常执行")
}
func panicAndREVFun() {defer fmt.Println("defer func 1")defer func() {if msg := recover(); msg != nil {fmt.Println(msg)//recover 之后,在下面完成当前代码的善后处理,函数将执行完毕}}()panic("this is panic msg")fmt.Println("无论panic是否被recovery,panic后面的代码不被执行")
}
=== RUN TestRecovery1
main
this is panic msg
defer func 1
后面的函数正常执行
defer2
defer1
— PASS: TestRecovery1 (0.00s)
PASS
Process finished with the exit code 0
defer使用注意事项
生产环境中常出现的问题
-
先通过一个简单的示例说明问题:defer遇到变量会怎么样?
func TestChan(t *testing.T) {x := 5defer fmt.Println(x) // 捕获并推迟输出x的值x = 10 }
实际输出:
=== RUN TestChan 5 --- PASS: TestChan (0.00s) PASS
-
channel 实际中经常遇到的错误:
var ch2 chan struct{} // 声明 func TestPrint(t *testing.T) {defer close(ch2)ch2 = make(chan struct{}) // 之后初始化... }
实际运行:
panic: close of nil channel [recovered]panic: close of nil channel
-
切片与defer相关
func TestChan(t *testing.T) {x := 5defer fmt.Println(x) // 捕获并推迟输出x的值 5x = 10s := make([]int, 2, 3)s[0] = 1defer fmt.Printf("defer中的输出:%v\n", s) // 保持入栈时切片的信息,即len=2因此输出了底层数组的值s[1] = 2 // 注意容量为2,s切片底层的len一直为2s = append(s, 3) // 此时len发生改变fmt.Println("非defer中的输出:", s) }
实际输出
=== RUN TestChan 非defer中的输出: [1 2 3] defer中的输出:[1 2] 5 --- PASS: TestChan (0.00s) PASS
- 分析原因:
- defer 语句的参数在定义时就已经确定,而不会等到函数执行到 defer 语句时再评估。(defer是在函数结束时调用,但是defer 函数参数确是立即求值的)
- 原理是:当程序执行到 defer 语句时,会将 defer 后面的函数调用及其参数存储在一个栈中
- 关键点:看defer语句捕获到栈中的 都是当前值(但是注意这个值可能本身就是一个引用,指向的是底层的数据结构,就比如切片)
对于channel使用defer关闭的总结: 为了避免混淆,通常建议在 defer 语句中直接使用最终的通道,或者将 defer 语句放在通道创建之后,这样可以确保 defer 语句关闭的是期望的通道。
-
最后看一个最逆天的例子。是的,bug写多了,自己都会出题了:
func TestDefer(t *testing.T) {// exp1x := 5defer fmt.Println(x) // 捕获并推迟输出x的值 5x = 10// exp2n := 10defer func() {fmt.Println(n) //n此时是引用!}()n = 20// exp3y := 3defer func(num int) {fmt.Println(num) // 闭包,输出3,值拷贝}(y)y = 4}
想不到吧,输出居然是:3、20、5
- 还是根据之前的分析:
- 这里的关键是,exp2中放到defer栈中的其实是一个匿名函数,这里 n 是被一个匿名函数捕获的,而不是直接作为 defer 参数传递。匿名函数中的 n 变量会捕获 n 的引用,所以在 defer 执行时,它访问的是最新的 n 值。总结:在匿名函数中,n 不是立即传值,而是被匿名函数捕获。
- 和之前一样,exp1中defer 在注册时会 捕获 当前的参数值,而不是延迟执行时的值。
- exp3就比较经典了,在使用waitgroup、循环开启goroutine中,和闭包相关的一个好习惯:直接拷贝参数值,以免造成意想不到的意外,做到可控。