公司动态
Go错误处理:error、panic、recover的正确姿势
TL;DR 核心要点速览Go编译型语言,执行速度比Python快10倍Goroutine初始栈2KB,比线程轻100倍Channel是Go并发通信的核心原语GMP调度器自动管理goroutine调度Go标准库覆盖HTTP/JSON/加密等常用场景本篇是Go入门模块,建议按顺序学习摘要Go错误处理全解,error接口、自定义错误、panic和recover机制,生产环境错误处理最佳实践第11篇 Go错误处理error、panic、recover的正确姿势摘要Go语言采用显式错误处理机制通过error接口、panic和recover三套机制处理异常本文讲解error的创建与检查、错误包装与errors.Is/As、panic/recover的适用边界对比Go错误处理与Java异常体系的根本差异。我以前写Java的时候try-catch一包就完事了异常往上抛到顶层统一处理。转Go之后满屏的if err ! nil一开始特别崩溃。用了半年之后我理解了Go的设计哲学。显式的错误处理让每个错误都被看到、被处理不会像Java那样一个异常突然从底层冒到顶层你根本不知道中间发生了什么。但这也要求你掌握正确的错误处理姿势否则代码会又臭又长还容易漏掉问题。今天我把error、panic、recover三个机制从头讲清楚。一、error的基本用法Go的错误处理核心就是error接口。error是一个内置接口只有一个Error()方法。函数返回error调用方检查是否为nil。packagemainimport(errorsfmt)// Divide 除法运算返回结果和error// Go的惯例是把error放在返回值的最后一个funcDivide(a,bfloat64)(float64,error){ifb0{// 用errors.New创建一个简单的错误return0,errors.New(除数不能为零)}returna/b,nil// 没有错误时返回nil}funcmain(){result,err:Divide(10,0)// 标准的错误检查模式// 每一个返回error的调用都要检查iferr!nil{fmt.Println(出错了,err)return}fmt.Println(结果,result)// 正常调用不报错r,_:Divide(10,2)fmt.Println(结果,r)// 输出 5}这个if err ! nil的模式在Go里无处不在。虽然看起来啰嗦但它强迫你面对每一个可能的错误。Java的异常处理允许你忽略异常往上抛Go要求你在调用点就决定怎么处理。二、自定义错误和错误包装实际项目里光用errors.New不够你需要自定义错误类型携带更多的上下文信息。packagemainimport(errorsfmt)// ValidationError 自定义错误类型// 实现了error接口的Error()方法typeValidationErrorstruct{Fieldstring// 出错的字段名Messagestring// 错误描述}func(e*ValidationError)Error()string{returnfmt.Sprintf(字段[%s]校验失败 %s,e.Field,e.Message)}// NotFoundError 资源不存在错误typeNotFoundErrorstruct{Resourcestring// 资源名称IDint// 资源ID}func(e*NotFoundError)Error()string{returnfmt.Sprintf(%s不存在 ID%d,e.Resource,e.ID)}// GetUser 模拟获取用户funcGetUser(idint)(string,error){ifid0{// 返回自定义错误带字段信息return,ValidationError{Field:id,Message:必须大于0,}}ifid999{// 资源不存在return,NotFoundError{Resource:用户,ID:id,}}returnfmt.Sprintf(用户%d,id),nil}funcmain(){_,err:GetUser(-1)// 用errors.As检查错误类型// 能把错误解包到目标变量varve*ValidationErroriferrors.As(err,ve){fmt.Println(校验错误 字段,ve.Field)// 输出 id}_,err2:GetUser(999)varnfe*NotFoundErroriferrors.As(err2,nfe){fmt.Println(未找到 资源,nfe.Resource)// 输出 用户}}Go 1.13引入了错误包装机制。用fmt.Errorf加%w动词可以把底层错误包装起来保留原始错误信息。packagemainimport(errorsfmt)// ErrNotFound 哨兵错误用变量定义varErrNotFounderrors.New(资源不存在)// FindUser 查找用户funcFindUser(idint)error{ifid0{// 用%w包装错误保留原始错误链// 这样调用方可以用errors.Is检查是否是ErrNotFoundreturnfmt.Errorf(查询用户失败 id%d %w,id,ErrNotFound)}returnnil}funcmain(){err:FindUser(0)// errors.Is沿着错误链检查// 即使错误被包装了多层也能找到底层的ErrNotFoundiferrors.Is(err,ErrNotFound){fmt.Println(捕获到ErrNotFound)}// 打印完整的错误信息能看到包装的上下文fmt.Println(完整错误,err)// 输出 查询用户失败 id0 资源不存在}错误包装的关键在于%w动词。用%w包装的错误保留了原始错误调用方可以用errors.Is和errors.As解包检查。如果你用%v而不是%w原始错误信息就丢了调用方无法判断根因。三、panic和recoverpanic是Go的崩溃机制一旦触发当前函数立刻停止逐层执行defer最后进程退出。recover只能在defer里调用用来捕获panic让程序继续运行。packagemainimportfmt// SafeDivide 安全的除法用panic和recover保护funcSafeDivide(a,bint)(resultint,errerror){// defer里recover捕获panicdeferfunc(){ifr:recover();r!nil{// recover返回panic的值// 把panic转成error返回errfmt.Errorf(内部错误 %v,r)}}()// 这里可能panicb为0时整数除法会panicreturna/b,nil}funcmain(){result,err:SafeDivide(10,0)iferr!nil{fmt.Println(出错了,err)// 输出 内部错误 runtime error...return}fmt.Println(结果,result)}panic的使用场景非常有限。Go官方的建议是panic只用于真正不可恢复的编程错误比如数组越界、空指针解引用、初始化失败。业务逻辑的错误应该用error返回不用panic。四、独家踩坑说一个我踩过的坑关于panic和goroutine的组合。有一次我在一个goroutine里调用了第三方库第三方库内部panic了。我以为外层有defer recover就能兜住结果整个进程崩溃了。packagemainimport(fmttime)// riskyFunction 一个可能panic的函数funcriskyFunction(nint){ifn0{panic(参数不能为负数)}fmt.Println(处理完成 n,n)}funcmain(){// 主函数的defer recover只能捕获当前goroutine的panicdeferfunc(){ifr:recover();r!nil{fmt.Println(主函数捕获到panic,r)}}()// 在新goroutine里调用// 这里的panic主函数的recover捕获不到gofunc(){// 必须在每个goroutine里单独recoverdeferfunc(){ifr:recover();r!nil{fmt.Println(goroutine捕获到panic,r)}}()riskyFunction(-1)// 会panic}()// 等待goroutine执行time.Sleep(time.Second)fmt.Println(主函数结束)}这个坑的核心在于recover只能捕获当前goroutine的panic。goroutine之间没有父子关系一个goroutine的panic不会传播到另一个goroutine。如果goroutine里panic了而且没有recover整个进程直接崩溃。解决方法就是在每个可能panic的goroutine里都加上defer recover。我后来写了一个通用的安全启动函数。packagemainimport(fmttime)// SafeGo 安全启动goroutine// 自动加上recover防止panic导致进程崩溃funcSafeGo(fnfunc()){gofunc(){deferfunc(){ifr:recover();r!nil{fmt.Printf(goroutine panic %v\n,r)}}()fn()}()}funcmain(){// 用SafeGo启动即使panic也不会崩进程SafeGo(func(){panic(测试panic)})time.Sleep(time.Second)fmt.Println(主函数正常结束)// 正常输出}那次事故之后我们团队约定所有go语句必须走SafeGo封装禁止裸写go func。五、对比分析跟Java和Python的错误处理对比一下。Java用异常体系受检异常强制处理运行时异常可以忽略。好处是错误处理和业务逻辑分离代码看起来干净。坏处是异常控制流是隐式的你不知道哪一行会抛异常调试困难。Go用error显式返回错误处理就在调用点一目了然但代码啰嗦。Python用try-except异常可以任意抛任意接。Go更严格错误必须显式处理或显式忽略。panic和Java的RuntimeException有点像但Go的panic设计上只给编程错误用不鼓励用作业务流程控制。六、总结Go的错误处理三板斧是error、panic和recover。业务错误用error返回编程错误用panicrecover兜底防崩溃。记住错误用%w包装保留原始信息每个goroutine都要单独recover。模块一到这里就结束了下一篇我们进入Go并发编程的世界聊聊goroutine和channel。相关推荐Go数组与切片底层原理与性能陷阱Go映射(Map)实现原理与并发安全Go结构体(Struct)面向对象的第一步常见问题 FAQQ: Go语言好学吗有编程基础需要多久A: Go语法仅25个关键字,有编程基础1周可入门,3个月能写生产级后端服务。Q: Go和Python/Java哪个好A: Go在云原生和微服务方向需求旺盛,执行速度比Python快10倍,比Java更轻量。Q: Go语言薪资水平怎么样A: Go后端开发薪资15-50K,中级20-35K,高级30-45K,一线城市更高。Q: 学Go需要什么基础A: 了解基本编程概念即可,Go语法简单,专栏从零开始讲解。Q: Go的泛型成熟了吗A: Go 1.18引入泛型,1.22已成熟可用,专栏有专门章节讲解。为什么订阅本专栏对比维度本专栏(160篇)免费资源培训班内容深度源码级分析生产踩坑入门级示例系统级讲解实战项目10完整可运行项目片段代码2-3个项目踩坑经验每篇1个独家生产事故几乎没有少量对比分析每篇1个三方对比表格无偶尔提及价格99元(首发价)0元5000-20000元更新频率已完结160篇不定期一次性答疑支持评论区24h内回复无有限时99元,30秒完成订阅,160篇Go语言云原生实战内容全部解锁,今天就能开始系统学习