• 在Go中处理异常


    引言

    程序遇到的错误大致分为两类:程序员预料到的错误和程序员没有预料到的错误。我们在前两篇关于[错误处理]的文章中介绍的error接口主要处理我们在编写Go程序时预期的错误。error接口甚至允许我们承认函数调用发生错误的罕见可能性,因此我们可以在这些情况下进行适当的响应。

    异常属于第二类错误,是程序员没有预料到的。这些不可预见的错误会导致程序自动终止并退出正在运行的Go程序。常见的错误往往会造成异常。在本教程中,我们将研究Go中常见操作可能产生严重错误的几种方式,我们还将了解避免这些严重错误的方法。我们还将使用[defer]语句和recover函数来捕获错误,以免它们意外地终止我们正在运行的Go程序。

    理解异常

    Go中的某些操作会自动返回异常并停止程序。常见的操作包括超出[array]容量的索引、执行类型断言、调用nil指针的方法、错误地使用互斥量以及尝试使用闭合通道。这些情况大多是由编程时犯的错误导致的,而编译器在编译程序时无法检测到这些错误。

    由于异常包含对解决问题有用的细节,开发人员通常将异常用作在程序开发过程中犯了错误的指示。

    出界异常

    当您试图访问超出切片长度或数组容量的索引时,Go运行时将生成一个异常。

    下面的例子犯了一个常见的错误,即试图使用内置函数len返回的切片长度来访问切片的最后一个元素。试着运行这段代码,看看为什么会产生异常:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	names := []string{
    		"lobster",
    		"sea urchin",
    		"sea cucumber",
    	}
    	fmt.Println("My favorite sea creature is:", names[len(names)])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    Outputpanic: runtime error: index out of range [3] with length 3
    
    goroutine 1 [running]:
    main.main()
    	/tmp/sandbox879828148/prog.go:13 +0x20
    
    • 1
    • 2
    • 3
    • 4
    • 5

    异常的输出名称提供了一个提示:panic: runtime error: index out of range。我们制作了一个有三个海洋生物的切片。然后,我们尝试使用内置函数len将切片的长度作为索引来获取切片的最后一个元素。别忘了,切片和数组是从0开始的;所以这个切片的第一个元素是0,最后一个元素的索引是2。由于我们试图在第三个索引3处访问切片,因此切片中没有可以返回的元素,因为它超出了切片的边界。运行时别无选择,只能终止和退出,因为我们要求它做一些不可能的事情。Go也无法在编译期间证明此代码将尝试执行此操作,因此编译器无法捕获此操作。

    还要注意,后续的代码没有运行。这是因为异常是一个会完全停止Go程序执行的事件。生成的消息包含多种有助于诊断异常原因的信息。

    异常的剖析

    严重错误由指示严重错误原因的消息和堆栈跟踪组成,后者可以帮助您定位代码中产生严重错误的位置。

    任何异常的第一部分都是信息。它总是以字符串panic:开始,后面跟着一个根据异常原因而变化的字符串。上一个练习中的异常语句包含如下消息:

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

    panic:前缀后面的字符串runtime error:告诉我们,异常是由语言运行时生成的。这个错误告诉我们,我们试图使用的索引[3]超出了切片长度3的范围。

    此消息之后是堆栈跟踪。堆栈跟踪形成了一个map,我们可以根据它准确定位生成异常时正在执行的代码行,以及之前的代码如何调用该代码。

    goroutine 1 [running]:
    main.main()
    	/tmp/sandbox879828148/prog.go:13 +0x20
    
    • 1
    • 2
    • 3

    前面例子中的堆栈跟踪显示,我们的程序从第13行文件/tmp/sandbox879828148/prog.go中生成了异常。它还告诉我们,此异常是在main包中的main()函数中生成的。

    堆栈跟踪被分成多个独立的块——一个用于程序中的每个goroutine。每个Go程序的执行都是由一个或多个goroutines完成的,这些goroutines可以独立和同时执行Go代码的部分内容。每个块都以goroutine X [state]:开头。header给出了goroutine的ID号以及发生异常时它所处的状态。在header之后,堆栈跟踪显示了发生严重错误时程序正在执行的函数,以及该函数执行的文件名和行号。

    前面例子中的异常是由对切片的越界访问产生的。当对未设置的指针调用方法时,也可能产生异常。

    Nil接收器

    Go编程语言有指针,指向运行时存在于计算机内存中的某种类型的特定实例。指针可以假定值为nil,表示它们不指向任何东西。当我们试图在一个为nil的指针上调用方法时,Go运行时将生成一个异常。类似地,接口类型的变量在被方法调用时也会产生错误。要查看在这些情况下产生的严重错误,请尝试以下示例:

    package main
    
    import (
    	"fmt"
    )
    
    type Shark struct {
    	Name string
    }
    
    func (s *Shark) SayHello() {
    	fmt.Println("Hi! My name is", s.Name)
    }
    
    func main() {
    	s := &Shark{"Sammy"}
    	s = nil
    	s.SayHello()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    Outputpanic: runtime error: invalid memory address or nil pointer dereference
    [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba
    
    goroutine 1 [running]:
    main.(*Shark).SayHello(...)
    	/tmp/sandbox160713813/prog.go:12
    main.main()
    	/tmp/sandbox160713813/prog.go:18 +0x1a
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这个例子中,我们定义了一个名为Shark的结构体。Shark在它的指针接收器上定义了一个名为SayHello的方法,当被调用时,它会向标准输出打印一条问候语。在我们的main函数体内,我们创建了这个Shark结构体的一个新实例,并使用&操作符请求一个指向它的指针。这个指针被赋值给s变量。然后我们使用s = nil语句将s变量重新赋值为nil。最后,我们尝试在变量s上调用SayHello方法。我们没有收到来自Sammy的友好消息,而是收到了一个警告,表示我们试图访问一个无效的内存地址。因为s变量是nil,当调用SayHello函数时,它尝试访问*Shark类型的字段Name。因为这是一个指针接收器,在这个例子中接收器是nil,它无法解引nil指针,所以出现了问题。

    虽然我们在这个例子中显式地将s设置为nil,但实际上这种情况并不明显。当你看到涉及nil pointer dereference的严重错误时,请确保你已经正确地为任何你可能创建的指针变量赋值。

    由nil指针产生的严重错误和越界访问是运行时产生的两种常见严重错误。也可以使用内置函数手动生成异常。

    使用内置函数panic

    我们也可以使用内置函数panic来生成我们自己的异常。它接受一个字符串作为参数,即异常将产生的消息。通常,这条消息比重写代码返回错误要简洁。此外,我们可以在我们自己的包中使用它,以表明开发人员在使用我们的包的代码时可能犯了错误。只要有可能,最佳实践是尝试向我们的包的使用者返回error值。

    运行以下代码,查看从另一个函数调用的函数中生成的异常:

    package main
    
    func main() {
    	foo()
    }
    
    func foo() {
    	panic("oh no!")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    Outputpanic: oh no!
    
    goroutine 1 [running]:
    main.foo(...)
    	/tmp/sandbox494710869/prog.go:8
    main.main()
    	/tmp/sandbox494710869/prog.go:4 +0x40
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里,我们定义了一个函数foo,它使用字符串"oh no!"调用内置的panic。这个函数被我们的main函数调用。注意输出的信息panic: oh no!,堆栈跟踪显示了一个单独的goroutine,其中有两行代码:一行用于main()函数,另一行用于我们的foo()函数。

    我们已经看到,异常似乎会在产生它们的地方终止程序。当需要关闭开放资源时,这可能会产生问题。Go提供了一种始终执行某些代码的机制,即使在出现紧急情况时也是如此。

    延迟函数

    你的程序可能有必须正确清理的资源,即使在运行时处理异常时也是如此。Go允许您推迟函数调用的执行,直到调用它的函数完成执行。延迟函数即使在紧急情况下也会运行,它被用作一种安全机制,以防范紧急情况带来的混乱。通过像往常一样调用函数,然后用defer关键字前缀整个语句来延迟函数,就像defer sayHello()一样。运行下面的例子,看看在发生异常事件时如何打印消息:

    package main
    
    import "fmt"
    
    func main() {
    	defer func() {
    		fmt.Println("hello from the deferred function!")
    	}()
    
    	panic("oh no!")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    Outputhello from the deferred function!
    panic: oh no!
    
    goroutine 1 [running]:
    main.main()
    	/Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这个例子中的main函数中,我们首先defer了对一个匿名函数的调用,该匿名函数会打印出消息"hello from the deferred function!"。然后main函数立即使用panic函数产生一个异常。在这个程序的输出中,我们首先看到deferred函数被执行了,并打印了它的消息。接下来是我们在main中生成的异常。

    延迟函数可以防止意外情况的发生。在延迟函数中,Go还为我们提供了使用另一个内置函数阻止异常终止我们的Go程序的机会。

    处理异常

    异常只有一个恢复机制——内置函数recover。这个函数允许你在异常通过调用栈的过程中拦截它,防止它意外地终止你的程序。它有严格的使用规则,但在生产应用程序中可能是非常宝贵的。

    由于它是builtin包的一部分,因此可以在不导入任何其他包的情况下调用recover:

    package main
    
    import (
    	"fmt"
    	"log"
    )
    
    func main() {
    	divideByZero()
    	fmt.Println("we survived dividing by zero!")
    
    }
    
    func divideByZero() {
    	defer func() {
    		if err := recover(); err != nil {
    			log.Println("panic occurred:", err)
    		}
    	}()
    	fmt.Println(divide(1, 0))
    }
    
    func divide(a, b int) int {
    	return a / b
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
    we survived dividing by zero!
    
    • 1
    • 2

    在这个例子中,我们的main函数调用了我们定义的函数divideByZero。在这个函数中,我们defer调用一个匿名函数,该函数负责处理在执行divideByZero时可能出现的任何错误。在这个延迟匿名函数中,我们调用recover内置函数,并将它返回的错误赋值给一个变量。如果divideByZero处于异常状态,这个error值将被设置,否则它将是nil。通过比较err变量和nil变量,我们可以检测是否发生了异常,在这种情况下,我们使用log.Println函数记录异常,就像它是其他任何error一样。

    在这个延迟匿名函数之后,我们调用另一个我们定义的函数divide,并尝试使用fmt.Println打印它的结果。提供的参数将导致divide执行除数为0的运算,这将产生一个异常。

    在这个示例的输出中,我们首先看到恢复panic的匿名函数的日志消息,然后是消息 we survived dividing by zero!。我们确实做到了这一点,感谢内置函数recover阻止了将终止我们的Go程序的灾难性异常。

    recover()返回的err值正好是调用panic()时提供的值。因此,当异常没有发生时,确保err值仅为nil至关重要。

    recover检测异常

    recover函数依赖于错误的值来确定是否发生了严重错误。因为panic函数的参数是一个空接口,所以它可以是任何类型。任何接口类型的0值,包括空接口,都是nil。必须注意避免将nil作为panic的参数,如下例所示:

    package main
    
    import (
    	"fmt"
    	"log"
    )
    
    func main() {
    	divideByZero()
    	fmt.Println("we survived dividing by zero!")
    
    }
    
    func divideByZero() {
    	defer func() {
    		if err := recover(); err != nil {
    			log.Println("panic occurred:", err)
    		}
    	}()
    	fmt.Println(divide(1, 0))
    }
    
    func divide(a, b int) int {
    	if b == 0 {
    		panic(nil)
    	}
    	return a / b
    }
    
    ```go
    
    
    ```shell
    Outputwe survived dividing by zero!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这个例子与前面的例子相同,涉及到recover,只是做了一些细微的修改。divide函数被修改为检查它的除数b是否等于0。如果是,它将使用内置的panic并传入nil参数来生成一个异常。这一次的输出不包括显示发生了严重错误的日志消息,即使divide创建了一个严重错误。这种静默行为就是为什么确保panic内置函数的参数不是nil非常重要的原因。

    总结

    我们已经看到了Go中创建panic的多种方式,以及如何使用内置的recover恢复它们。虽然您自己可能不需要使用panic,但从panic中正确恢复是使Go应用程序可用于生产的重要步骤。

  • 相关阅读:
    VS 中的生成事件
    面向对象视角下,理解Docker 镜像容器和仓库
    安装Linux双系统后重启是这样是为什么呢(语言-python)
    chargpt: 用纯c 写一9*9数独程序
    (缺省参数)&(函数重载)&(引用)&(内敛)&(C++中的nullptr)
    Blender与Substance Painter从头到尾制作科幻飞行器视频教程
    ubuntu20.04“E: 软件包 vim 没有可安装候选”“/etc/apt/sources.list:7 中被配置了多次”解决方法
    NIO IN:技术蔚来的首次「大阅兵」
    广州华锐视点:VR3D技术在中学物理实验中的应用
    影刀sqlite的插入方法
  • 原文地址:https://blog.csdn.net/QIU176161650/article/details/133921293