返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.3 错误处理

发布于 2024-10-12 19:15:47 字数 5663 浏览 0 评论 0 收藏 0

没有结构化异常,以返回值标记错误状态。

// builtin
type error interface {
	Error() string
}

// io
func ReadAll(r Reader) ([]byte, error)
  • 标志性错误 :一种明确状态,比如 io.EOF
  • 提示性错误 :返回可读信息,提示错误原因。

必须检查所有返回错误,这也导致代码不太美观。

func main() {
	file, err := os.Open("./main.go")
	if err != nil {
		log.Fatalln(err)
	}

	defer file.Close()

	content, err := io.ReadAll(file)
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Println(string(content))
}

标志性错误,通常以 全局变量 (指针、接口)方式定义。而提示性错误,则直接返回 临时对象

// io
var EOF = errors.New("EOF")
// src/errors/errors.go

package errors

func New(text string) error {
    return &errorString{text}   // pointer & interface
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}
  • 是否有错: err != nil
  • 具体错误: err == ErrVar
  • 错误匹配: case ErrVal
  • 类型转换: e, ok := err.(T)

自定义承载更多上下文的错误类型。

type TestError struct {
	x int
}

func (e *TestError) Error() string {
	return fmt.Sprintf("test: %d", e.x)
}

var ErrZero = &TestError{ 0 }  // 指针! (以便判断是否同一对象)

// -----------------------------

func main() {
	var e error = ErrZero
	fmt.Println(e == ErrZero)

	if t, ok := e.(*TestError); ok {
		fmt.Println(t.x)
	}
}

函数返回时,须注意 error 是否真等于 nil ,以免错不成错。

func test() error {    
	var err *TestError

	// 结构体指针。
	println(err == nil)   // true
    
	// 接口只有类型和值都为 nil 时才等
    // 于 nil。err 显然有类型信息。正
    // 确做法是直接 return nil。
    
	// 转型为接口。
	return err
}

func main() {
	err := test()
	println(err == nil)   // false
}

标准库:

  • errors.New : 创建包含文本信息的错误对象。
  • errors.Join : 打包一到多个错误对象,构建树状结构。
  • fmt.Errorf : 以 %w 包装一到多个错误对象。

包装对象:

  • errors.Unwrap : 返回被包装错误对象(或列表)。
  • errors.Is : 递归查找是否有指定错误对象。
  • errors.As : 递归查找并获取类型匹配的错误对象。

创建错误树,返回完整的错误信息。

func database() error {
	return errors.New("data")
}

func cache() error {
	if err := database(); err != nil {
		return fmt.Errorf("cache miss: %w", err)
	}

	return nil
}

func handle() error {
	return cache()
}

func main() {
	fmt.Println(handle())
}

// cache miss: data

从错误树获取信息。

type TestError struct {}
func (*TestError) Error() string { return "" }

func main() {
    
    // 打包。
	a := errors.New("a")
	b := fmt.Errorf("b, %w", a)
	c := fmt.Errorf("c, %w", b)
    
    fmt.Println(c)                      // c, b, a
    fmt.Println(errors.Unwrap(c) == b)  // true

    // 递归检查。
    fmt.Println(errors.Is(c, a))        // true

	// -------------------------------

	x := &TestError{}
	y := fmt.Errorf("y, %w", x)
	z := fmt.Errorf("z, %w", y)
 
    // 提取(二级指针)类型匹配的错误对象。
	var x2 *TestError
	if errors.As(z, &x2) {
        fmt.Println(x == x2)           // true
	}
}

恐慌

与返回 error 相比, panic / recover 的使用方式,很像结构化异常。

func panic(v any)
func recover() any

恐慌panic )立即中断当前流程,执行延迟调用。
在延迟调用中, 恢复recover ) 捕获并返回恐慌数据。

  • 沿调用堆栈向外传递,直至被捕获,或进程崩溃。
  • 连续引发恐慌,仅最后一次可被捕获。
  • 先捕获恐慌,恢复执行,然后才返回数据。
  • 恢复之后,可再度引发恐慌,可再次捕获。
  • 无论恢复与否,延迟调用总会执行。
  • 延迟调用中引发恐慌,不影响后续延迟调用执行。
func main() {
	defer func() {
        
        // 拦截恐慌,返回数据。
        // 数据未必是 error,也可能是 nil。
        // 无法回到 panic 后续位置继续执行。
        
		if r := recover(); r != nil {
			log.Fatalln(r)
		}
        
	}()
    
    func() {
		panic("p1")   // 终止当前函数,执行 defer。
	}()
	
	println("exit.")  // 即便 recover,也不再执行。
}

// p1
func main() {
	defer func() {
        
        // 只有最后一次被捕获。
		if err := recover(); err != nil {
			log.Fatalln(err)
		}
        
	}()
    
    func() {
    	defer func(){
    		panic("p2")  // 第二次。
    	}()

		panic("p1")      // 第一次。
	}()

	println("exit.")
}

// p2
func main() {
	defer func() {
		log.Println(recover())      // 捕获第二次:p2
	}()
    
    func() {
    	defer func(){
    		panic("p2")             // 第二次!
    	}()

    	defer func() {
    		log.Println(recover())  // 捕获第一次:p1
    	}()

		panic("p1")                 // 第一次!
	}()

	println("exit.")
}

// p1
// p2

恢复函数 recover 只能在延迟调用(topmost)内正确执行。
直接注册为延迟调用,或被延迟函数间接调用都无法捕获恐慌。

func catch() {
	recover()
}

func main() {
	defer catch()                   // 有效!在延迟函数内直接调用。
	// defer log.Println(recover()) // 无效!作为参数立即执行。
	// defer recover()              // 无效!被直接注册为延迟调用。  

	panic("p!")
}

*PanicNilError 替代 nil ,使其有具体含义。

在 1.21 之前, panic(nil) 和没有发生恐慌, recover 都返回 nil ,无法区分这两种状态。

func nopanic() {
	defer func() {
		e := recover()
		println(e == nil)   // true
	}()
}

func panicnil() {
	defer func() {
		e := recover()
		println(e == nil)   // false

		_, ok := e.(*runtime.PanicNilError)
		println(ok)         // true
	}()

	panic(nil)
}

适用场景:

  • 在调用堆栈任意位置中断,跳转到合适位置(恢复,参数为依据)。
  • runtime.Goexit :不能用于 main goroutine,进程崩溃。
  • os.Exit :进程立即终止,延迟调用不会执行。
  • 无法恢复的故障,输出调用堆栈现场。
  • 文件系统损坏。
  • 数据库无法连接。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文