Go异常处理与其他语言的区别(补)
先说结论,Go 的异常处理和 Java、C#、Python 等主流语言最大的区别:Go 不把错误(Error)当成异常(Exception),而是当成一种普通的返回值。
我说白了写久了 Go 项目,也能明显感受到 Golang 在对于普通 err 与其他语言有很明显的区别,if err 也是其他语言开发者吐槽 Go 的老生常谈了, Go 开发者对于此也基本都有几十厘米的脂肪记忆了。其他语言:Exception(异常)
以 Java 为例:
void A() {
B();
}
void B() {
C();
}
void C() {
throw new RuntimeException();
}发生错误时:
函数A
↓
函数B
↓
函数C
↓
throw Exception异常会一路向上传播(Stack Unwinding):
throw
↑
C(退出)
↑
B(退出)
↑
A(退出)
↑
main如果没人接:
程序退出如果 A 有:
void A() {
try {
B();
} catch (Exception e) {
...
}
}那么流程就是:
A
↓
B
↓
C
↓
throw
↑
退出 C
↑
退出 B
↑
catch(A)所以异常本质上是一种非正常控制流(Non-local Control Flow)。
例如有 funcA(),实际上你不知道 funcA() 到底会不会:
throw IOException
throw SQLException
throw RuntimeException
throw NullPointerException
...它可能在任意地方突然跳出来。
Go的Error
Go 对于普通的 Error 不这么设计。Go 函数一般都是:
value, err := ReadFile(path)或者
result, err := service.CreateOrder()调用方必须(也是声明的变量必须被调用):
if err != nil {
return err
}错误就是一个普通返回值。
例如:
func Read() ([]byte, error)调用:
data, err := Read()
if err != nil {
...
}整个控制流没有变化:
Read()
↓
return value, err
↓
调用方检查 err不会像传统 Try-Catch 突然跳走。
为什么 Go 要这么设计?
在 Go 的设计理念,贯彻了 Errors are values. ,既然是数据,就应该像普通变量一样处理。
例如:
user, err := QueryUser(id)
if err != nil {
return err
}这里:
err只是:
error 接口而不是:
throw Errorerror 本身只是一个接口
Go 的 error,其实定义极其简单:
type error interface {
Error() string
}常用的 errors 库,例如:errors.New("not found"),其实就是实现了:
type myError struct{}
func (e myError) Error() string {
return "not found"
}这样一个 error 的接口,根据 Go 对于 interface 的设计,我们可以理解成任何实现了 Error() 方法的对象,都可以是 error
Go 为什么不用 try-catch?
Go 官方认为异常适合真正异常的情况,而不是普通业务错误。
例如文件不存在是不是异常?
对于 Java 而言,需要抛出错误:
IOException但对于 Go:
err != nilGo 认为文件不存在这类错误,其实非常正常,可能是用户输入错路径等情况,这不是会引起程序崩溃的问题,只是单纯的业务场景错误。
Go panic 的使用
Go 不是没有异常(panic)。
例如:
panic("boom")流程就是:
panic
↓
函数退出
↓
defer
↓
上一层
↓
defer
↓
上一层一直到 main,如果期间没有一层使用 recover() 劫持 panic,则程序退出。这一点其实和 Java Exception 很像。
例如:
func A() {
B()
}
func B() {
panic("boom")
}panic 传输流程如下:
A
↓
B
↓
panic
↑
A defer
↑
main defer
↑
exitrecover
Go 可以使用 recover 劫持 panic:
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()这样 panic 会被拦住。因此 panic + recover 就是 Go 的 Try-Catch。
但是官方明确建议不要把 panic/recover 当成业务异常处理机制。也就是说,像数据库连接失败这种严重影响程序甚至会导致程序崩溃的情况,即使用 panic 停止程序运行,避免程序出现更多意外情况。
Go 的异常分层
一般来说普通错误直接 return error
例如:
- 文件不存在
- 网络超时
- 参数非法
- SQL 查询失败
只有程序已经无法继续运行,才 panic
例如:
- 数组越界(运行时)
- nil 指针(运行时)
- 初始化失败
- 不可能发生的逻辑错误
- 库内部出现违反约束的状态
Go 还有几个非常有特色的错误处理能力
错误包装(Error Wrapping)
fmt.Errorf("query user: %w", err)例如:
err := os.ErrNotExist
return fmt.Errorf("read config: %w", err)最终错误:
read config: file does not exist同时还能保留原始错误。
errors.Is()
判断错误链:
if errors.Is(err, os.ErrNotExist) {
...
}不用 err.Error() == "xxxx" 这种脆弱的字符串比较。
errors.As()
获取具体错误类型:
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}类似 Java:
catch(IOException e)但是不会打断控制流。
自定义错误类型
例如:
type NotFoundError struct {
ID int
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("id=%d not found", e.ID)
}以后:
return NotFoundError{ID: 10}别人可以:
var e NotFoundError
if errors.As(err, &e) {
...
}Go 与传统异常机制的对比
| 对比项 | Java/C#/Python | Go |
|---|---|---|
| 错误处理 | Exception | error 返回值 |
| 控制流 | throw 打断流程 | 正常 return |
| 是否必须处理 | 不一定 | 调用方通常会显式判断 err |
| 常见业务错误 | throw Exception | return err |
| 真正不可恢复错误 | Exception/Error | panic |
| 捕获机制 | try/catch | recover |
| 错误传播 | 自动沿调用栈传播 | 显式 return err 层层返回 |
| 错误包装 | 多依赖异常链 | %w、errors.Is、errors.As |
Go 最特别的地方
Go 把错误处理从一种隐式的控制流机制,变成了显式的数据流机制:
- 错误是值(Errors are values):错误和普通返回值一样传递、组合和包装,而不是通过抛异常改变程序流程。
- 控制流始终清晰:阅读代码时,可以直接看到哪些地方可能返回错误,以及错误如何被处理,不需要追踪潜在的
throw。 - 业务错误与程序错误分离:绝大多数可预期的问题(如 I/O、网络、数据库、参数错误)使用
error;只有真正表示程序进入不可恢复状态时才使用panic。
这种设计让 Go 的代码看起来需要写很多 if err != nil,但换来的好处是错误传播路径完全显式,更容易推断程序行为,也更符合 Go 倡导的简单、可预测的编程风格。
RoLingG | 博客
评论(0)