go语言的error,panic/recover和defer详解

218 阅读6分钟

error

error 在 Go 中其实是一个普通的接口。它不仅保存着错误的信息,还提供了一系列的方式供开发者使用。因此开发者可以自行拓展,嵌套,封装新的 error ,为项目提供自定义错误模块。我们看error的源码就可以发现它就是一个实现了Error()方法的类型:

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
}

可以看到errors包给我们提供的主要就是New()方法,它返回一个errorString,里面记录了错误信息。这点和c语言其实很像,error就是一个提示错误信息的字符串。

如何使用error

error只应该被处理一次,打印error也是对error的一种处理。所以对于error,要么打印出来,要么就把error返回传递给上一层。

我们在开发的过程中,可以有一些约定。在哪些部分应该打印错误,在哪些又应该返回错误。在业务逻辑中,我们需要对error进行封装(即加入更丰富的提示信息,如调用栈等/上层参数等),又或者需要对error进行判断,根据它的错误执行不同的逻辑。如访问数据库的代码,在数据库负载比较高的情况下,调用db包里的方法可能会返回一个临时的db.DBError的错误,对于这种情况我们需要做重试。errors包中就提供了对上述逻辑的支持。

  • errors.New函数用于创建一个新的错误实例。它接受一个字符串参数,这个字符串描述了错误的详情,并返回一个满足error接口的值。
package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("something went wrong")
    fmt.Println(err)
}
  • errors.Is函数用来判断一个错误值是否等于给定的错误实例或是否为其错误链中的一部分。主要用于处理包装错误后的错误比较。
package main

import (
    "errors"
    "fmt"
)

func main() {
    var targetError = errors.New("target error")

    // 包装error,其实就是拼接字符串
    err := fmt.Errorf("context: %w", targetError)

    // 检查err是否是/包含targetError
    if errors.Is(err, targetError) {
       fmt.Println("Error is target error.")
    }
}
  • errors.As函数用于将错误值转换为特定的错误类型。如果错误链中存在该类型的错误,它会将错误的值赋给目标类型,并返回true
package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    _, err := os.Open("non-existing-file.txt")
    if err != nil {
       var pathError *os.PathError
       if errors.As(err, &pathError) {
          fmt.Println("Failed at path:", pathError.Path)
       }
    }
}
  • fmt.Errorf()和 join()用于对错误进行包装,添加更丰富的错误信息
package main

import (
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("err1")
    err2 := errors.New("err2")
    // 两者效果一样,区别是join会默认给拼接的err间加上换行符
    //err := errors.Join(err1, err2)
    err := fmt.Errorf("%w \n%w", err1, err2)
    fmt.Println(err)
    if errors.Is(err, err1) {
       fmt.Println("err is err1")
    }
    if errors.Is(err, err2) {
       fmt.Println("err is err2")
    }
}
  • errors.Unwrap函数返回被包装的错误(如果有的话)。当错误被另一个错误包装时,可以使用这个方法来获取原始错误。
package main

import (
    "errors"
    "fmt"
)

func main() {
    originalError := errors.New("original error")
    wrappedError := fmt.Errorf("wrapped: %w", originalError)

    unwrappedError := errors.Unwrap(wrappedError)
    fmt.Println(unwrappedError) // 输出: original error
}

panic/recover

GO 也提供了从 panic 中恢复的接口—— recover 。但这不意味着开发者应该把其当作 try... catch... 使用。而是应该当作是程序崩溃后的特殊处理的最后机会。在 Go 的设计中,panic 一旦触发,说明程序应该要退出了。但在某些业务场景下,我们可能还会有日志上报,日志打印,信息通知等操作。此时,就应该考虑使用 recover来做这些兜底操作。

package main

import "fmt"

func mayPanic() {
    panic("a problem")
}

func main() {
    defer func() {
       if r := recover(); r != nil {
          fmt.Println("Recovered from panic:", r)
       }
    }()
    mayPanic()
    fmt.Println("After mayPanic()")
}

panic被引发的时候,它会从函数调用中逐层向上退栈,直到遇到recover,否则直接终止程序并输出错误信息。

