如何从外部包中的异步恐慌中恢复

huangapple go评论150阅读模式
英文:

How to recover from an asynchronous panic in external package

问题

我正在学习Go语言,并且正在尝试理解如何正确处理来自外部包的panic。

这里有一个可运行的示例,假设一个包定义了doFoo方法(为了示例的完整性,它在同一个包中):

package main

import (
	"log"
	"net/http"

	"sync"
	"time"

	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
)

// 外部包中的方法
func doFoo() {
	var wg sync.WaitGroup
	wg.Add(1)
	// 做一些很酷的异步操作
	go func() {
		time.Sleep(500)
		wg.Done()
		panic("Oops!")
	}()
}

func router() *mux.Router {
	var router = mux.NewRouter().StrictSlash(true)
	router.HandleFunc("/doFoo", index).Methods("GET")
	return router
}

func main() {
	log.Fatal(http.ListenAndServe(":8080", handlers.RecoveryHandler()(router())))
}

func index(w http.ResponseWriter, r *http.Request) {
	defer func() {
		recover()
		w.WriteHeader(http.StatusInternalServerError)
	}()
	doFoo()
	w.WriteHeader(http.StatusOK)
}

调用doFoo方法将导致服务器崩溃,我理解这是正确的行为,因为应用程序现在处于不确定状态。最好崩溃并通过负载均衡器将后续请求转发到其他进程。

但是,我的API服务器可能仍在为其他客户端提供服务,可能正在维护Websockets,并且我可能还希望在此处返回500错误。

作为来自Node.js的开发者,我习惯于uncaughtException的概念,用于处理未捕获的同步异常,以及unhandledRejection用于处理未捕获的异步异常。这两个进程构造使开发者可以选择立即崩溃程序(如果有意义的话),或记录错误,返回适当的HTTP代码,然后在需要时优雅地关闭。

在我的在线研究中,我发现很多资源都说panic不像异常那样不寻常,你不需要担心它们。但事实上,在编写代码时很容易引发panic。完全由开发者来确保他的库不会引发panic,这里完全涉及到人为因素。

这让我想知道,我是否需要审查我将使用的每个包的整个代码库,包括所有的包依赖关系?只是因为我无法防止在某个外部包中遗漏恢复操作,从而导致整个服务器崩溃,破坏用户的体验?

或者,我是否不知道某种策略,可以在库代码中发生异步panic时优雅地失败?

我注意到自1.8版本以来有优雅的关闭功能,但我无法使用它,因为我的程序已经崩溃。
https://golang.org/pkg/net/http/#Server.Shutdown

还有gorilla recovery handler,但同样,它只能保护同步panic。
http://www.gorillatoolkit.org/pkg/handlers#RecoveryHandler

更新:
我知道panic不是异常。重申这一点并不回答问题,这个问题不是关于panic和异常的。这个问题是关于理解语言可能提供的工具,以在不需要开发者阅读整个包树中的每一行代码的情况下强制执行边界。如果语言中不可能实现这一点,那么说明一下也是一个有效的答案。我只是不知道是否可能实现。

英文:

I'm learning Go and I'm trying to understand how to properly deal with panics from external packages.

