Go 语言中的异常处理简单实践 panic、recover【GO 基础】

go,panic,recover · 浏览次数 : 0

小编点评

Go 语言中并没有像其他语言那样的结构化异常处理机制。当遇到错误时,Go 语言通常使用 `panic` 函数来抛出一个错误,然后在 `defer` 块中通过 `recover` 函数来捕获这个错误。这种处理方式适用于处理那些无法恢复的运行时错误。 ### panic 和 recover - `panic` 函数用于触发一个运行时错误,导致当前函数立即停止执行,并开始回溯调用栈。 - `recover` 函数用于捕获一个之前抛出的 `panic`,如果捕获到,它将返回对应的错误值;如果没有捕获到,`recover` 会返回 `nil`。 ### 示例 下面是一个简单的示例,演示了如何使用 `panic` 和 `recover` 来处理一个除以零的错误: ```go package main import ( "fmt" ) func main() { defer func() { if err := recover(); err != nil { fmt.Println("输出:", err.(string)) } }() div(10, 0) } func div(x, y int) (int, error) { if y == 0 { return 0, fmt.Errorf("division by zero") } return x / y, nil } ``` 在这个例子中,当我们试图除以零时,`div` 函数会抛出一个 `error`,然后 `main` 函数中的 `defer` 块会捕获这个错误,并打印出错误信息。 ### 注意事项 - 不要随意使用 `panic` 处理非严重错误,因为这会导致程序崩溃。 - 对于可以处理的错误,应该通过返回错误值的方式来传递给调用者。 - Go 语言推荐使用错误返回码而非异常机制来处理错误,这样可以避免破坏程序的执行流程。 - 在实际开发中,建议优先使用错误处理机制,谨慎使用 `panic` 和 `recover`,以编写出更加稳定和高效的 Go 程序。

正文

〇、Go 中的异常处理简介

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误

panic、recover 参数类型为 interface{},因此可抛出任何类型对象。

func panic(v interface{})
func recover() interface{}

处理流程:方法体重抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

关于 panic:

  触发运行时错误:panic 用于立即停止当前函数的执行,并开始回溯调用栈直到程序终止或遇到 recover。
  传递错误信息:panic 可以接受任何类型的参数,通常传递字符串或错误接口实例,方便错误信息的传递和处理。
  易错点:随意使用 panic 处理非严重错误是不推荐的,其主要用于处理不可恢复的运行时错误,对于可处理的错误,应通过返回错误值的方式传递给调用者。

关于 recover:

  捕获 panic:recover 只能在 defer 语句中调用,用于捕获当前 goroutine 发生的 panic,如果没有 panic 发生,则返回 nil。
  recover 处理异常后:逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  易错点:recover 只能捕获同一 goroutine 内发生的 panic,对于其他 goroutine 引发的 panic 无能为力。

Go 语言推荐使用错误返回码而非异常机制来处理错误,通过 error 接口返回错误信息,这是一种更灵活且不会破坏程序执行流程的方法。在实际开发中,建议优先使用错误处理机制,谨慎使用 panic 和 recover,以编写出更加稳定和高效的 Go 程序。

一、异常捕获简单测试

1.1 简单的捕获 panic

如下代码,直接触发 panic,在 defer 中通过 recover 捕获,并转换成 string 输出:

package main

func main() {
	test()
}

func test() {
	defer func() {
		if err := recover(); err != nil {
			println("输出:", err.(string)) // 将 interface{} 转型为具体类型 string
			// 输出: panic error!
		}
	}()
	panic("panic error!")
}

再来个示例,往已关闭的通道中发送数据,会引发异常:

package main

import (
	"fmt"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err) // 输出:send on closed channel
		}
	}()

	var ch chan int = make(chan int, 10)
	close(ch) // 关闭通道
	ch <- 1   // 继续往通道中发送值,就会引发异常
}

1.2 多个异常时 recover 如何捕获?

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

如下代码,只有 defer 中的 panic 会被捕获,另一个异常将会漏掉:

package main

import "fmt"

func test() {
	defer func() {
		fmt.Println(recover())
	}()
	defer func() {
		panic("defer panic")
	}()
	panic("test panic")
}

func main() {
	test()
}
// 输出:
// defer panic

因此,需要再第二个 defer 中针对 test panic 进行处理。

1.3 一个异常在多层 defer 如何捕获?

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。

如下代码,在第一层进行了延迟调用,然后第〇层就未获取到 panic:

package main

import "fmt"

func test() {
	defer func() {
		fmt.Println("第〇层:", recover()) // 无效
	}()
	defer func() {
		fmt.Println("第一层:", recover()) // 有效
	}()
	defer fmt.Println("第二层:", recover()) // 无效!
	defer fmt.Println("第三层:", recover()) // 无效!
	defer func() {
		func() {
			println("第四层:defer inner")
			fmt.Println("第四层:", recover()) // 无效!
		}()
	}()
	panic("test panic")
}

func main() {
	test()
}
// 输出:
// 第四层:defer inner
// 第四层: <nil>
// 第三层: <nil>
// 第二层: <nil>
// 第一层: test panic
// 第〇层: <nil>

1.4 通过 error 类型的错误对象来表示函数调用状态

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

type error interface {
    Error() string
}

标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

package main

import (
	"errors"
	"fmt"
)

var ErrDivByZero = errors.New("division by zero") // 定义错误类型 ErrDivByZero

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, ErrDivByZero
	}
	return x / y, nil
}

