Go 语言基础--错误&异常浅析

前言

如果go是你的第一门语言,go的异常和错误体系可能比较容易接受,但如果你有一定的Java或者c++基础,go的异常和错误体系可能会比较不适应。
go的错误及异常体系也同样的追求简洁优雅,它摒弃了Java或者c++ 中的try-catch-finally模式,通过返回值的形式来表示错误,因为go认为try-catch会干扰程序的正常的控制流程,所以通过返回值的性质,认为错误其实是程序运行过程中的重要组成部分。
除此之外go把错误也异常分开了,真正的异常是指程序已经无法向下执行,需要由服务来进行特殊处理,在go中的表现形式是defer、panic、recover。

错误

使用

go为我们提供了一个错误接口、接口中包含一个描述错误信息的 Error()方法:

type error interface {
    Error() string
}

比如说类型转换就是一个经典的场景:

var a = "1"
aInt, err := strconv.Atoi(a)

这部分错误通常是可以接受的,所以可以为这是函数正常返回之一,我们也应该对于这种情况进行自身的处理,比如说当转换异常时说明参数不合法我们应该拒绝这个操作或者给它一个默认值,并且我们可以通过err.Error()来拿到错误信息,给出程序的一些警报。而不是说通过像Java一样的try-catch、throw来制造一个异常流,这样异常流+流程控制两个维度叠加在一起相对来说是较难读懂的。
除此之外,我们可以通过之前介绍的接口的方式来定义自己的错误类型,来完善正常的业务逻辑,像下面这样,我们就可以使用自身的错误类型了,这里有点像是Java中摆脱了try-catch的Exeception。

type Operation struct {...}
func (op Operation) Error() string {
 // Do something
}

// 定义完自己异常类型后我们可以把这个err 返回值,当作正常结果进行逻辑处理了(当然了,原生的error也可以这样用,但是功能或者错误信息有一定的局限)
if err != nil {
    switch err {
    case Err1:
        // Do something
        return
    case Err2:
        // Do something
        return
    default:
        // Do something
        return
    }
}
实现

go标准库关于error的实现也蛮简单的,源码位于src/errors包下


image.png

标准库中对于Error接口做了一个基础的实现叫做errorString

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}

其实就是实现了一下Error接口,包含一个错误信息的string,因为该string是不可导出的,我们只能对于标准库中的error进行Error信息的查看。
标准库中的错误说实话没什么非常大的用处,顶多是通过errors.New() 来定义自定义的标准错误类型的错误。更多的时候我们需要像上面那样在Error接口的基础上定义自己的异常,来实现更加丰富的功能。

异常

真正异常情况下,go中依赖的是defer、panic、recover操作来处理异常。
defer之前提到过,类似于C++ 中的析构函数,不过同析构函数不一样的是defer主要用于在函数结束时执行一段代码。
panic用来表示非常严重不可恢复的错误,像是Java中的Error,在go里面这是一个内置函数,在panic发生时程序通常会宕掉,并且打出调用栈来帮忙分析处理。
recover通常是用来处理panic这种重大异常的,来让程序不退出,仅影响这一次操作。
这里的处理方式很像是Java中try-chtch异常处理方式,但是go中对于异常的定义不像是Java中那样,我们应该改变Java中的一些思路,尽可能使用错误,不能误用异常,只有致命的panic异常时才这样处理。
在使用go时,panic是非常危险的,即使你的程序有supervise之类的守护进程,不断的挂掉重启,也会严重的影响程序的可用性,通常来说我们使用recover来进行panic的捕获,来阻止程序崩溃。

基础使用

先来看一下demo:


func test() {

defer func() {

// do something

fmt.println("c")

if err:=recover();err!=nil{

fmt.println("d")

fmt.Println(err) // 这里的err其实就是panic传入的内容

}

}()

fmt.println("a")

// do something maybe panic

panic("panic")

fmt.println("b")

}

这里程序的输出顺序是:a\c\d\panic

panic 发生时,会直接从当前行跳出,如果有defer的recover将会被拦住,执行defer中的内容。

通常来说,panic一般是由一些运行时错误导致的,比如说数组越界、空指针等。针对这类问题:

1、写代码时要谨慎处理,避免发生panic,

2、要有recover来阻止panic 崩溃程序。

原理

panic和recover关键字会在编译时被编译器转换为OPANIC、ORECOVER类型的节点,然后进一步转换成gopanic、gorecover两个运行时的函数调用。

先来看一下panic的数据结构:src/runtime/runtime2.go


//go:notinheap

type _panic struct {

argp unsafe.Pointer

arg interface{}

link *_panic

recovered bool

aborted bool

}

每次发生panic函数的调用时。都会创建上述结构体的一个实例来存储相关的信息和结构。

其中:

argp只想defer调用时参数的指针

arg panic的入参

