Go 错误处理:用 panic 取代 err != nil 的模式

Go 语言的错误处理方式通常基于 err != nil 模式,这是 Go 语言的经典做法,但在某些场合,我们可以考虑使用 panic 来处理错误,尤其是在程序无法继续执行的严重错误发生时。虽然 Go 的设计哲学强调简洁与明确的错误处理,但并不排斥在适当情况下通过 panic 让程序"崩溃"并返回错误信息。本文将探讨如何在 Go 中使用 panic 取代常见的 err != nil 错误处理模式。

一、Go 中的错误处理

Go 的标准错误处理模式通常是通过返回一个 error 类型的值来指示操作是否成功。如果发生错误,通常会将错误值赋给变量 err,然后使用 if err != nil 来检查是否发生了错误。这种做法简洁且明确,但在一些极端情况下,使用 panic 可以让程序在发生不可恢复的错误时立即终止,从而避免进一步错误的积累或误操作。

简单来讲,就是在业务代码中使用 panic 的方式来替代 "永无止境" 的 if err != nil

我们一起来看看是怎么做,又有什么优缺点,互相学习一轮。

为什么想替换

在 Go 语言中 if err != nil 写的太多,还要管方法声明各种,嫌麻烦又不方便:

go 复制代码
err := foo()
if err != nil {
     //do something..
     return err
}

err := foo()
if err != nil {
     //do something..
     return err
}

err := foo()
if err != nil {
     //do something..
     return err
}

err := foo()
if err != nil {
     //do something..
     return err
}

上述还是示例代码,比较直面。若是在工程实践,还得各种 package 跳来跳去加 if err != nil,讲更繁琐,要去关心整体的上下游。

其余更具体的就不赘述了,可以关注我的公众号翻看先前的文章。

怎么替换 err != nil

不想写 if err != nil 的代码,方式之一就是用 panic 来替代他。

示例代码如下:

go 复制代码
func GetFish(db *sql.DB, name string) []string {
 rows, err := db.Query("select name from users where `name` = ?", name)
 if err != nil {
  panic(err)
 }
 defer rows.Close()

 var names []string
 for rows.Next() {
  var name string
  err := rows.Scan(&name)
  if err != nil {
   panic(err)
  }

  names = append(names, name)
 }

 err = rows.Err()
 if err != nil {
  panic(err)
 }

 return names
}

在上述业务代码中,我们通过 panic 的方式取代了 return err 的函数返回,自然其所关联的下游业务代码也就不需要编写 if err != nil 的代码:

css 复制代码
func main() {
 fish1 := GetFish(db, "煎鱼")
 fish2 := GetFish(db, "咸鱼")
 fish3 := GetFish(db, "摸鱼")
 ...
}

同时在转换为使用 panic 模式的错误机制后,我们必须要在外层增加 recover 方法:

go 复制代码
func AppRecovery() gin.HandlerFunc {
 return func(c *gin.Context) {
  defer func() {
   if err := recover(); err != nil {
    if _, ok := err.(AppErr); ok {
     // do something...
    } else {
     panic(err)
    }
   }
  }()
 }
}

每次 panic 后根据其抛出的错误进行断言,识别是否定制的 AppErr 错误类型,若是则可以进行一系列的处理动作。

否则可继续向上 panic 抛出给顶级的 Recovery 方法进行处理。

这就是一个相对完整的 panic 错误链路处理了。

优缺点

  • 从优点上来讲:
    • 整体代码结构看起来更加的简洁,仅专注于实现逻辑即可。
    • 不需要关注和编写冗杂的 if err != nil 的错误处理代码。
  • 从缺点上来讲:
    • 认知负担的增加,需要参加项目的每一个新老同学都清楚该模式,要做一个基本规范或培训。

    • 存在一定的性能开销,每次 panic 都存在用户态的上下文切换。

    • 存在一定的风险性,一旦 panic 没有 recover 住,就会导致事故。

    • Go 官方并不推荐,与 panic 本身的定义相违背,也就是 panicerror 的概念混淆。

三、使用 panic 的优缺点

优点:

  1. 简化代码 :在一些不需要错误恢复的场景中,使用 panic 可以使代码更加简洁,避免大量的 err != nil 检查,尤其是在初始化阶段。
  2. 清晰的错误处理 :对于无法恢复的错误,panic 提供了立即终止程序的方式,避免错误传播到应用程序的其他部分,导致更加复杂的错误。
  3. 调试友好panic 会打印堆栈信息,帮助开发者快速定位错误发生的位置,尤其适用于开发阶段的调试。

缺点:

  1. 不可恢复 :使用 panic 会立即中止程序,不能像常规错误处理那样提供机会让程序恢复。错误一旦发生,程序无法继续执行,这在某些应用场景中可能不可接受。
  2. 不符合 Go 错误处理哲学 :Go 的设计哲学强调明确的错误处理和程序的正常控制流。过度使用 panic 会使程序变得难以理解和维护,违背了 Go 的简洁与可读性原则。
  3. 性能开销 :虽然 panicrecover 是轻量级的,但它们仍然会引入一定的性能开销,尤其是在频繁使用的场合。

四、如何使用 panicrecover

Go 提供了 recover 函数,用于从 panic 中恢复程序的执行。recover 只能在 defer 函数中调用,因此它可以用于处理和恢复 panic,避免程序完全崩溃。

示例: 使用 panicrecover

go 复制代码
go
package main

import (
	"fmt"
)

func mayPanic() {
	panic("Something went wrong!")
}

func safeCall() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered from panic:", r)
		}
	}()
	mayPanic()
}

func main() {
	safeCall()
	fmt.Println("Program continues after recovery.")
}

在这个例子中,mayPanic 会触发 panic,但 safeCall 通过 deferrecover 恢复了程序的执行,使得程序不会崩溃并且能够继续执行后续的代码。

五、总结

Go 中的 panicerr != nil 各有其适用的场景。err != nil 适用于需要进一步处理的错误,而 panic 更适合用于那些无法恢复、必须立即终止程序的错误。选择使用 panic 还是 err != nil,需要根据错误的严重程度、应用的要求以及程序的设计来决定。在开发中,我们要确保在适当的场合使用 panic,同时避免过度依赖它,以免影响程序的可维护性和稳定性。

相关推荐
摸鱼的春哥1 分钟前
【编程】是什么编程思想,让老板对小伙怒飙英文?Are you OK?
前端·javascript·后端
尘世中一位迷途小书童1 分钟前
版本管理实战:Changeset 工作流完全指南(含中英文对照)
前端·面试·架构
吃饺子不吃馅11 分钟前
【八股汇总,背就完事】这一次再也不怕webpack面试了
前端·面试·webpack
逛逛GitHub40 分钟前
这个牛逼的股票市场平台,在 GitHub 上开源了。
前端·github
Max81241 分钟前
Agno Agent 服务端文件上传处理机制
后端
调试人生的显微镜1 小时前
苹果 App 怎么上架?从开发到发布的完整流程与使用 开心上架 跨平台上传
后端
顾漂亮1 小时前
Spring AOP 实战案例+避坑指南
java·后端·spring
间彧1 小时前
Redis Stream相比阻塞列表和发布订阅有哪些优势?适合什么场景?
后端
间彧1 小时前
Redis阻塞弹出和发布订阅模式有什么区别?各自适合什么场景?
后端
苏三说技术1 小时前
统计接口耗时的6种常见方法
后端