func main() {
	defer func() {
		fmt.Println(recover()) // 捕获 panic,无 panic 就打印 <nil>
	}()
	switch z, err := div(10, 0); err { // div(10, 0) 返回 ErrDivByZero
	case nil:
		println(z)
	case ErrDivByZero: // 触发 panic
		panic(err)
	}
}

// 输出:
// division by zero

1.5 通过将代码块重构成匿名函数来实现 try-catch 的效果

将代码块重构成匿名函数,并包含异常处理,如此可确保后续代码被执行

如下代码,当被除数为 0 时会报错,在匿名函数中被捕获并记录,然后不影响正常输出:

package main

import "fmt"

func test(x, y int) {
	var z int
	func() {
		defer func() {
			if err := recover(); err != nil {
				fmt.Println("err:", err)
				z = 0
			}
		}()
		z = x / y
	}()
	fmt.Printf("x / y = %d\n", z)
}

func main() {
	test(2, 0)
}
// 输出:
// err: runtime error: integer divide by zero
// x / y = 0

1.6 实现类似 try-catch 的异常处理

package main

import "fmt"

func Try(fun func(), handler func(interface{})) {
	defer func() {
		if err := recover(); err != nil {
			handler(err) // 捕获异常后,执行异常处理逻辑
		}
	}()
	fun() // 直接执行处理逻辑
}

func main() {
	Try(
		func() {
			// 程序处理逻辑。。。
			panic("test panic") // 抛出异常
		},
		func(err interface{}) {
			// 异常处理逻辑。。。
			fmt.Println(err)
		},
	)
}

参考:http://www.topgoer.com/%E5%87%BD%E6%95%B0/%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86.html

与Go 语言中的异常处理简单实践 panic、recover【GO 基础】相似的内容:

Go 语言中的异常处理简单实践 panic、recover【GO 基础】

〇、Go 中的异常处理简介 Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。 func panic(v interface{}) func recover() interfa

spannerlib优雅的go异常处理

蹩脚的go 异常处理 一般写go的人,如果他不是写算法,正常写业务代码的话,可能都会为优雅的异常处理而烦恼,因为脑子抽筋的go设计者们,总是感觉语法糖是一种很低级的东西。但是在我们大多数公司的业务逻辑中,没有语法糖让代码非常丑陋,不易于维护。 如何让go 代码更具有可读性,哪么就要给go加糖! 引入

Vue - 入门

零:前端目前形势 前端的发展史 HTML(5)、CSS(3)、JavaScript(ES5、ES6):编写一个个的页面 -> 给后端(PHP、Python、Go、Java) -> 后端嵌入模板语法 -> 后端渲染完数据 -> 返回数据给前端 -> 在浏览器中查看 Ajax的出现 -> 后台发送异步请

golang reflect 反射机制的使用场景

Go语言中的 reflect 包提供了运行时反射机制,允许程序在运行时检查和操作任意对象的数据类型和值。 以下是 reflect 包的一些典型使用场景: 1. 动态类型判断与转换:当需要处理多种类型的变量且具体类型直到运行时才能确定时,可以使用反射来检查变量的实际类型,并在可能的情况下进行类型转换。

golang 泛型的格式写法

Go语言中的泛型(Generics)是在 Go 1.18 版本中引入的一个重要特性,它允许你编写可重用的代码,而不需要为每种数据类型重复编写相同的逻辑。 泛型通过参数化类型(type parameters)来实现,使得函数、方法、接口和结构体可以与多种类型一起工作。 下面详细介绍Go语言中泛型的基本

Go运算操作符全解与实战:编写更高效的代码!

本文全面探讨了Go语言中的各类运算操作符,从基础的数学和位运算到逻辑和特殊运算符。文章旨在深入解析每一种运算操作符的工作原理、应用场景和注意事项,以帮助开发者编写更高效、健壮和可读的Go代码。 简介 Go语言,作为一种现代的编程语言,不仅因为其简单易读的语法而受到欢迎,还因为它的性能和高度并发能力在

Go代码包与引入:如何有效组织您的项目

本文深入探讨了Go语言中的代码包和包引入机制,从基础概念到高级应用一一剖析。文章详细讲解了如何创建、组织和管理代码包,以及包引入的多种使用场景和最佳实践。通过阅读本文,开发者将获得全面而深入的理解,进一步提升Go开发的效率和质量。 关注公众号【TechLeadCloud】,分享互联网架构、云服务技术

Go之流程控制大全: 细节、示例与最佳实践

本文深入探讨Go语言中的流程控制语法,包括基本的if-else条件分支、for循环、switch-case多条件分支,以及与特定数据类型相关的流程控制,如for-range循环和type-switch。文章还详细描述了goto、fallthrough等跳转语句的使用方法,通过清晰的代码示例为读者提供

Go语言关键字解析:深入了解Go语言中的关键字

> 摘要:本文由葡萄城技术团队于博客园原创并首发。转载请注明出处:[葡萄城官网](https://www.grapecity.com.cn/),葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 # 前言 为了更加深入地介绍Go语言以及与C\#语言的比较,本文将会从多个维度出发进行详细的

Go接口 - 构建可扩展Go应用

本文深入探讨了Go语言中接口的概念和实际应用场景。从基础知识如接口的定义和实现,到更复杂的实战应用如解耦与抽象、多态、错误处理、插件架构以及资源管理,文章通过丰富的代码示例和详细的解释,展示了Go接口在软件开发中的强大功能和灵活性。 关注【TechLeadCloud】,分享互联网架构、云服务技术的全