升升的NotaBooka



路漫漫其修远兮
吾将上下而求索




Go的异常处理(附最佳实践)



前言

在程序执行的时候,很可能会遇到各种各样的问题,可能是内部的逻辑问题,也可能是外部环境依赖问题。

作为开发者,要提前考虑到程序执行时可能出现的各种问题,来提前预定义好相对应的处理逻辑。

不同的语言,有不同的异常处理方式。比如C语言使用error code的方式,python和java采用try机制等。

而golang的处理方式就显得比较有一点别致了。

异常的种类

我个人看法,异常可以分为两大类:

  • 一类是会影响整个程序继续执行的异常,遇到这种问题,可能我们当前进程或线程就会直接退出。
  • 另一类是,某一个小逻辑有问题,我们向上层通知一下之后程序仍然要继续执行。

不同的异常,应该有不同的处理方式,golang也提供了两种不同的方式来分别处理这两类。

error类型

golang有一个内建的error类型,用来存放异常信息。 其实error是一个interface。它预定义的interface是这样的:

type error interface {
    Error() string
}

任何一种类型,只要实现了Error()方法,都可以赋值给error类型,这就给了我们很多可定制化的空间,这个我们后边再讲。

当然,如果我们没有很复杂的需求时,我们可以这样来新建一个字符串类型的error实例:

err := errors.New("I'm error msg...")

也可以使用fmt包中的Errorf和Errorln这两个方法:

err := fmt.Errorf("I'm error msg...")
err1 := fmt.Errorln("I'm error msg ...")

这两种方式都可以生成一个error类型的实例,向上层传递。

如果没有异常,我们应该返回nil。

在上层,我们可以这样获得异常的详情:

msg := err.Error()

这就是error最简单的向上层传递。

简单的panic与recover

考虑这种情况:如果我们的调用栈比较深。在最底层的方法中,我们发现了一个很严重的异常,要直接返回到最上层,用这种一级一级的传递显然是很麻烦的。

golang提供了一种直接把异常向上层抛的机制,类似python的raise&except。这就是panic&recover

panic的声明如下:

func panic(v interface{})

可以传递任何的参数,让panic抛出异常。

当golang遇到panic语句时,当前方法会立刻停止执行,向上层返回(若有defer先defer)。

上层方法接收到panic信号之后,所有响应就相当于方法内panic一致。

panic就这样一级一级向上层传递,如果遇到recover则恢复,否则最终程序将直接终止。

因为panic无法绕过defer的逻辑,因此我们一般把recover放在defer里。

简单的用法如下:

func panic_test() {
    defer func(){
        if err := recover(); err != nil {
            fmt.Println(err)
        }    
    }()
    panic("I am an error ....")
}

在上例中,我们给panic传递的是一个字符串,所以可以直接print。但panic可以接受任何参数哦,这样用起来是不是有些浪费?

定制panic参数

如果我们需要panic向上层传递更多的信息,传一个字符串可能就不太够用了。

上边说道我们可以传递任意参数给panic,我们可以是不是可以考虑定义一个结构体,里边存起我们所有需要的信息。

type MyErr struct {
    level  int
    msg    string
    detail string
}

func main() {
    defer func() {
        if err := recover(); err != nil {
            if my_err, ok := err.(MyErr); ok {
                fmt.Println("level : ", my_err.level)
                fmt.Println("msg : ", my_err.msg)
                fmt.Println("detail : ", my_err.detail)
            } else {
                fmt.Println(err)
            }
        }
    }()

    panic(MyErr{1, "I'm msg..", "I'm detail.."})
}

好啦,这样我们在recover中就可以获取到结构体中的所有内容了,这样传递异常信息是不是很方便呢?

个人Web开发最佳实践

学习golang也有半年多了,对于golang web开发一直滚打摸爬,学习过一些框架。

个人整理了一个自己用的最佳实践,分享给大家,个中不足还请指正,如果大家有更好的意见和建议请随时联系我^_^。 (此处感谢陈子军老师的分享和指导)

笔者个人用过beego、gin和martini等框架。这些MVC框架都是由Model-View-Controller来分层的。有时候我们也会人为的在代码中分一些层级。

如果在model层发现一些会直接异常的问题时,一层一层向上返回,最终再由HTTP返回,这样无疑是很麻烦的,会很大程度的增加开发成本、增加维护成本。

因此我会在任何地方,直接向上层panic出来;然后在controller层,统一recover。抛出来的异常,是由我们自己定义的结构体,所有信息我们都可以处理。这里我们可以定义一个适合于HTTP返回的结构体。

type Error struct {
    Code int    `json:"code"`
    Msg  string `json:"msg"`
}


func _build(code int, defval string, custom ...string) Error {
    msg := defval
    if len(custom) > 0 {
        msg = custom[0]
    }

    return Error{
        Code: code,
        Msg:  msg,
    }
}

func DBError(msg ...string) Error {
    return _build(http.StatusInternalServerError, "DB Error", msg...)
}

func ParamError(msg ...string) Error {
    return _build(http.StatusBadRequest, "Param Error", msg...)
}

func ServerError(msg ...string) Error {
    return _build(http.StatusInternalServerError, "Server Error", msg...)
}

func PrivError(msg ...string) Error {
    return _build(http.StatusForbidden, "Forbidden Error", msg...)
}

func HandleError() {
    if err := recover(); err != nil {
        if msg, ok := err.(Error); ok {
            log.Print(msg)
            //我们自己程序内部触发的panic
            //TODO:这里我们根据错误内容可以做HTTP的返回等其他操作
        } else {
            if err_2, ok := err.(error); ok {
                //request的异常的panic
                msg := err_2.Error()
                log.Print(msg)
            }
        }
    }
}   

以上。

异常的处理方式,不同的项目架构应该有不同的方式。

这种方式其实导致了model层和controller层的耦合,不过在具体使用的时候,我会在model层和controller层中间再抽出一层manager层。model只负责处理数据,而manager才真正负责业务逻辑。不过如果我们复用manager层代码的时候,可能会有些麻烦。不过开发中复用manager层还是比较少的哈。

因此:这种方式只适用于:个人敏捷开发,周期短,需要代码维护成本较低,且有独立的逻辑模块的项目。