Go中Panic and Recover

这篇具有很好参考价值的文章主要介绍了Go中Panic and Recover。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

什么是Panic?

在 Go 程序中处理异常情况的惯用方法是使用errors.。errors足以应对程序中出现的大多数异常情况。

**但有些情况下,程序在出现异常情况后无法继续执行。在这种情况下,我们使用panic提前终止程序。当函数遇到恐慌时,其执行将停止,所有延迟的函数都会被执行,然后控制权返回给其调用者。这个过程一直持续到当前goroutine的所有函数都返回为止,此时程序将打印恐慌消息,然后打印堆栈跟踪,然后终止。**当我们编写示例程序时,这个概念会更加清晰。

**可以重新获得对发生恐慌的程序的控制,recover**我们将在本教程的后面讨论这一点。

可以认为panic和recover类似于Java等其他语言中的try-catch-finally习惯用法,只不过它们在Go中很少使用。

什么时候应该使用恐慌?

一个重要因素是您应该避免恐慌并尽可能恢复和使用错误。只有在程序无法继续执行的情况下才应该使用恐慌和恢复机制。

恐慌有两个有效的用例。

  1. 程序无法继续执行的不可恢复错误。 一个示例是 Web 服务器无法绑定到所需端口。在这种情况下,恐慌是合理的,因为如果端口绑定本身失败,就无事可做。
  2. 程序员错误。 假设我们有一个接受指针作为参数的方法,并且有人使用参数调用该方法nilnil在这种情况下,我们可能会感到恐慌,因为调用带有参数且期望有效指针的方法是程序员的错误。

恐慌的例子

panic下面提供了内置函数的签名,

func panic(interface{})

当程序终止时,传递给恐慌函数的参数将被打印。当我们编写示例程序时,它的用途就会很清楚。所以让我们立即这样做。

我们将从一个人为的例子开始,展示恐慌是如何运作的。

package main

import (
	"fmt"
)

func fullName(firstName *string, lastName *string) {
	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Printf("%s %s\n", *firstName, *lastName)
	fmt.Println("returned normally from fullName")
}

func main() {
	firstName := "Elon"
	fullName(&firstName, nil)
	fmt.Println("returned normally from main")
}

Run in playground

上面是一个打印一个人的全名的简单程序。第 7 行中的函数fullName。打印一个人的全名。该函数检查firstName和lastName指针是否nil。如果是nil函数调用则panic带有相应的消息。当程序终止时将打印此消息。

运行该程序将打印以下输出,

panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0x0?, 0xc00003e730?)
	/tmp/sandbox3307338859/prog.go:12 +0x106
main.main()
	/tmp/sandbox3307338859/prog.go:20 +0x2f

让我们分析这个输出,以了解恐慌是如何工作的以及程序恐慌时如何打印堆栈跟踪。

在19 行号中。我们分配ElonfirstName. 我们在第 20行中使用调用fullName函数。 因此第 11 行条件的会满足,程序会恐慌。当遇到恐慌时,程序执行终止,传递给恐慌函数的参数将被打印,然后是堆栈跟踪。由于程序在第12 行的紧急函数调用之后终止后面的不会被执行。

该程序首先打印传递给函数的消息panic

panic: runtime error: last name cannot be nil

然后打印堆栈跟踪。

该程序在第12 行出现恐慌。因此,

goroutine 1 [running]:
main.fullName(0xc00006af58, 0x0)
	/tmp/sandbox210590465/prog.go:12 +0x193

将首先打印。然后将打印堆栈中的下一项。在我们的例子中,在20行fullName调用的地方是堆栈跟踪中的下一项。因此接下来会打印它。

main.main()
	/tmp/sandbox210590465/prog.go:20 +0x4d

现在我们已经到达导致恐慌的顶级函数,并且上面没有更多级别,因此没有更多内容可打印。

再举一个例子

恐慌也可能是由运行时发生的错误引起的,例如尝试访问切片中不存在的索引。

让我们编写一个人为的示例,该示例会因越界切片访问而产生恐慌。

package main

import (
	"fmt"
)

func slicePanic() {
	n := []int{5, 7, 4}
	fmt.Println(n[4])
	fmt.Println("normally returned from a")
}
func main() {
	slicePanic()
	fmt.Println("normally returned from main")
}

