每个程序都会遇到错误,有时处理错误可以像报告错误并退出程序一样简单,但其他错误可能需要额外的操作,比如需要关闭打开的文件或网络链接,或者以其他方式清理,这样程序不会混乱。本篇将展示如何延迟清理操作,以便在出现错误时也能执行这些操作;还将展示如何在适当的情况下使程序出现panic,以及如何在事后恢复。
从文件中读取数字,重新访问
package main
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
)
func OpenFile(fileName string) (*os.File, error) {
fmt.Println("Opening", fileName)
return os.Open(fileName) //打开文件并返回指向该文件的指针,以及遇到的任何错误
}
func CloseFile(file *os.File) {
fmt.Println("Closing file")
file.Close() //关闭文件
}
func GetFloats(fileName string) ([]float64, error) {
var numbers []float64
file, err := OpenFile(fileName) //不是直接调用os.Open,而是调用OpenFile
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
number, err := strconv.ParseFloat(scanner.Text(), 64)
if err != nil {
return nil, err
}
numbers = append(numbers, number)
}
CloseFile(file) //不是直接调用file.Close,而是调用CloseFile
if scanner.Err() != nil {
return nil, scanner.Err()
}
return numbers, nil
}
func main() {
numbers, err := GetFloats(os.Args[1]) //存储从文件中读取的数字切片以及任何错误,使用第一个命令行参数作为文件名
if err != nil { //如果有错误,记录并退出
log.Fatal(err)
}
var sum float64 = 0
for _, number := range numbers { //把切片中的所有数字加起来
sum += number
}
fmt.Printf("Sum: %0.2f\n", sum) //打印总和
}
我们希望将要读取的文件名称指定为命令行参数,因此在main函数中,通过访问 os.Args[1]
从第一个命令行参数获取要打开文件的名称。(os.Args[0]
元素是正在运行的程序的名称,实际的程序参数出现在os.Args[1]和后面的元素中)。
然后将文件名传递给GetFloats来读取文件,并得到一个返回float64值的切片。如果在这个过程中遇到任何错误,它们将从GetFloats函数返回,将把它们存储在err变量中,若err不是nil,意味着有错误,只需记录并退出。否则意味着文件被成功读取,使用for循环将切片中的每个值相加,并打印总和。
data.txt:
1.25
8.75
5.0
10.5
15.5
执行:
# go run sum.go data.txt
Opening 123.txt
Closing file
Sum: 41.00
任何错误都阻止关闭文件
但是如果提供了一个格式不正确的文件,就会出现问题,比如文件的行不能解析为float64。
20.25
hello //无法解析为float64
123.11
这是正常的,因为每个程序都会遇到接收无效数据的情况,但是GetFloats函数在完成后,调用CloseFile函数,实际上却没有调用。
当对无法转换为float64的字符串调用strconv.ParseFloat时,返回一个错误,我们的代码被设置为在此时从GetFloats函数返回,但是这个返回发生在调用CloseFile之前,这意味着文件永远不会被关闭。
func GetFloats(fileName string) ([]float64, error) {
var numbers []float64
file, err := OpenFile(fileName)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
number, err := strconv.ParseFloat(scanner.Text(), 64) //当不能将文本行转换为float64时,ParseFloat返回一个错误,这回导致GetFloats返回一个错误
if err != nil {
return nil, err
}
numbers = append(numbers, number)
}
CloseFile(file) //CloseFile永远不会被调用
if scanner.Err() != nil {
return nil, scanner.Err()
}
return numbers, nil
}
延迟函数调用
关闭文件失败似乎没什么大不了,对于一个只打开一个文件的简单程序来说,可能没问题,但是每个打开的文件都会消耗操作系统的资源,打开文件越多可能会累积并导致程序失败,甚至影响操作系统。
如果有一个无论如何都希望运行的函数调用,可以使用defer
语句。可以将defer关键字放在任何普通函数或方法调用之前
,Go将延迟执行函数调用,直到当前函数退出之后。
通常来说,函数调用一遇到就立即执行:
package main
import "fmt"
func Socialize() {
fmt.Println("Goodbye!")
fmt.Println("Hello!")
fmt.Println("Nice!")
}
func main() {
Socialize()
}
但是,如果在调用fmt.Println(“Goodbye!”) 之前添加defer关键字,则在Socialize函数中的所有剩余代码运行之前以及Socialize退出之前,该调用不会执行。
package main
import "fmt"
func Socialize() {
defer fmt.Println("Goodbye!") //在函数调用之前添加defer关键字
fmt.Println("Hello!")
fmt.Println("Nice!")
}
func main() {
Socialize()
}
输出:
Hello!
Nice!
Goodbye! //第一个函数调用被推迟到Socialize退出之后
使用延迟函数调用从错误中恢复
defer 用于“无论如何”都需要发生的函数调用: defer 关键字通过使用return 关键字确保函数调用发生,即使调用函数提前退出。
package main
import (
"fmt"
"log"
)
func Socialize() error {
defer fmt.Println("Goodbye!")
fmt.Println("Hello!")
return fmt.Errorf("I don't want to talk.")
fmt.Println("Nice!") //不会运行
return nil //不会运行
}
func main() {
err := Socialize()
if err != nil {
log.Fatal(err)
}
}
输出:
Hello!
Goodbye!
2023/08/18 10:38:37 I don't want to talk.
defer 关键字确保函数调用发生,即使调用函数提前退出。
当return语句被执行时:
1.return语句首先计算其返回值(如果有的话),并保存在内部变量中;
2.执行所有defer语句;
3.实际返回已保存的返回值,并退出函数;
在Socialize函数中的return语句被执行时,首先保存了要返回的错误值,然后执行defer,最后再执行return返回错误值,退出函数。
先执行defer,最后执行return
使用延迟函数调用确保文件关闭
因为defer关键字可以确保“无论如何”都执行函数调用,所以通常用于需要运行的代码,即使在出现错误的情况下也是如此,比如在文件打开后关闭它们。
func GetFloats(fileName string) ([]float64, error) {
var numbers []float64
file, err := OpenFile(fileName) //不是直接调用os.Open,而是调用OpenFile
if err != nil {
return nil, err
}
defer CloseFile(file) //添加defer,这样即使函数退出后也会执行,并将其移动到OpenFile之后
scanner := bufio.NewScanner(file)
for scanner.Scan() {
number, err := strconv.ParseFloat(scanner.Text(), 64)
if err != nil {
return nil, err //即使返回错误,CloseFile仍然会被调用
}
numbers = append(numbers, number)
}
if scanner.Err() != nil {
return nil, scanner.Err() //如果函数正常完成,就会调用CloseFile
}
return numbers, nil
}
使用defer可以确保在GetFloats退出时调用CloseFile,不管它是正常完成还是解析文件出错。
问:是否可以延迟其他语句,比如for循环或者变量赋值?
答:不可以,只能延迟函数和方法调用,可以编写一个函数或方法,然后延迟对函数或方法的调用,但是defer关键字本身只能与函数或方法调用一起使用。
列出目录中的文件
Go还有一些特性可以帮助处理错误。
创建一个名为my_directory的内容,指出它包含的每个项的名称,以及它是文件还是子目录。
├── my_directory
│ ├── a.txt
│ ├── subdir
│ └── z.txt
io/ioutil 包 包含一个ReadDir函数,允许读取目录内容,向ReadDir传递一个目录的名称,将返回一个值切片,每个值切片对应目录包含的每个文件或子目录(以及遇到的任何错误)。
每个切片的值都满足FileInfo接口,该接口包括一个返回文件名的Name方法和一个如果是目录则返回true的IsDir方法。
因此,程序调用ReadDir,将my_directory的名称作为参数传递给它,然后循环遍历返回的切片中的每个值,如果IsDir返回值为true,将打印“Directory: ” 和文件名,否则将打印“File: ” 和文件名。
package main
import (
"fmt"
"io/ioutil"
"log"
)
func main() {
files, err := ioutil.ReadDir("my_directory") //获取一个包含代表"my_directory"的内容的值的切片
if err != nil {
log.Fatal(err)
}
for _, file := range files { //对于切片中的每个文件。。。
if file.IsDir() { //如果文件是一个目录...
fmt.Println("Directory:", file.Name())
} else { //否则,打印...
fmt.Println("File:", file.Name())
}
}
}
输出:
File: a.txt
Directory: subdir
File: z.txt
程序运行并生成my_directory所包含的文件和目录的列表。
读取单个目录内容的程序不复杂,但是若想列出更复杂的内容,比如Go工作区目录,它将包含嵌套在子目录中的整个子目录树,有些包含文件,有些不包含。
├── go
│ └── src
│ ├── geo
│ │ ├── coordinates.go
│ │ └── landmark.go
│ ├── locked
│ │ └── secret.go
│ └── vehicle
│ └── car.go
递归函数调用
递归允许函数调用自身。
若不小心,会得到一个无限循环,函数会不停调用自己:
package main
import "fmt"
func recurses() {
fmt.Println("Oh, no, I'm stuck!")
recurses() //函数调用自己
}
func main() {
recurses()
}
输出:
Oh, no, I'm stuck!
Oh, no, I'm stuck!
Oh, no, I'm stuck!
Oh, no, ^Csignal: interrupt
但是如果能确保递归循环能停止,其实递归是有用的。
这是一个递归count函数,从第一个数到最后一个数进行计数(通常来说循环更有效,这里仅演示递归工作原理)。
package main
import "fmt"
func count(start int, end int) {
fmt.Println(start) //打印当前的起始数
if start < end { //如果还没有达到结束数字
count(start+1, end) //“count”函数使用比之前多1的起始数调用自身
}
}
func main() {
count(1, 3) //第一次调用“count”,指定它应该从1到3计数
}
程序的顺序:
1.main使用起始(start)参数1和结束(end)参数3调用count;
2.count打印起始参数1;
3.start(1) 小于end(3),因此count以起始数2和结束数3调用自己;
4.第二次调用count将打印其新的起始参数:2;
5.start(2) 小于end(3),因此count以起始数3和结束数3调用自己;
6.第三次调用count将打印其新的起始参数:3;
7.start(3) 不小于end(3),因此count不再调用自己,只是返回;
8.前两次count调用也返回了,程序结束。
若再添加对Printf调用,来显示每次count的调用和输出,这个程序更明显:
package main
import "fmt"
func count(start int, end int) {
fmt.Printf("count(%d, %d) called\n", start, end)
fmt.Println(start)
if start < end {
count(start+1, end)
}
fmt.Printf("Returning from count(%d, %d) call\n", start, end)
}
func main() {
count(1, 3)
}
输出:
count(1, 3) called
1
count(2, 3) called
2
count(3, 3) called
3
Returning from count(3, 3) call
Returning from count(2, 3) call
Returning from count(1, 3) call
递归列出目录内容
package main
import (
"fmt"
"io/ioutil"
"log"
"path/filepath"
)
func scanDirectory(path string) error { //递归函数,接受要扫描的路径,返回遇到的任何错误
fmt.Println(path) //打印当前目录
files, err := ioutil.ReadDir(path) //获取包含目录内容的切片
if err != nil {
return err
}
for _, file := range files {
filePath := filepath.Join(path, file.Name()) //用斜杠将目录路径和文件名连接起来
if file.IsDir() { //如果这是一个目录
err := scanDirectory(filePath) //递归调用scanDirectory,这次是用子目录的路径
if err != nil {
return err
}
} else {
fmt.Println(filePath) //如果这是一个普通文件,只需打印它的路径
}
}
return nil
}
func main() {
err := scanDirectory("go") //通过对顶部目录调用scanDirectory来启动该过程
if err != nil {
log.Fatal(err)
}
}
输出;
go
go/src
go/src/geo
go/src/geo/coordinates.go
go/src/geo/landmark.go
go/src/locked
go/src/locked/secret.go
go/src/vehicle
go/src/vehicle/car.go
scanDirectory第一件事是打印当前路径,这样就知道我们在哪个目录下工作,然后它对该路径调用ioutil.ReadDir来获取目录的内容。
循环遍历ReadDir返回FileInfo值切片,处理每个值,调用filepath.Join将当前目录路径与当前文件名用斜杠连接起来(因此"go"和"src"被连接成"go/src")。
如果当前文件不是一个目录,scanDirectory只打印其完整卢金个,然后移动到下一个文件(如果当前目录中有其他文件)。
但是如果当前文件是一个目录,则会启动递归:scanDirectory使用该子目录的路径调用自己,如果该子目录有任何子目录,那么scanDirectory将使用每个子目录来调用自己,以此类推,遍历整个文件数。
过程:
1.main使用“go”路径调用scanDirectory;
2.scanDirectory打印它所传递的路径“go”,代表它所工作的目录;
3.使用“go”路径调用 ioutil.ReadDir;
4.返回的切片中只有一条内容:“src”;
5.对“go”的当前目录路径和“src”文件名调用filepath.Join,得到新路径“go/src”;
6.src 是一个子目录,所以再次调用 scanDirectory,这次使用的路径是 “go/src”,<–递归
7.scanDirectory打印新路径:“go/src”
8.使用“go/src”路径调用 ioutil.ReadDir ;
9.返回的切片中的第一条内容是“geo”;
10.对“go/src”的当前目录路径和“geo”文件名调用filepath.Join,得到新路径“go/src/geo”;
11.geo是一个子目录,因此再次调用 scanDirectory,这次使用的路径是 “go/src/geo”;
12.scanDirectory打印新路径"go/src/geo";
13.使用“go/src/geo”路径调用ioutil.ReadDir;
14.返回的切片中第一条内容是“coordinates.go”;
15.coordinates.go不是目录,所以只打印名字;
16.以此类推。。。
递归函数比较难写,并且通常会比非递归解决方式消耗更多的计算资源,但有时候递归函数可以解决用其他方式难以解决的问题。
递归函数中的错误处理
如果scanDirectory在扫描任何子目录时遇到错误,比如没有访问该目录的权限,将返回一个错误。
添加两个Printf语句来显示返回的错误,会发现处理此错误的方式并不理想,如果在递归的scanDirectory调用中发生错误,则必须沿整个链返回该错误,直到main函数为止。
func scanDirectory(path string) error {
fmt.Println(path)
files, err := ioutil.ReadDir(path)
if err != nil {
fmt.Printf("Returning error form scanDirectory(\"%s\") call\n", path) //对ReadDir调用中的错误打印调试信息
return err
}
for _, file := range files {
filePath := filepath.Join(path, file.Name())
if file.IsDir() {
err := scanDirectory(filePath)
if err != nil {
fmt.Printf("Returning error form scanDirectory(\"%s\") call\n", path) //对递归的scanDirectory调用中的错误打印调试信息
return err
}
} else {
fmt.Println(filePath)
}
}
return nil
}
发起一个panic
以前遇到过panic,在访问数组和切片中的无效索引时。当类型断言失败时,也会看到:当程序出现panic时,当前函数停止运行,程序打印日志消息并崩溃。可以通过调用内置的panic函数来引发panic。
package main
func main() {
panic("oh, no, we're going down")
}
输出:
panic: oh, no, we're going down
goroutine 1 [running]:
main.main()
/golang/panic.go:4 +0x39
exit status 2
panic函数需要一个满足空接口的参数(也就是说,它可以是任何类型),该参数将被转换为字符串(如果需要),并作为panic日志信息的一部分打印出来。
堆栈跟踪
每个被调用的函数都需要返回到调用的它的函数,为了实现这一点,Go保持一个调用堆栈,即在任何给定点上处于活动状态的函数的调用的列表。
当程序发生panic时,panic输出中包含堆栈跟踪,即调用堆栈列表,这对于确定导致程序崩溃的原因很有用。
package main
func main() {
one()
}
func one() {
two()
}
func two() {
three()
}
func three() {
panic("this call stack's too deep for me!")
}
输出:
panic: this call stack's too deep for me!
goroutine 1 [running]:
main.three(...)
/golang/one.go:13
main.two(...)
/golang/one.go:10
main.one(...)
/golang/one.go:7
main.main()
/golang/one.go:4 +0x3b
exit status 2
堆栈跟踪包括已执行的函数调用的列表。
延迟调用在崩溃前完成
当程序出现panic时,所有延迟的函数调用仍然会被执行,如果有多个延迟调用,它们的执行顺序将与被延迟的顺序相反。
package main
import "fmt"
func main() {
one()
}
func one() {
defer fmt.Println("deferred in one()") //这个函数调用被延迟,所以排在最后执行
two()
}
func two() {
defer fmt.Println("deferred in two()") //这个函数调用被延迟,所以排在最后执行
panic("Let's see what's been deferred!")
}
输出:
deferred in two()
deferred in one()
panic: Let's see what's been deferred!
goroutine 1 [running]:
main.two()
/golang/two.go:14 +0x95
main.one()
/golang/two.go:10 +0x85
main.main()
/golang/two.go:6 +0x20
exit status 2
通过scanDirectory使用"panic"
package main
import (
"fmt"
"io/ioutil"
"path/filepath"
)
func scanDirectory(path string) { //不再需要返回错误值
fmt.Println(path)
files, err := ioutil.ReadDir(path)
if err != nil {
panic(err) //不返回错误值,而是将其传递给"panic"
}
for _, file := range files {
filePath := filepath.Join(path, file.Name())
if file.IsDir() {
scanDirectory(filePath) //不再需要存储或检查错误返回值
} else {
fmt.Println(filePath)
}
}
}
func main() {
scanDirectory("go") //不再需要存储或检查错误返回值
}
从scanDirectory声明中删除错误返回值,若从ReadDir返回一个error值,将其传递给panic,可以从对scanDirectory的递归调用中删除错误处理代码,也可以在main中从对scanDirectory的调用中删除错误处理代码。
当scanDirectory在读取目录遇到错误时,就产生panic,所有对scanDirectory的递归调用都退出。
何时产生panic
事实上,调用panic并不是处理错误的理想方法。
无法访问的文件、网络故障和错误的用户输入通常是被允许的,应该通过错误值来进行适当的处理,通产,调用panic应该留给"不可能"的情况,比如错误表示的是程序中的错误,而不是用户方的错误。
下面这个示例使用panic来指明一个bug。会颁发隐藏在三扇虚拟门中其中一扇门后面的奖品,doorNumber 变量不是由用户输入的,而是由rand.Intn函数选择的一个随机数,如果doorNumber 包含1、2或3以外的任何数字,就是程序的bug,而不是用户错误。
package main
import (
"fmt"
"math/rand"
"time"
)
func awardPrize() {
doorNumber := rand.Intn(3) + 1 //产生一个1到3之间的随机整数
if doorNumber == 1 {
fmt.Println("you win a cruise!")
} else if doorNumber == 2 {
fmt.Println("you win a car!")
} else if doorNumber == 3 {
fmt.Println("you win a goat!")
} else {
panic("invalid door number") //不应该产生其他数字,如果产生就panic
}
}
func main() {
rand.Seed(time.Now().Unix())
awardPrize()
}
因此,假如doorNumber包含无效值,调用panic是有意义的。
“recover"函数
将scanDirectory函数改为使用panic而不是返回错误,这大大简化了错误处理代码,但panic也会导致程序崩溃,出现难看的堆栈跟踪,宁愿只显示错误信息。
Go提供了一个内置的recover
函数,可以阻止程序陷入panic,需要使用它来体面的退出程序。
正常程序执行过程中调用recover时,只返回nil,而不执行其他操作:
package main
import "fmt"
func main() {
fmt.Println(recover()) //如果在一个程序中调用"recover",而这个程序没有panic...
}
输出:
<nil> //什么都不做,返回nil
如果在程序处于panic状态时调用recover,将停止panic,但是当在函数中调用panic时,该函数将停止执行。因此,在panic所在的同一个函数中调用recover没有意义,因为panic无论如何都会继续:
func freakOut() {
panic("oh no") //panic阻止了freakOut函数的其余部分运行
recover()
}
func main() {
freakOut()
fmt.Println("Exiting normally")
}
输出:
panic: oh no
goroutine 1 [running]:
main.freakOut()
/golang/freakOut.go:6 +0x39
main.main()
/golang/freakOut.go:11 +0x22
exit status 2
但是,当程序panic时,有一种方式可以调用recover。在panic期间,任何延迟的函数调用都将完成,因此可以在一个单独的函数中放置一个recover调用,并在引发panic的代码之前使用defer调用该函数。
func calmDown() {
recover()
}
func freakOut() {
defer calmDown()
panic("oh no")
}
func main() {
freakOut()
fmt.Println("Exiting normally")
}
调用recover不会导致在出现panic时恢复执行,至少不会完全恢复。产生panic的函数将立即返回,而该函数块中panic之后的任何代码都不会执行。但是在产生panic的函数返回之后,正常的执行将恢复。
func calmDown() {
recover()
}
func freakOut() {
defer calmDown()
panic("oh no") //当恢复时freakOut在这个位置返回
fmt.Println("I won't be run!") //panic之后的代码永远不会运行
}
func main() {
freakOut()
fmt.Println("Exiting normally") //这段代码在freakOut返回后运行
}
panic值从recover中返回
程序当没有panic时,调用recover返回nil,但是当出现panic时,recover返回传递给panic的任何值
,这可以用来收集有关panic的信息,帮助恢复或向用户报告错误。
func calmDown() {
fmt.Println(recover()) //调用"recover"并打印panic值
}
func main() {
defer calmDown()
panic("oh no") //将从"recover"返回的值
}
输出:
oh no
panic函数其参数类型是 interface{} ,即空接口,因此panic可以接收任何值。同样recover的返回值类型也是 interface{} ,可以将recover的返回值传递给诸如Println(它接受interface{}值)之类的fmt函数,但不能直接对其调用方法。
将error值传递给panic,但这样做时,error被转换为一个空接口值,当延迟的函数稍后调用recover时,返回的是interface{}值,即使底层的error值有一个Error方法,试图调用interface{}值上的Error会导致编译错误。
func calmDown() {
p := recover() //返回一个空接口
fmt.Println(p.Error()) //即使底层的“error”值有一个Error方法,但interface{}没有
}
func main() {
defer calmDown()
err := fmt.Errorf("there's an error")
panic(err) //将错误值而不是字符串传递给“panic”
}
输出:
p.Error undefined (type interface{} is interface with no methods)
要对panic值调用方法或执行其他操作,需要使用类型断言将其转换回其底层类型:
func calmDown() {
p := recover()
err, ok := p.(error) //断言panic值的类型为“error”
if ok {
fmt.Println(err.Error()) //现在有了一个“error”值,可以调用Error方法
}
}
func main() {
defer calmDown()
err := fmt.Errorf("there's an error")
panic(err)
}
输出:
there's an error
从scanDirectory中的panic恢复
添加一个reportPanic函数,在main中使用defer调用它,在调用scanDirectory前调用它,这可能会引起潜在的panic。
在reportPanic中,调用recover并存储它返回的panic值,如果程序处于panic状态,这将会停止panic。
package main
import (
"fmt"
"io/ioutil"
"path/filepath"
)
func reportPanic() {
p := recover() //调用“recover”并存储它的返回值
if p == nil { //如果返回nil,则没有panic
return
}
err, ok := p.(error) //否则,获取底层的“error”值
if ok {
fmt.Println(err) //然后打印出来
}
}
func scanDirectory(path string) {
fmt.Println(path)
files, err := ioutil.ReadDir(path)
if err != nil {
panic(err)
}
for _, file := range files {
filePath := filepath.Join(path, file.Name())
if file.IsDir() {
scanDirectory(filePath)
} else {
fmt.Println(filePath)
}
}
}
func main() {
defer reportPanic() //在调用可能引起panic的代码前,延迟调用新reportPanic函数
scanDirectory("go")
}
如果没有panic,从reportPanic返回,不做任何事情,若panic值不是nil,意味着出现了panic,因为scanDirectory将error值传递给panic,所以使用类型断言将 interface{}panic 值转换为error值,若转换成功,则打印error值。
这样的话,程序输出只会看到错误信息,而不是难看的panic日志和堆栈跟踪。
恢复panic
reportPanic还存在一个潜在的问题,它可以拦截任何panic,即使不是来自scanDirectory,如果panic值不能转换为error类型,reportPanic将不会打印它。
可以通过在main中使用一个string参数来添加另一个对panic的调用:
func main() {
defer reportPanic()
panic("some other issue")
scanDirectory("go")
}
输出:
//没有输出
reportPanic函数从新的panic中恢复,但是因为panic值不是一个error,所以reportPanic不会打印它,我们不知道为什么程序失败了。
有一种策略来处理不曾预料且不准备从中恢复的panic,即简单的恢复panic状态。
如果将panic值转换为error的类型断言成功,只需像以前那样打印它,如果失败,只需用同样的panic值再次调用panic。
func reportPanic() {
p := recover()
if p == nil {
return
}
err, ok := p.(error)
if ok {
fmt.Println(err)
} else {
panic(p) //如果panic值不是error,则使用相同的值恢复panic
}
}
func scanDirectory(path string) {
// ...
}
func main() {
defer reportPanic()
panic("some other issue") //一旦确定reportPanic起作用,不要忘记删除这个测试panic
scanDirectory("go")
}
输出:
panic: some other issue [recovered]
panic: some other issue
goroutine 1 [running]:
main.reportPanic()
/golang/reportPanic_panic.go:18 +0xec
panic(0x10ab940, 0x10e8ff0)
/usr/local/go/src/runtime/panic.go:969 +0x166
main.main()
/golang/reportPanic_panic.go:40 +0x5b
exit status 2
reportPanic从我们对panic的测试调用中恢复了,但是当error类型断言失败,它将再次陷入panic。
Go语言本身的设计不鼓励使用panic和recover,Go鼓励以处理程序其他部分的方式处理错误,比如使用if和return语句,以及error值。
练习:
func snack() {
defer fmt.Println("closing")
fmt.Println("opening")
panic("empty")
}
func main() {
snack()
}
输出:
opening
closing //这个调用被延迟,直到snack函数退出(在panic期间)才进行调用
panic: empty
....