前面写并发的时候,几乎每个函数都返回了error,然后就是if err != nil那套。这篇就把 Go 的错误处理单独拎出来说透,顺便聊聊 panic 和 recover。Go 没有 Java 那套 try-catch 异常机制,一开始我很不习惯,但写多了反而觉得这种显式的错误处理更清晰,也更容易让人思考“这个错误到底该不该在这里处理”。
Go 的 error 是什么
Go 内置的error其实就是一个接口:
typeerrorinterface{Error()string}任何类型只要实现了Error() string方法,就实现了error接口。所以自定义错误非常简单。
errors.New 创建简单错误
最常用的创建错误方式:
import"errors"err:=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 捕获 panic
recover是 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 暴露问题)
- 并发访问未初始化的 map(Go 运行时会 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 是怎么工作的,怎么管理依赖版本,还有工作区模式,这些内容面试也经常问。到时候继续记录。
如果这篇文章对你有帮助,欢迎点赞收藏,评论区一起交流。