Here is a runnable example, say a package defines the doFoo method. (It's located in the same package here for the sake of the example )

package main

import (
	"log"
	"net/http"

	"sync"
	"time"

	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
)
// Method from External package
func doFoo() {
	var wg sync.WaitGroup
	wg.Add(1)
	// Do some cool async stuff
	go func() {
		time.Sleep(500)
		wg.Done()
		panic("Oops !")
	}()
}

func router() *mux.Router {
	var router = mux.NewRouter().StrictSlash(true)
	router.HandleFunc("/doFoo", index).Methods("GET")
	return router
}

func main() {
	log.Fatal(http.ListenAndServe(":8080", handlers.RecoveryHandler()(router())))
}

func index(w http.ResponseWriter, r *http.Request) {
	defer func() {
		recover()
		w.WriteHeader(http.StatusInternalServerError)
	}()
	doFoo()
	w.WriteHeader(http.StatusOK)
}

Invoking the doFoo method will crash the server, I appreciate that is correct behavior, since the application is now in an undermined state. And it's best to crash and have subsequent requests forwarded to a different processes trough some load balancer.
But, my api server might still be serving other clients, it might be maintaining websockets, and I Might would also want to return a 500 error here.

Coming from nodejs, I am used to the concept of uncaughtException, for handeling uncaptured synchronous exceptions and unhandledRejection to handle uncaptured asynchronous exceptions. These two process constructs give the developer the choice to either crash the program right away ( if it makes sense ), or log the error, return a proper http code, and then maybe shutdown gracefully if needed.

In my online research I find a lot of resources saying, panic's are not like exceptions, they are unusual, you don't need to worry about them. But it seems like it's actually very easy to cause a panic when writing code. It's completely up to the developer to ensure his library does not panic, the human factor is 100% involved here.

This leads me to wonder, do I need to audit the entire code base of every single package I'm going to use, including all the package dependencies as well ? just because I have no means of safeguarding against a missed recover in some external package that will take down my whole server and ruin my user's experience ?

Or is there some strategy I am not aware of that I can fail gracefully when an asynchronous panic occurs in library code ?

I noticed there is graceful shutdown since 1.8, but I can't use this because my program has already crashed.
https://golang.org/pkg/net/http/#Server.Shutdown

There is the gorilla recovery handler, but again, this only protects against synchronous panics.
http://www.gorillatoolkit.org/pkg/handlers#RecoveryHandler

Update:
I am aware that panics are not exceptions. Restating that does not answer the question, panics and exceptions is not what this question is about. This question is about understanding what tools the language may provide to enforce boundaries without bestowing the need to read every single line in the entire package tree onto the developer. If it's not possible in the language then stating that is a valid answer. I just don't know if it is or not.

答案1

得分: 3

恐慌不是异常。不要将它们视为异常,你就会没问题。

首先:包的API不应该引发恐慌,它们应该始终返回错误,除非在某些非常罕见的情况下,而且必须清楚地记录下何时以及为什么会引发恐慌(regexp.MustCompile 是一个可能引发恐慌的很好的例子)。任何在遇到错误时引发恐慌(而且没有非常充分的理由这样做)的包都是有问题的,不要使用它们。

如果你进行边界检查,确保不要访问空指针等,你就不必担心恐慌。

至于在goroutine中恢复恐慌,除非该goroutine有自己的恢复处理程序,否则你无法做到。

如果goroutine来自第三方库,请不要使用该库!如果他们不够严格以至于不检查边界情况和/或懒得只是在错误上引发恐慌,那你为什么要使用他们的代码呢?谁知道它还隐藏着什么其他的问题呢?

如果goroutine是你自己的代码,请尽量消除可能引发恐慌的因素,然后根据需要添加恢复处理程序来捕获无法预防的恐慌。

英文:

Panics are not exceptions. Do not treat them like exceptions and you will be fine.

First things first: Package APIs should never panic, they should always return an error except in certain very rare cases, and then they must be clearly documented as to when and why they can panic (regexp.MustCompile is a good example of something that may panic). Any package that panics if it hits an error (and doesn't have a very good reason to do so) is bad, don't use it.

If you do bounds checking, make sure not to acess nil pointers, etc you should never have to worry about panics.

As for recovering panics in a goroutine, unless the goroutine has its own recovery handler you can't.

If the goroutine in from a third party library, don't use that library! If they are lax enough not to check edge cases and/or are lazy enough to just panic on error, why are you using their code? Who knows what other mines it holds?

If the goroutine is your own code, try to eliminate things that can panic, then add a recovery handler to catch the ones you can't prevent if needed.

huangapple
  • 本文由 发表于 2017年9月15日 10:52:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/46230973.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定