G

[Golang] Go异常处理与其他语言的区别(补)

RoLingG Golang 2026-08-27

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 Error

error 本身只是一个接口

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 != nil

Go 认为文件不存在这类错误,其实非常正常,可能是用户输入错路径等情况,这不是会引起程序崩溃的问题,只是单纯的业务场景错误。


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
 ↑
exit

recover

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#/PythonGo
错误处理Exceptionerror 返回值
控制流throw 打断流程正常 return
是否必须处理不一定调用方通常会显式判断 err
常见业务错误throw Exceptionreturn err
真正不可恢复错误Exception/Errorpanic
捕获机制try/catchrecover
错误传播自动沿调用栈传播显式 return err 层层返回
错误包装多依赖异常链%werrors.Iserrors.As

Go 最特别的地方

Go 把错误处理从一种隐式的控制流机制,变成了显式的数据流机制

  • 错误是值(Errors are values):错误和普通返回值一样传递、组合和包装,而不是通过抛异常改变程序流程。
  • 控制流始终清晰:阅读代码时,可以直接看到哪些地方可能返回错误,以及错误如何被处理,不需要追踪潜在的 throw
  • 业务错误与程序错误分离:绝大多数可预期的问题(如 I/O、网络、数据库、参数错误)使用 error;只有真正表示程序进入不可恢复状态时才使用 panic

这种设计让 Go 的代码看起来需要写很多 if err != nil,但换来的好处是错误传播路径完全显式,更容易推断程序行为,也更符合 Go 倡导的简单、可预测的编程风格。

PREV
[每日算法] 接雨水

评论(0)

发布评论