一些恐慌/恢复用例

恐慌和恢复(panic/recover)已经在之前的文章中介绍过了。 下面将展示一些恐慌/恢复用例。

用例1:避免恐慌导致程序崩溃

这可能是最常见的panic/recover用例了。 此用例广泛地使用于并发程序中,尤其是响应大量用户请求的应用。

一个例子:
package main

import "errors"
import "log"
import "net"

func main() {
	listener, err := net.Listen("tcp", ":12345")
	if err != nil {
		log.Fatalln(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println(err)
		}
		// 在一个新协程中处理客户端连接。
		go ClientHandler(conn)
	}
}

func ClientHandler(c net.Conn) {
	defer func() {
		if v := recover(); v != nil {
			log.Println("捕获了一个恐慌:", v)
			log.Println("防止了程序崩溃")
		}
		c.Close()
	}()
	panic("未知错误") // 演示目的产生的一个恐慌
}

运行此服务器程序,并在另一个终端窗口运行telnet localhost 12345,我们可以观察到服务器程序不会因为客户连接处理协程中的产生的恐慌而导致崩溃。

如果我们在上例中不捕获客户连接处理协程中的潜在恐慌,则这样的恐慌将使整个程序崩溃。

用例2:自动重启因为恐慌而退出的协程

当在一个协程将要退出时,程序侦测到此协程是因为一个恐慌而导致此次退出时,我们可以立即重新创建一个相同功能的协程。 一个例子:
package main

import "log"
import "time"

func shouldNotExit() {
	for {
		time.Sleep(time.Second) // 模拟一个工作负载
		// 模拟一个未预料到的恐慌。
		if time.Now().UnixNano() & 0x3 == 0 {
			panic("unexpected situation")
		}
	}
}

func NeverExit(name string, f func()) {
	defer func() {
		if v := recover(); v != nil { // 侦测到一个恐慌
			log.Printf("协程%s崩溃了,准备重启一个", name)
			go NeverExit(name, f) // 重启一个同功能协程
		}
	}()
	f()
}

func main() {
	log.SetFlags(0)
	go NeverExit("job#A", shouldNotExit)
	go NeverExit("job#B", shouldNotExit)
	select{} // 永久阻塞主线程
}

用例3:使用panic/recover函数调用模拟长程跳转

有时,我们可以使用panic/recover函数调用来模拟跨函数跳转,尽管一般这种方式并不推荐使用。 这种跳转方式的可读性不高,代码效率也不是很高,唯一的好处是它有时可以使代码看上去不是很啰嗦。

在下面这个例子中,一旦一个恐慌在一个内嵌函数中产生,当前协程中的执行将会跳转到延迟调用处。
package main

import "fmt"

func main() {
	n := func () (result int)  {
		defer func() {
			if v := recover(); v != nil {
				if n, ok := v.(int); ok {
					result = n
				}
			}
		}()

		func () {
			func () {
				func () {
					// ...
					panic(123) // 用恐慌来表示成功返回
				}()
				// ...
			}()
		}()
		// ...
		return 0
	}()
	fmt.Println(n) // 123
}

用例4:使用panic/recover函数调用来减少错误检查代码

一个例子:
func doSomething() (err error) {
	defer func() {
		switch e := recover().(type) {
		case nil:
		case error:
			err = e
		default:
			panic(e) // 重新抛出此恐慌
		}
	}()

	doStep1()
	doStep2()
	doStep3()
	doStep4()
	doStep5()

	return
}

// 在现实中,各个doStepN函数的原型可能不同。
// 这里,每个doStepN函数的行为如下:
// * 如果已经成功,则调用panic(nil)来制造一个恐慌
//   以示不需继续;
// * 如果本步失败,则调用panic(err)来制造一个恐慌
//   以示不需继续;
// * 不制造任何恐慌表示继续下一步。
func doStepN() {
	...
	if err != nil {
		panic(err)
	}
	...
	if done {
		panic(nil)
	}
}

下面这段同功能的代码比上面这段代码看上去要啰嗦一些。

func doSomething() (err error) {
	shouldContinue, err := doStep1()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep2()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep3()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep4()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep5()
	if !shouldContinue {
		return err
	}

	return
}

// 如果返回值err不为nil,则shouldContinue一定为true。
// 如果shouldContinue为true,返回值err可能为nil或者非nil。
func doStepN() (shouldContinue bool, err error) {
	...
	if err != nil {
		return false, err
	}
	...
	if done {
		return false, nil
	}
	return true, nil
}

但是,这种panic/recover函数调用的使用方式一般并不推荐使用,因为它的效率略低一些,并且这种用法不太符合Go编程习俗。

另外需要注意的是:从Go 1.21开始,一个panic(nil)调用将变得和panic(new(runtime.PanicNilError))等价。 所以,从Go 1.21开始,上面代码中的延迟函数调用应该被重写为:
import "runtime"

...

func doSomething() (err error) {
	defer func() {
		switch e := recover().(type) {
		case nil, *runtime.PanicNilError:
		case error:
			err = e
		default:
			panic(e) // 重新抛出此恐慌
		}
	}()

	doStep1()
	...
}


目录↡

Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。

本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .来浏览和确认各种改动。

本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。

你或许对本书作者老貘开发的一些App感兴趣。

The English version of this book is here.
赞赏
(《Go语言101》系列丛书由老貘从2016年7月开始编写。目前此系列丛书仍在不断改进和增容中。你的赞赏是本系列丛书和此Go101.org网站不断增容和维护的动力。)

目录: