Skip to content

错误处理

本文档详细介绍Go语言的错误处理机制,包括error接口、错误创建、错误处理模式和最佳实践。

📋 目录

错误处理概述

Go语言的错误处理遵循"简洁、明确"的设计原则,通过显式的错误返回值来处理异常情况,避免了隐式的异常机制。

设计理念

Go语言的错误处理设计理念:

传统方式Go语言方式优势
返回-1、NULL等返回error接口类型安全,语义明确
异常抛出/捕获显式错误检查控制流清晰,强制处理
隐式错误传播显式错误传播错误路径可见,便于调试

基本错误处理

go
// 标准的错误处理模式
file, err := os.Open("filename.txt")
if err != nil {
    // 处理错误
    log.Fatal(err)
    return
}
defer file.Close()

// 继续正常逻辑
// ...

核心原则:

  • 错误是值,可以被检查、传递、处理
  • 通过与nil比较来判断是否有错误
  • 错误应该被显式处理,不能忽略

Error类型

error类型是一个接口类型,这是它的定义:

go
type error interface {
    Error() string
}

error是一个内置的接口类型,可以在/builtin/包下面找到相应的定义。而在很多内部包里面用到的 errorerrors包下面的实现的私有结构errorString

go
// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

可以通过errors.New把一个字符串转化为errorString,以得到一个满足接口error的对象,其内部实现如下:

go
// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

下面这个例子演示了如何使用errors.New:

go
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

在下面的例子中,在调用Sqrt的时候传递的一个负数,然后就得到了non-nilerror对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

go
f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

自定义Error

error是一个interface,所以在实现自己的包的时候,通过定义实现此接口的结构,就可以实现自己的错误定义,请看来自Json包的示例:

go
type SyntaxError struct {
    msg    string // 错误描述
    Offset int64  // 错误发生的位置
}

func (e *SyntaxError) Error() string { return e.msg }

Offset字段在调用Error的时候不会被打印,但可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子:

go
if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

需要注意的是,函数返回自定义错误时,返回值推荐设置为error类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如:

go
func Decode() *SyntaxError { // 错误,将可能导致上层调用者err!=nil的判断永远为true。
    var err *SyntaxError     // 预声明错误变量
    if 出错条件 {
        err = &SyntaxError{}
    }
    return err               // 错误,err永远等于非nil,导致上层调用者err!=nil的判断始终为true
}

原因见 http://golang.org/doc/faq#nil_error (需科学上网)

上面例子简单的演示了如何自定义Error类型。但是如果还需要更复杂的错误处理呢?此时,来参考一下net包采用的方法:

go
package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

在调用的地方,通过类型断言err是不是net.Error,来细化错误的处理,例如下面的例子,如果一个网络发生临时性错误,那么将会sleep 1秒之后重试:

go
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

错误处理

Go在错误处理上采用了与C类似的检查返回值的方式,而不是其他多数主流语言采用的异常方式,这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是通过复用检测函数来减少类似的代码。

请看下面这个例子代码:

go
func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

上面的例子中获取数据和模板展示调用时都有检测错误,当有错误发生时,调用了统一的处理函数http.Error,返回给客户端500错误码,并显示相应的错误数据。但是当越来越多的HandleFunc加入之后,这样的错误处理逻辑代码就会越来越多,其实可以通过自定义路由器来缩减代码

go
type appHandler func(http.ResponseWriter, *http.Request) error

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

上面定义了自定义的路由器,然后可以通过如下方式来注册函数:

go
func init() {
    http.Handle("/view", appHandler(viewRecord))
}

当请求/view的时候逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。

go
func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

上面的例子错误处理的时候所有的错误返回给用户的都是500错误码,然后打印出来相应的错误代码,其实可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,可以自定义返回的错误类型:

go
type appError struct {
    Error   error
    Message string
    Code    int
}

这样自定义路由器可以改成如下方式:

go
type appHandler func(http.ResponseWriter, *http.Request) *appError

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

这样修改完自定义错误之后,逻辑处理可以改成如下方式:

go
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

如上所示,在访问view的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。

基于 MIT 许可发布