Run in playground

在上面的程序中, 我们正在尝试访问切片n[4]中的无效索引。该程序将出现以下输出,

panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.slicePanic()
	/tmp/sandbox942516049/prog.go:9 +0x1d
main.main()
	/tmp/sandbox942516049/prog.go:13 +0x22

恐慌期间推迟通话

让我们回忆一下恐慌的作用。当函数遇到恐慌时,其执行将停止,所有延迟的函数都会被执行,然后控制权返回给其调用者。这个过程一直持续到当前 goroutine 的所有函数都返回为止,此时程序将打印恐慌消息,然后打印堆栈跟踪,然后终止。

在上面的示例中,我们没有推迟任何函数调用。如果存在延迟函数调用,则会执行该函数,然后将控制权返回给其调用者。

让我们稍微修改一下上面的示例并使用 defer 语句。

package main

import (
	"fmt"
)

func fullName(firstName *string, lastName *string) {
	defer fmt.Println("deferred call in fullName")
	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Printf("%s %s\n", *firstName, *lastName)
	fmt.Println("returned normally from fullName")
}

func main() {
	defer fmt.Println("deferred call in main")
	firstName := "Elon"
	fullName(&firstName, nil)
	fmt.Println("returned normally from main")
}

Run in playground

所做的唯一更改是在第 8 和 20行中添加了延迟函数调用。

该程序打印,

deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af28, 0x0)
	/tmp/sandbox451943841/prog.go:13 +0x23f
main.main()
	/tmp/sandbox451943841/prog.go:22 +0xc6

当程序在第13行发生恐慌时。首先执行任何延迟函数调用,然后控制权返回到执行延迟调用的调用者,依此类推,直到到达顶层调用者。

在我们的例子中,defer在第 8行的声明。首先执行fullName函数。这将打印以下消息。

deferred call in fullName

然后控制返回到main执行延迟调用的函数,因此打印:

deferred call in main

现在控件已到达顶层函数,因此程序会打印紧急消息,然后打印堆栈跟踪,然后终止。

从恐慌中恢复

恢复是一个内置函数,用于重新获得对发生恐慌的程序的控制。

下面提供了恢复函数的签名,

func recover() interface{}

仅当在延迟函数内部调用时恢复才有用。在延迟函数内执行恢复调用可以通过恢复正常执行来停止恐慌序列,并检索传递给恐慌函数调用的错误消息。如果在延迟函数之外调用恢复,它不会停止恐慌序列。

让我们修改我们的程序并使用recover在panic之后恢复正常执行。

package main

import (
	"fmt"
)

func recoverFullName() {
	if r := recover(); r != nil {
		fmt.Println("recovered from ", r)
	}
}

func fullName(firstName *string, lastName *string) {
	defer recoverFullName()
	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Printf("%s %s\n", *firstName, *lastName)
	fmt.Println("returned normally from fullName")
}

func main() {
	defer fmt.Println("deferred call in main")
	firstName := "Elon"
	fullName(&firstName, nil)
	fmt.Println("returned normally from main")
}

Run in playground

当发生恐慌时,将调用fullName延迟函数来停止恐慌

该程序将打印,

recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main

当程序在第 19行发生恐慌时,延迟recoverFullName函数被调用,该函数又调用recover()以重新获得对恐慌序列的控制。呼叫行recover() 返回传递给的参数panic()并因此打印,

recovered from  runtime error: last name cannot be nil

执行后recover(),恐慌停止,控制权返回给调用者,在本例中为main函数。main由于恐慌已恢复,程序从第 29 行开始继续正常执行。它打印returned normally from main 后跟deferred call in main

让我们再看一个示例,其中我们从由于访问切片的无效索引而引起的恐慌中恢复。

package main

import (
	"fmt"
)

func recoverInvalidAccess() {
	if r := recover(); r != nil {
		fmt.Println("Recovered", r)
	}
}

func invalidSliceAccess() {
	defer recoverInvalidAccess()
	n := []int{5, 7, 4}
	fmt.Println(n[4])
	fmt.Println("normally returned from a")
}

func main() {
	invalidSliceAccess()
	fmt.Println("normally returned from main")
}

