ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Go 语言入门笔记(八):错误处理与 panic/recover——Go 的“异常”哲学

Go 语言入门笔记(八):错误处理与 panic/recover——Go 的“异常”哲学 前面写并发的时候几乎每个函数都返回了error然后就是if err ! nil那套。这篇就把 Go 的错误处理单独拎出来说透顺便聊聊 panic 和 recover。Go 没有 Java 那套 try-catch 异常机制一开始我很不习惯但写多了反而觉得这种显式的错误处理更清晰也更容易让人思考“这个错误到底该不该在这里处理”。Go 的 error 是什么Go 内置的error其实就是一个接口typeerrorinterface{Error()string}任何类型只要实现了Error() string方法就实现了error接口。所以自定义错误非常简单。errors.New 创建简单错误最常用的创建错误方式importerrorserr:errors.New(这是一个错误)fmt.Println(err.Error())// 这是一个错误fmt.Errorf 格式化错误err:fmt.Errorf(读取文件 %s 失败,test.txt)fmt.Println(err)// 读取文件 test.txt 失败fmt.Errorf内部其实就是调用errors.New只是多了格式化功能。错误处理的基本模式Go 里处理错误几乎是“约定俗成”的套路funcdoSomething()(int,error){// 业务逻辑return0,nil}result,err:doSomething()iferr!nil{// 处理错误log.Println(出错了:,err)return}// 正常流程fmt.Println(result)刚开始我觉得这种写法很啰嗦每个可能出错的函数都要跟着一个if err ! nil。但时间长了发现这种显式处理强迫你去思考每一个错误分支代码的健壮性反而更高。相比之下Java 的异常机制很容易让人忽略某些异常直到运行时才暴露。自定义错误类型有时候光靠errors.New不够需要携带更多上下文比如错误码、堆栈信息等。这时可以定义一个结构体来实现error接口typeMyErrorstruct{CodeintMessagestring}func(e*MyError)Error()string{returnfmt.Sprintf(code%d, message%s,e.Code,e.Message)}// 使用err:MyError{Code:404,Message:资源不存在}fmt.Println(err.Error())这种方式很灵活可以在错误里塞任何字段。很多第三方库都自定义了错误类型比如net.OpError、os.PathError等。错误包装%w 和 errors.Is / errors.As错误的包装Go 1.13 引入了错误包装机制用fmt.Errorf配合%w动词可以把一个错误包进另一个错误里funcreadFile(pathstring)error{_,err:os.Open(path)iferr!nil{returnfmt.Errorf(打开文件 %s 失败: %w,path,err)}returnnil}这样返回的错误包含了原始错误err同时增加了新的上下文信息。外层可以通过errors.Is或errors.As来检查是否包含某个特定错误。errors.Is判断错误链中是否包含某个错误err:readFile(test.txt)iferrors.Is(err,os.ErrNotExist){fmt.Println(文件不存在)}elseiferr!nil{fmt.Println(其他错误:,err)}errors.Is会沿着错误链一直找看是否包含目标错误。这样即便错误被包装了好几层也能准确判断出来。errors.As提取错误链中的特定类型varmyErr*MyErroriferrors.As(err,myErr){fmt.Println(错误码:,myErr.Code)}errors.As会检查错误链中是否存在某个具体类型的错误如果存在就赋值给目标变量。这在自定义错误类型时很有用。注意%w只能包装一个错误不能包装多个。而且最好只包装一次不要重复包装否则错误链会变得很啰嗦。panic 和 recover不是用来处理常规错误的Go 里还有一个panic机制可以让程序立即崩溃。但它不是用来处理普通错误的而是用来处理不可恢复的严重问题比如数组越界、空指针引用、除零等运行时错误。panic 的触发funcmain(){panic(程序崩溃了)fmt.Println(这行不会执行)}运行时会打印 panic 信息和堆栈然后退出。除了手动调用panic很多运行时错误也会触发 panic比如vars[]intfmt.Println(s[0])// panic: runtime error: index out of rangerecover 捕获 panicrecover是 Go 提供的唯一能从 panic 中恢复的机制。它必须在defer函数中调用才有效funcsafeRun(){deferfunc(){ifr:recover();r!nil{fmt.Println(捕获到 panic:,r)}}()panic(出事了)fmt.Println(这行不会执行)}funcmain(){safeRun()fmt.Println(程序继续执行)}输出捕获到 panic: 出事了 程序继续执行recover返回 panic 传入的值如果没有 panic 则返回nil。需要注意的是recover只能恢复同一个 goroutine 里的 panic跨 goroutine 的 panic 无法恢复。什么时候该用 panic原则能返回 error 就不要用 panic。panic 意味着程序进入不可控状态应该立即终止。一般用于程序启动时的致命错误如配置加载失败编程错误比如函数内部逻辑矛盾应该用 panic 暴露问题并发访问未初始化的 mapGo 运行时会 panic我刚开始写 Go 的时候总想用 panic 来简化错误处理结果被同事提醒panic 会中断整个程序而 error 让调用方有机会决定如何处理。所以现在除非真的无法继续运行否则一律返回 error。一个常见坑recover 只在 defer 函数里有效看这段代码funcwrongRecover(){ifr:recover();r!nil{fmt.Println(捕获到:,r)}panic(错误)}funcmain(){wrongRecover()fmt.Println(这行不会执行)}这段代码不会捕获 panic因为recover必须直接写在defer函数里而且要在 panic 发生之前已经注册。正确的做法是funccorrectRecover(){deferfunc(){ifr:recover();r!nil{fmt.Println(捕获到:,r)}}()panic(错误)}这个细节面试经常考。另一个坑panic 后 defer 的返回值如果函数有命名返回值在 panic 被 recover 后defer 可以修改返回值这也是前面讲 defer 时提到的。结合 panic/recover 可以写出比较高级的错误处理模式但用不好会让代码很费解建议少用。小结这篇把错误处理和 panic/recover 梳理了一遍error是接口任何实现Error() string的类型都可以当作错误。处理错误的基本模式返回error调用方if err ! nil检查。自定义错误类型可以携带更多信息。错误包装用%w配合errors.Is和errors.As判断错误链。panic用于不可恢复的错误recover只能在defer函数里有效。能返回 error 就不要用 panic这是 Go 的哲学。下一篇准备聊聊 Go 的包管理和依赖管理毕竟实际项目不可能只写一个文件。Go Modules 是怎么工作的怎么管理依赖版本还有工作区模式这些内容面试也经常问。到时候继续记录。如果这篇文章对你有帮助欢迎点赞收藏评论区一起交流。
返回列表