Go 语言的异常处理机制一直是社区讨论和争议的焦点。Go 采用了一种独特的错误处理方式,主要通过返回错误值来处理异常情况,而不是使用传统的 try-catch-finally 异常处理模型。以下是一些社区中关于 Go 异常处理的常见争议点:
1.社区争论意见
-
显式错误检查:
支持者: 认为显式错误检查可以减少因忽略错误处理而导致的隐蔽错误,提高代码的可读性和可维护性。
反对者: 则认为,强制的显式错误检查会导致代码冗余,尤其是在有深层嵌套调用时。 -
缺乏泛型错误处理:
反对者:在 Go 1.0 至 Go 1.16 版本中,错误处理缺乏泛型机制,导致开发者需要对每个错误类型进行单独处理。
支持者: Go 1.17 引入了错误封装和错误链的概念,这在一定程度上缓解了这个问题。 -
Panic 和 Recover 的使用:
反对者:panic
和recover
用于处理运行时的异常情况,但它们的使用在社区中存在争议。一些开发者认为panic
应该仅用于不可恢复的错误,而其他人可能会滥用它们来处理常规的错误情况。recover
的使用也受到争议,因为过度使用可能会使控制流复杂化,并且难以追踪程序的执行路径。 -
错误传播:
反对者:在深层嵌套的函数调用中,错误需要逐层传递,这可能会导致代码难以阅读和维护。 -
与面向对象语言的对比:
反对者:来自面向对象编程背景的开发者可能会对 Go 的错误处理方式感到不适应,因为它们习惯于使用异常处理机制。
特别是调用栈很深的情况下,例如下面的演示代码:
package main
import (
"fmt"
)
func divide(dividend, divisor float64) (float64, error) {
if divisor == 0 {
return 0, fmt.Errorf("除数不能为0")
}
return dividend / divisor, nil
}
func middleFun(dividend, divisor float64) (float64, error) {
result, err := divide(dividend, divisor)
if err != nil {
return result, err
}
// do sth
return result, nil
}
func outerFun(dividend, divisor float64) (float64, error) {
result, err := middleFun(dividend, divisor)
if err != nil {
return result, err
}
// do sth
return result, nil
}
func main() {
result, err := outerFun(10, 3)
if err != nil {
fmt.Printf("发生错误: %v\n", err)
return
}
fmt.Printf("结果: %v\n", result)
}
当然,仁者见仁,习惯Go开发的人会觉得这种设计很哲学,甚至当听到别人说这种设计不好的还会嗤之以鼻,心里想,肯定是java或者别的OOP语言转过来的。
2. Java异常处理
2.1.异常与错误
在Java中,异常(Exception)和错误(Error)都是Throwable
类的子类,但它们在Java异常处理机制中扮演不同的角色
异常是程序正常运行中出现的非预期情况,通常是可以被程序处理的。通过try-catch
块捕获并处理异常,以避免程序异常终止,例如下面的代码示例
try {
// 可能抛出异常的代码
} catch (IOException e) {
// 处理IOException
} finnaly {
// 不管有没有异常,这里都会执行
}
错误是程序运行时遇到的严重问题,通常是编程错误或系统问题,如OutOfMemoryError
、StackOverflowError
。可能会导致程序崩溃退出。
异常与错误的比较
- 可恢复性:异常通常是可恢复的,而错误通常是不可恢复的。
- 处理方式:异常需要程序员显式捕获和处理,错误则通常不被捕获。
- 使用场景:异常用于控制程序流程中的异常情况,错误用于指示程序无法处理的严重问题。
- 编译检查:受检异常需要编译时检查,错误不需要。
2.2.受检异常与运行期异常
受检异常是编译时检查的异常,它们通常是可预见的异常情况,如 IOException
、SQLException
等。
在方法中通过 throws
关键字声明抛出,方法调用者必须显式捕捉异常,并进行相关处理。
强制程序员处理这些异常,以避免程序在运行时因未处理的异常而意外终止。
public void readFile(String path) throws IOException {
// 可能抛出 IOException 的代码
}
运行时异常是编译时不检查的异常,通常是编程错误导致的,如 NullPointerException
、IndexOutOfBoundsException
等。
不需要在方法中声明抛出,也不需要强制捕获,但建议捕获并处理以提高程序的健壮性。
指出程序中的逻辑错误或不正确的使用情况,鼓励程序员在开发过程中修复这些问题。
public void processArray(int[] array, int index) {
if (index >= array.length) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + array.length);
}
// 正常处理数组元素
}
3. Go异常处理
3.1.通过错误码返回值
Go 语言中没有传统的异常(exception)机制,而是使用返回错误值的方式来处理错误情况。Go函数允许多重返回值,可以申明两个返回值,一个是函数的结果,另一个是错误对象。
result, err := SomeFunction()
if err != nil {
// 处理错误
}
Go 中的错误是一个内置的接口类型 error
,任何类型都可以实现这个接口,只要它们提供了一个 Error()
方法返回错误信息字符串。
type MyError struct {
// 错误相关的字段
}
func (e *MyError) Error() string {
return "my error message"
}
使用 fmt.Errorf
可以创建新的错误,并在其中包含原始错误的信息
err := fmt.Errorf("wrap error: %w", originalError)
函数返回错误码,可以类比java的受检异常。不同的是,java编译器会强制开发者捕捉异常,而Go的函数返回值,IDE只是警告提示,容易被人忽略。
3.2.panic函数
Go倾向于使用简洁的控制结构和显式的错误检查。但如果程序设计不合理或者考虑不周到,没有返回某些错误,这对于某些底层代码很有可能是致命的,可能会引起程序奔溃。这个时候,可以祭出panic函数作为兜底。
在 Go 语言中,panic
是一个内置的关键词,用于异常情况,当程序遇到无法恢复的错误时,可以通过调用 panic
来立即中断当前函数的执行,并且开始逐层向上 unwind 调用栈,同时清理 defer
语句。panic
通常用于以下情况:
-
不可恢复的错误:当程序遇到无法处理的错误,比如违反了程序的预期条件。
-
触发异常流程:
panic
触发了一个异常流程,这会导致当前 goroutine 停止执行,并开始执行栈展开。 -
与
recover
配合使用:panic
可以与recover
一起使用来实现错误恢复。recover
能够捕获 panic,并恢复程序的执行。 -
栈追踪:当
panic
发生时,Go 运行时会打印出栈追踪信息,这对于调试程序非常有帮助。 -
延迟函数(
defer
):在 panic 过程中,任何注册的延迟函数(使用defer
关键字注册的)都会被执行。 -
程序终止:如果程序中的
panic
没有被捕获和恢复,程序将终止执行。 -
使用场景:
panic
通常用于测试代码中,或者在初始化阶段检测到严重问题时。在正常的业务逻辑中,推荐使用错误返回值来处理错误情况。
例如下面的代码:
func someFunction() {
if someCondition {
panic("An unexpected condition occurred")
}
// ...
}
搭配recover
recover
是一个内置函数,只能在延迟函数中使用,并且只有在 panic 发生时才有效果。- 使用
recover
可以捕获 panic,并恢复程序执行,但通常只在调试或资源清理时使用。 - panic+recover,可以类比java的try catch finnaly。不同的是,go的recover只有发生panic才会触发,而java的finnaly是不管有没有异常都会触发。
defer func() { if r := recover(); r != nil { log.Printf("Recovered in someFunction, error: %v", r) } }()
- Go建议慎重使用panic, 而java的try catch使用非常广泛,有些程序员甚至不管三七二十一,在每个方法都加一个try catch,这只能说是一种“反模式”。