Run in playground

运行上面的程序将输出,

Recovered runtime error: index out of range [4] with length 3
normally returned from main

从输出中,您可以了解到我们已经从恐慌中恢复过来。

恢复后获取堆栈跟踪

如果我们从恐慌中恢复,我们就会丢失有关恐慌的堆栈跟踪。即使在恢复后的上面的程序中,我们也丢失了堆栈跟踪。

有一种方法可以使用Debug包的PrintStack函数打印堆栈跟踪

package main

import (
	"fmt"
	"runtime/debug"
)

func recoverFullName() {
	if r := recover(); r != nil {
		fmt.Println("recovered from ", r)
		debug.PrintStack()
	}
}

func fullName(firstName *string, lastName *string) {
	defer recoverFullName()
	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Printf("%s %s\n", *firstName, *lastName)
	fmt.Println("returned normally from fullName")
}

func main() {
	defer fmt.Println("deferred call in main")
	firstName := "Elon"
	fullName(&firstName, nil)
	fmt.Println("returned normally from main")
}

Run in playground

在上面的程序中,我们使用debug.PrintStack()在第 11 行来打印堆栈跟踪。

该程序将打印,

recovered from  runtime error: last name cannot be nil
goroutine 1 [running]:
runtime/debug.Stack(0x37, 0x0, 0x0)
	/usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
	/usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x22
main.recoverFullName()
	/tmp/sandbox771195810/prog.go:11 +0xb4
panic(0x4a1b60, 0x4dc300)
	/usr/local/go-faketime/src/runtime/panic.go:969 +0x166
main.fullName(0xc0000a2f28, 0x0)
	/tmp/sandbox771195810/prog.go:21 +0x1cb
main.main()
	/tmp/sandbox771195810/prog.go:30 +0xc6
returned normally from main
deferred call in main

从输出中,您可以了解到恐慌已恢复并被recovered from runtime error: last name cannot be nil打印。接下来,打印堆栈跟踪。然后恐慌恢复后打印

returned normally from main
deferred call in main

恐慌、恢复和 Goroutine

仅当从同一个发生恐慌的goroutine调用时,Recover 才起作用。**不可能从不同 goroutine 中发生的恐慌中恢复。**让我们通过一个例子来理解这一点。

package main

import (
	"fmt"
)

func recovery() {
	if r := recover(); r != nil {
		fmt.Println("recovered:", r)
	}
}

func sum(a int, b int) {
	defer recovery()
	fmt.Printf("%d + %d = %d\n", a, b, a+b)
	done := make(chan bool)
	go divide(a, b, done)
	<-done
}

func divide(a int, b int, done chan bool) {
	fmt.Printf("%d / %d = %d", a, b, a/b)
	done <- true

}

func main() {
	sum(5, 0)
	fmt.Println("normally returned from main")
}

Run in playground

在上面的程序中,该函数divide()将在第 22 行发生恐慌。因为 b 为零并且不可能将数字除以零。该sum()函数调用一个延迟函数recovery(),用于从恐慌中恢复。该函数divide()在第 1 7行作为单独的 goroutine 被调用。 我们在18行号的done通道上等待。确保divide()完成执行。

你认为该程序的输出是什么?恐慌情绪会恢复吗?答案是不。恐慌将无法恢复。这是因为该recovery函数存在于不同的 goroutine 中,并且恐慌发生在divide()不同 goroutine 中的函数中。因此不可能恢复。

运行该程序将打印,

5 + 0 = 5
panic: runtime error: integer divide by zero

goroutine 18 [running]:
main.divide(0x5, 0x0, 0xc0000a2000)
	/tmp/sandbox877118715/prog.go:22 +0x167
created by main.sum
	/tmp/sandbox877118715/prog.go:17 +0x1a9

您可以从输出中看到恢复尚未发生。

如果该divide()函数在同一个 goroutine 中调用,我们就会从恐慌中恢复过来。

如果17 行号程序修改为

go divide(a, b, done)

divide(a, b, done)

由于恐慌发生在同一个 goroutine 中,因此恢复将会发生。如果程序在进行上述更改后运行,它将打印文章来源地址https://www.toymoban.com/news/detail-741712.html

