程序遇到的错误大致分为两类:程序员预料到的错误和程序员没有预料到的错误。我们在前两篇关于[错误处理]的文章中介绍的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)])
}
Outputpanic: runtime error: index out of range [3] with length 3
goroutine 1 [running]:
main.main()
/tmp/sandbox879828148/prog.go:13 +0x20
异常的输出名称提供了一个提示: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
panic:
前缀后面的字符串runtime error:
告诉我们,异常是由语言运行时生成的。这个错误告诉我们,我们试图使用的索引[3]
超出了切片长度3
的范围。
此消息之后是堆栈跟踪。堆栈跟踪形成了一个map,我们可以根据它准确定位生成异常时正在执行的代码行,以及之前的代码如何调用该代码。
goroutine 1 [running]:
main.main()
/tmp/sandbox879828148/prog.go:13 +0x20
前面例子中的堆栈跟踪显示,我们的程序从第13行文件/tmp/sandbox879828148/prog.go
中生成了异常。它还告诉我们,此异常是在main
包中的main()
函数中生成的。
堆栈跟踪被分成多个独立的块——一个用于程序中的每个goroutine。每个Go程序的执行都是由一个或多个goroutines完成的,这些goroutines可以独立和同时执行Go代码的部分内容。每个块都以goroutine X [state]:
开头。header给出了goroutine的ID号以及发生异常时它所处的状态。在header之后,堆栈跟踪显示了发生严重错误时程序正在执行的函数,以及该函数执行的文件名和行号。
前面例子中的异常是由对切片的越界访问产生的。当对未设置的指针调用方法时,也可能产生异常。
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()
}
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
在这个例子中,我们定义了一个名为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!")
}
Outputpanic: oh no!
goroutine 1 [running]:
main.foo(...)
/tmp/sandbox494710869/prog.go:8
main.main()
/tmp/sandbox494710869/prog.go:4 +0x40
在这里,我们定义了一个函数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!")
}
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
在这个例子中的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
}
Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!
在这个例子中,我们的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!
这个例子与前面的例子相同,涉及到recover
,只是做了一些细微的修改。divide
函数被修改为检查它的除数b
是否等于0
。如果是,它将使用内置的panic
并传入nil
参数来生成一个异常。这一次的输出不包括显示发生了严重错误的日志消息,即使divide
创建了一个严重错误。这种静默行为就是为什么确保panic
内置函数的参数不是nil
非常重要的原因。
我们已经看到了Go中创建panic
的多种方式,以及如何使用内置的recover
恢复它们。虽然您自己可能不需要使用panic
,但从panic中正确恢复是使Go应用程序可用于生产的重要步骤。