defer

defer 是一个非常有用的语句,用来确保一个函数调用会在当前函数执行完成后,自动执行。通常,defer 用于资源清理工作,比如关闭文件句柄、解锁互斥锁、关闭数据库连接等,无论函数是通过正常路径完成,还是因为发生了 panic 异常而中断。它有以下注意事项:

  • defer中的用到的参数是在执行时确定的
package main

import "fmt"

func main() {
    var i = 1
    defer fmt.Println(i)
    i++
    fmt.Println(i)
}
// 先输出2,再输出1

也就是说:defer中的值会在执行它的时候赋值,而不是调用的时候

  • 多个defer的执行顺序类似于栈,后进先出
package main

import "fmt"

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    // 输出: 3 2 1
}
  • return后的语句先执行,再执行defer
package main

import "fmt"

func deferFunc() int {
    fmt.Println("defer func called")
    return 0
}

func returnFunc() int {
    fmt.Println("return func called")
    return 0
}

func returnAndDefer() int {

    defer deferFunc()

    return returnFunc()
}

func main() {
    returnAndDefer()
}
// return func called
// defer func called
  • 有名函数返回值遇见defer 在go语言中,有名函数返回值在整个函数作用域中都有效,并且它会被默认初始化。我们先看一个简单的例子:
package main

import "fmt"

func returnTest() (t int) {
    defer func() {
       t = t + 100
    }()
    return t
}

func main() {
    fmt.Println(returnTest()) //输出 100
}

从上面的例子中我们知道,先执行return,再执行defer。但此时defer语句中依然可以修改返回值 即使我们修改return语句,它也依然会把返回值当成t

package main

import "fmt"

func returnTest() (t int) {
    defer func() {
       t = t + 100
    }()
    return 9
}

func main() {
    fmt.Println(returnTest()) //输出 109
}
  • defer遇见panic,但并不捕获
package main

import "fmt"

func testPanic() {
    defer func() { fmt.Println("before panic 1") }()
    defer func() { fmt.Println("before panic 2") }()

    panic("panic")

    defer func() { fmt.Println("after panic") }()
}

func main() {
    testPanic()
}
// 输出
// before panic 2
// before panic 1
// panic: panic

总结一下:defer在遇见return/panic都会被调用执行,并且panic/return之后的defer不会被调用,这也符合直觉上的判断。

  • defer遇见panic,捕获panic
package main

import "fmt"

func testPanic() {
    defer func() { fmt.Println("before panic 1") }()
    defer func() { fmt.Println("before panic 2") }()
    defer func() {
       fmt.Println("before recover")
       if err := recover(); err != nil {
          fmt.Println("panic recover")
       }
    }()

    panic("panic")

}

func main() {
    testPanic()
}
// 输出
// before recover
// panic recover
// before panic 2
// before panic 1

我们尝试调换一下顺序:

package main

import "fmt"

func testPanic() {
    defer func() {
       fmt.Println("before recover")
       if err := recover(); err != nil {
          fmt.Println("panic recover")
       }
    }()
    defer func() { fmt.Println("before panic 1") }()
    defer func() { fmt.Println("before panic 2") }()

    panic("panic")

}

func main() {
    testPanic()
}
// 输出
// before panic 2
// before panic 1
// before recover
// panic recover

可以发现:无论defer中有没有捕获panic,在发生panic之前的defer语句都可以顺利执行,我们可以利用defer的这一特性来收拾残局、清理资源等,怪不得大家总是把defer写的很靠前呢。

  • defer的函数中包含子函数
package main

import "fmt"

func function(index int, value int) int {

    fmt.Println(index)

    return index
}

func main() {
    defer function(1, function(3, 0))
    defer function(2, function(4, 0))
}
// 输出: 3, 4, 2, 1

语句执行到第一个defer的时候,先将它入栈。入栈的时候需要函数的地址和形参。而这里的形参又是一个函数的返回值,故先执行该函数。入栈第二个defer的时候也是类似。当函数执行结束,开始把defer挨个出栈并调用执行。所以输出顺序是:3, 4, 2, 1.