当前位置: 首页 > article >正文

【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 = true
	p.doPrintf(format, a)
	s := string(p.buf)
	var err error
	switch len(p.wrappedErrs) {
	case 0:
		err = errors.New(s)
	case 1:
		w := &wrapError{msg: s}
		w.err, _ = a[p.wrappedErrs[0]].(error)
		err = w
	default:
		if p.reordered {
			slices.Sort(p.wrappedErrs)
		}
		var errs []error
		for 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

  1. panic后面的代码不会被执行,包括之后的defer
  2. 如果panic之前有defer语句,先执行defer语句(LIFO)。panic之后的defer语句不被执行
  3. 如果有panic发生,我们尽可能接收它,并处理

recover

  1. recover:接收panic异常并处理,一般是recover结合defer处理 panic 恐慌
  2. recover必须在defer语句中调用,才能捕获到panic。defer语句会延迟函数的执行,直到包含它的函数即将返回时,才执行defer语句中的函数。
  3. recover会返回panic的参数,直接接收即可
  4. 即便使用了recover,panic后面的代码依然不会被执行
  5. 当前函数的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使用注意事项

生产环境中常出现的问题

  1. 先通过一个简单的示例说明问题:defer遇到变量会怎么样?

    func TestChan(t *testing.T) {
    	x := 5
    	defer fmt.Println(x) // 捕获并推迟输出x的值
    	x = 10
    }
    
    

    实际输出:

    === RUN   TestChan
    5
    --- PASS: TestChan (0.00s)
    PASS
    
  2. 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
    
  3. 切片与defer相关

    func TestChan(t *testing.T) {
    	x := 5
    	defer fmt.Println(x) // 捕获并推迟输出x的值 5
    	x = 10
    
    	s := make([]int, 2, 3)
    	s[0] = 1
    	defer fmt.Printf("defer中的输出:%v\n", s) // 保持入栈时切片的信息,即len=2因此输出了底层数组的值
    
    	s[1] = 2 // 注意容量为2,s切片底层的len一直为2
    	s = 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) {
    
    	// exp1
    	x := 5
    	defer fmt.Println(x) // 捕获并推迟输出x的值 5
    	x = 10
    
    	// exp2
    	n := 10
    	defer func() {
    		fmt.Println(n) //n此时是引用!
    	}()
    	n = 20
    
    	// exp3
    	y := 3
    	defer 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中,和闭包相关的一个好习惯:直接拷贝参数值,以免造成意想不到的意外,做到可控。

http://www.kler.cn/a/465965.html

相关文章:

  • JVM学习:CMS和G1收集器浅析
  • 【Android项目学习】3. MVVMHabit
  • LangChain 介绍
  • 计算机的错误计算(二百零二)
  • 安卓入门十一 常用网络协议四
  • python.exe无法找到程序入口 无法定位程序输入点(Anaconda Prompt报错)
  • 探索Docker Compose:轻松管理多容器应用
  • RAID磁盘整列
  • cut-命令详解
  • 【Linux】传输层协议UDP
  • CDP集群安全指南-静态数据加密
  • 奇异值分解SVD
  • vue字符串的数字比较大小有问题
  • typescript安装后仍然不能使用tsc,如何解决
  • mask-R-cnn模型详解
  • overleaf写学术论文常用语法+注意事项+审阅修订
  • 重庆大学软件工程复试怎么准备?
  • 使用免费内网穿透(p2p)网络环境搭建小型文件管理服务器(简单操作)
  • ESP32-S3遇见OpenAI:OpenAI官方发布ESP32嵌入式实时RTC SDK
  • 中药和西药的区别
  • 《解密奖励函数:引导智能体走向最优策略》
  • 【数据结构】栈与队列(FIFO)
  • 基于TI AM62X/AM64X+FPGA+AD7606/ADS8568多通道AD采集的电力应用
  • sklearn基础教程
  • PAI灵骏智算服务
  • 【什么是中间件】