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

【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

  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 := 5defer 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 := 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中,和闭包相关的一个好习惯:直接拷贝参数值,以免造成意想不到的意外,做到可控。

http://www.mrgr.cn/news/82413.html

相关文章:

  • 1月第一讲:WxPython跨平台开发框架之前后端结合实现附件信息的上传及管理
  • 基于SpringBoot的微信小程序民宿预约管理系统
  • ubuntu安装firefox
  • 基于开发/发布/缺陷分离模型的 Git 分支管理实践20250103
  • PHP如何删除数组中的特定值?
  • 【虚拟机】VMware 16图文安装和配置 AlmaLinux OS 9.5 教程
  • 数据挖掘——支持向量机分类器
  • 【sql】CAST(GROUP_CONCAT())实现一对多对象json输出
  • C 实现植物大战僵尸(二)
  • Elasticsearch: 高级搜索
  • UnityRenderStreaming使用记录(四)
  • 鸿蒙HarmonyOS开发:拨打电话、短信服务、网络搜索、蜂窝数据、SIM卡管理、observer订阅管理
  • 《计算机网络A》单选题-复习题库
  • 专题十四——BFS
  • 【开源社区openEuler实践】compass-ci
  • Vue2: table加载树形数据的踩坑记录
  • uni-app开发-习惯养成小程序/app介绍
  • HackMyVM-Adroit靶机的测试报告
  • springboot原生socket通讯教程
  • 【开源社区openEuler实践】A-Tune性能优化工具介绍
  • 【ACCSS】2024年亚信安全云认证专家题库
  • 高等数学学习笔记 ☞ 无穷小与无穷大
  • 【开源社区openEuler实践】rust_shyper
  • 2025元旦源码免费送
  • 从企业级 RAG 到 AI Assistant,阿里云 Elasticsearch AI 搜索技术实践
  • 数据挖掘——回归算法