link指向更早调用的_panic的实例 (很显然panic出现时是一个异常链)

recoveres表示当前是否被恢复(recover)

aborted是否被强行终止

panic 终止进程

没有被recover的panic会导致程序直接退出,主要在gopanic中做了这件事。

继续看源码:src/runtime/runtime2.go l:445


func gopanic(e interface{}) {

gp := getg()

if gp.m.curg != gp {

print("panic: ")

printany(e)

print("\n")

throw("panic on system stack")

}

if gp.m.mallocing != 0 {

print("panic: ")

printany(e)

print("\n")

throw("panic during malloc")

}

if gp.m.preemptoff != "" {

print("panic: ")

printany(e)

print("\n")

print("preempt off reason: ")

print(gp.m.preemptoff)

print("\n")

throw("panic during preemptoff")

}

if gp.m.locks != 0 {

print("panic: ")

printany(e)

print("\n")

throw("panic holding locks")

}

var p _panic

p.arg = e

p.link = gp._panic

gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

atomic.Xadd(&runningPanicDefers, 1)

for {

d := gp._defer

if d == nil {

break

}

if d.started {

if d._panic != nil {

d._panic.aborted = true

}

d._panic = nil

d.fn = nil

gp._defer = d.link

freedefer(d)

continue

}

d.started = true

d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

p.argp = unsafe.Pointer(getargp(0))

reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))

p.argp = nil

if gp._defer != d {

throw("bad defer entry in panic")

}

d._panic = nil

d.fn = nil

gp._defer = d.link

pc := d.pc

sp := unsafe.Pointer(d.sp)

freedefer(d)

if p.recovered {

atomic.Xadd(&runningPanicDefers, -1)

gp._panic = p.link

for gp._panic != nil && gp._panic.aborted {

gp._panic = gp._panic.link

}

if gp._panic == nil {

gp.sig = 0

}

gp.sigcode0 = uintptr(sp)

gp.sigcode1 = pc

mcall(recovery)

throw("recovery failed")

}

}

preprintpanics(gp._panic)

fatalpanic(gp._panic)

*(*int)(nil) = 0

}

1、首先对内部变量还有抢锁的情况做了check。

2、获取当前的goroutine

3、创建一个_panic实例

4、从当前的goroutine中获取一个_defer结构体

5、如果_defer存在,调用reflectcall执行_defer中的代码

6、将下一个的_defer结构设置到 Goroutine 上并回到 4

7、调用fatalpanic中止整个程序

其中,在fatalpanic中止整个程序之前就会通过printpanics打印出全部的panic消息以及调用时传入的参数


func preprintpanics(p *_panic) {

defer func() {

if recover() != nil {

throw("panic while printing panic value")

}

}()

for p != nil {

switch v := p.arg.(type) {

case error:

p.arg = v.Error()

case stringer:

p.arg = v.String()

}

p = p.link

}

}

func printpanics(p *_panic) {

if p.link != nil {

printpanics(p.link)

print("\t")

}

print("panic: ")

printany(p.arg)

if p.recovered {

print(" [recovered]")

}

print("\n")

}

fatalpanic会调用exit来退出程序,并且返回错误码2.


func fatalpanic(msgs *_panic) {

pc := getcallerpc()

sp := getcallersp()

gp := getg()

var docrash bool

systemstack(func() {

if startpanic_m() && msgs != nil {

atomic.Xadd(&runningPanicDefers, -1)

printpanics(msgs)

}

docrash = dopanic_m(gp, pc, sp)

})

if docrash {

crash()

}

systemstack(func() {

exit(2)

})

*(*int)(nil) = 0 // not reached

}

recover 恢复程序

上面介绍了panic崩溃程序的过程,接下来看一下recover阻止崩溃,恢复程序的过程。

看一下gorecover 函数:


func gorecover(argp uintptr) interface{} {

gp := getg()

p := gp._panic

if p != nil && !p.recovered && argp == uintptr(p.argp) {

p.recovered = true

return p.arg

}

return nil

}

这个函数非常简单,修改panic结构体的recovered字段,当前函数的调用其实都发生在gopanic期间。

然后后期检测这个字段的时候,就不崩溃了(看一下gopanic函数就比较清晰了)


if p.recovered {

atomic.Xadd(&runningPanicDefers, -1)

gp._panic = p.link

for gp._panic != nil && gp._panic.aborted {

gp._panic = gp._panic.link

}

if gp._panic == nil {

gp.sig = 0

}

gp.sigcode0 = uintptr(sp)

gp.sigcode1 = pc

mcall(recovery)

throw("recovery failed")

}

从_defer结构体中取出了程序计数器pc和栈指针sp并调用recovery方法进行调度,调度之前会准备好sp、pc以及函数的返回值。

这一块儿就是panic和recover的过程啦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容