在程序开发时我们都会认为外部提供的接口或者数据都是不可信的。比如函数总是要检查入参的正确性,在做单元测试的时候要把外部提供的接口给屏蔽掉等。之所以都会这么做,主要还是很难保证自己还是其他人可以提供一个没有任何缺陷的接口。既然接口是人写的,那么多少会有些考虑不到的地方,这时候接口在被调用的时候就有可能发生错误或者异常。这里讨论golang中的异常处理机制,其实就是panic和recover这两个接口的运用,类似于C++中的try和catch。
1、golang中的panic
panic,中文解释为恐慌。举个例子,单代码中出现这样的语句的时候,相信所有开发人员在产品上线的时候都会恐慌:
var MakecoreData *int = nil
*MakecoreData = 10000
如果不做任何处理的时候,这个golang出现如果跑到这里的时候就会出现这样的结果。
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x5d0676]
然后重新就退出了,这个结果其实跟C/C++中的core是一样的。
当然这个例子可能不会有人会犯这种低级错误,但是如果这两个语句中间加上一段复制的逻辑,且需要一定的条件触发的话,那可能就很难发现了。
再来看看golang中panic的机制,当程序发生异常的时候,golang语言级别的会抛出一个panic,以上面例子来说,当代码运行到*MakecoreData = 10000的时候,会抛出一个panic,函数就结束了,如果没有做任何处理,那么程序中对应的g就异常,程序的生命周期就结束了。同样的,如果在代码中直接调用panic也是一致的结果。例如直接调用:panic(“have a panic”)的话,运行结果为:
panic(“have a panic”),然后显示对应的堆栈信息。
2,golang中的recover
在golang当异常发生的时候,都会产生一个panic。而一个panic总会有一个recover与之对应。当没有任何处理的时候,默认的recover行为的行为是将进程退出。recover会返回一个error类型的返回值,记录异常的情况。由于当异常产生的时候,产生异常的函数将不再执行,会立即退出。那recover就只能在defer中调用,而且是要在产生异常前的defer语句中调用。因为在golang中,只有执行到defer语句的时候才会将对应的函数插入到defer队列中,如果defer语句在异常产生后才调用,该defer对应要执行的函数由于没有插入到defer队列而没有被调用到。
下面将以几个例子来说明:
1)recover不在defer中:
import (
"fmt"
"net/http"
"runtime"
)
func main() {
if err := recover(); err != nil {
fmt.Println(err)
}
var MakecoreData *int = nil
*MakecoreData = 10000
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("hello world")
}
从代码来看,产生异常的代码前后都调用了recover,但是从结果来看,运行结果跟前后不加recover是一致的。
2)在异常代码后增加一个defer语句,语句中有recover,例子如下:
import (
"fmt"
"net/http"
"runtime"
)
func main() {
var MakecoreData *int = nil
*MakecoreData = 10000
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
fmt.Println("hello world")
}
从结果来看,还是跟没有加recover是一致的。
3、在发生异常前增加一个defer语句,语句中有recover。例子如下:
import (
"fmt"
"net/http"
"runtime"
)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var MakecoreData *int = nil
*MakecoreData = 10000
fmt.Println("hello world")
}
这段代码中recover才会真正的被执行。这就跟前面分析的一种。当遇到异常的时候,发生异常的代码后面的函数体将不会被执行。函数退出,这时候会执行defer队列中注册的函数,如果在这里有recover操作。那么异常就会被捕获到。然后函数退出,不会导致程序结束。
3、使用golang异常机制,增强代码的健壮性。
从golang的异常机制可以看出,异常处理recover最好是在接口的入口处就将其插入到对应defer队列中。这样在接口调用过程中,即使发生异常,程序依然可以提供服务。以web服务为例子来看看如何使用golang的异常机制增强代码的健壮性:
1、无任何异常处理机制的web服务如下:
import (
"fmt"
"log"
"net/http"
)
func HelloServer(w http.ResponseWriter, req *http.Request) {
var MakecoreData *int = nil
*MakecoreData = 10000
fmt.Fprintf(w, "hello world")
}
func main() {
http.HandleFunc("/hello", HelloServer)
err := http.ListenAndServe(":12345", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
这个代码很明显,在执行业务代码的时候会发生异常。异常产生的原因跟上面章节描述的一致。但是如果服务有多个页面,不仅仅只有/hello的时,当客户端调用localhost:12345/hello以后,会返回一个404返回码。但是服务并不会挂掉。原因在于golang的http包中在接受一个请求的时候会有先在入口处调用recover来保证golang开发工程师在写业务代码的时候,如果发生了异常,不至于使整个服务都崩溃掉。
4、利用golang其他功能配合异常处理机制增强服务的健壮性。
利用golang的异常机制,可以是服务在有缺陷的情况下依然可以提供部分暂时没有缺陷的服务。但是这样是远远不够的,因为发现缺陷后他们需要修复,修改后还要重新部署。
对于大部分服务来说,服务需要提供24*7的不间断服务。那么在解决方案中,需要做的更多了。比如:
1、定位异常点。
定位方法可以通过golang中runtime包获取堆栈信息及入参并保存,可以知道程序在哪里发生异常,这样可以方便后续的维护。
2、采用主备服务
当确定某个服务存在缺陷的时候,采用主备,不接业务的服务器升级可以减少由于更新服务带来的业务中断。
3、服务业务模块采用热插拔插件式
业务模块是服务中跑的最多的代码。一般如果存在异常,那么至少有90%以上出现在业务模块代码上。如果采用插件的形式,看在服务不停止的情况下更新模块代码。
有疑问加站长微信联系(非本文作者)