5 + 0 = 5
recovered: runtime error: integer divide by zero
normally returned from main

到了这里,关于Go中Panic and Recover的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Golang】Golang进阶系列教程--Go 语言 map 如何顺序读取?

    Go 语言中的 map 是一种非常强大的数据结构,它允许我们快速地存储和检索键值对。 然而,当我们遍历 map 时,会有一个有趣的现象,那就是输出的键值对顺序是不确定的。 先看一段代码示例: 当我们多执行几次这段代码时,就会发现,输出的顺序是不同的。 首先,Go 语言

    2024年02月14日
    浏览(69)
  • 【Golang】Golang进阶系列教程--Go 语言数组和切片的区别

    在 Go 语言中,数组和切片看起来很像,但其实它们又有很多的不同之处,这篇文章就来说说它们到底有哪些不同。 数组和切片是两个常用的数据结构。它们都可以用于存储一组相同类型的元素,但在底层实现和使用方式上存在一些重要的区别。 Go 中数组的长度是不可改变的

    2024年02月15日
    浏览(61)
  • 【Golang】Golang进阶系列教程--Go 语言切片是如何扩容的?

    在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。 切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。 切片是一种引用类型,它有三个属性:指针,长度和容量。 底层源码定义如下: 指针: 指向

    2024年02月14日
    浏览(70)
  • 【GoLang入门教程】Go语言工程结构详述

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:人工智能 前言 当创建一个Go语言项目时,良好的工程结构是确保项目可维护性、可扩展性和清晰性的关键。 虽然Go本身没有强制性的项目结构要求,但是采用一致性

    2024年01月24日
    浏览(80)
  • 【Golang入门教程】Go语言变量的初始化

    强烈推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站: 人工智能 推荐一个个人工作,日常中比较常用的人工智能工具,无需魔法,忍不住分享一下给大家。点击跳转到网站: 人工智能工具 引言 在Go语言中,变量

    2024年04月17日
    浏览(79)
  • 【Golang】Golang进阶系列教程--为什么 Go 语言 struct 要使用 tags

    在 Go 语言中,struct 是一种常见的数据类型,它可以用来表示复杂的数据结构。在 struct 中,我们可以定义多个字段,每个字段可以有不同的类型和名称。 除了这些基本信息之外,Go 还提供了 struct tags,它可以用来指定 struct 中每个字段的元信息。 在本文中,我们将探讨为什

    2024年02月15日
    浏览(80)
  • 【Golang】go编程语言适合哪些项目开发?

    前言 在当今数字化时代,软件开发已成为各行各业的核心需求之一。 而选择适合的编程语言对于项目的成功开发至关重要。 本文将重点探讨Go编程语言适合哪些项目开发,以帮助读者在选择合适的编程语言时做出明智的决策。 Go 编程语言适合哪些项目开发? Go是由Google开发

    2024年02月04日
    浏览(80)
  • 【Go语言】Golang保姆级入门教程 Go初学者chapter2

    setting的首选项 一个程序就是一个世界 变量是程序的基本组成单位 变量的使用步骤 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zuxG8imp-1691479164956)(https://cdn.staticaly.com/gh/hudiework/img@main/image-20230726152905139.png)] 变量表示内存中的一个存储区 注意:

    2024年02月14日
    浏览(129)
  • 【Go语言】Golang保姆级入门教程 Go初学者chapter3

    下划线“_”本身在Go中一个特殊的标识符,成为空标识符。可以代表任何其他的标识符,但是他对应的值就会被忽略 仅仅被作为站维度使用, 不能作为标识符使用 因为Go语言中没有private public 所以标记变量首字母大写代表其他包可以使用 小写就是不可使用的 注意:Go语言中

    2024年02月13日
    浏览(62)
  • 【GoLang入门教程】Go语言几种标准库介绍(五)

    前言 上一篇,我们介绍了fmt、Go、html 三个库,这篇我们继续介绍剩下的库 几种库 image库 (常见图形格式的访问及生成) 在 Go 语言的标准库中, image 包提供了对图像的基本操作和处理功能。 这个包定义了 Image 接口和一些基本的图像类型,同时也包含了一些实现了该接口的具

    2024年02月03日
    浏览(73)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包