Go的官方博客有很多的干货,这篇文章Error handling and Go说明了Go错误处理的一些小技巧。
如果你折腾过一阵子Go语言,那么可能已经见到过了内置的error
类型。Go语言中使用error
类型来表示错误状态。例如,函数os.open
在打开文件失败时,会返回一个non-nil的error
值。
func Open(name string) (file *File, err error)
下面的代码调用os.Open
打开文件。如果发生错误,调用log.Fatal
打印一条错误信息,然后终止。
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
在Go语言中,只要知道error
的类型就能作很多事情了,但是在本文中会进一步介绍error
,并讨论一些好的错误处理方式。
error
类型是一种接口。一个error
变量表示任何一个可以将自身表示成字符串的值。接口的声明如下:
type error interface {
Error() string
}
error
类型,像所有内置的类型,在universe block中预先定义好了。
最常用到的error
实现是errors包的不可导出类型errorString
。
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
通过errors.New
函数,可以构建出一个这样的值。这个函数把接收的字符串转换成errors.errorString
,返回一个error
值。
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
可以像这样使用errors.New
:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
调用者传递一个负值给函数Sqrt
就会收到一个non-nil的error
值(具体类型是errors.errorString
)。调用者通过调用error
的Error
方法或者把错误打印出来就能够得到错误的字符串(”math: square root of…”)。
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt
包通过调用Error() string
方法来格式化error
值。
error
的实现负责给出上下文错误信息。os.Open
返回的错误格式化成”open /etc/passwd: permission denied,”而不仅仅是”permission denied.”。我们的Sqrt
返回的错误没有关于非法参数的信息。
要增加这种信息,可以使用fmt
包的函数Errorf
。它像Printf
一样格式化字符串,并将其作为一个error返回。
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
虽然在多数场合中,fmt.Errorf
已经能够应付了,但是由于error
实际上是一种接口,我们可以使用任意数据结构表示错误值,这样调用者就能获取到错误的细节。
例如,假设调用方想从传递负值给函数Sqrt
的错误中恢复。通过定义一个新的错误实现,而不是errors.errorString
,就可以了。
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
这样,有经验的调用方就可以通过type assertion
来检测NegativeSqrtError
并处理它,然而只是将这个错误传递给fmt.Println
或者log.Fatal
不会看到行为上的改变。
作为另一个例子,json
包中当函数json.Decode
在解析JSON blob遇到语法错误时会返回一个SyntaxError
类型的错误。
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
在默认的格式化中没有关于域Offset
,但是调用放可以用它来增加文件和行号到错误信息中:
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
接口只要求Error
方法;特殊的错误实现可能包含其他的方法。例如,包net
会返回常见的error
类型的错误,但是有些error
的实现包含接口net.Error
定义的额外方法:
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
客户端代码可以通过类型检测net.Error
,从而区分网络错误是临时的还是永久的。例如,当网络爬虫遇到临时错误时可以休眠然后重试,否则放弃。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
在Go语言中,错误处理是非常重要的。语言设计和规范鼓励在错误产生的地方检测错误(有别于其他语言通过抛出并捕获异常)。在有些场合,这种做法导致Go代码冗余,所幸的是可以通过某些方法最小化重复的出错处理。
考虑这样一个App Engine应用,它的HTTP处理器负责从datastore中获取一条记录然后根据模板格式化。
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)
}
}
上面的函数负责处理由datastore.Get
和viewTemplate
的Execute
方法返回的错误。在两种情形中,都会通过HTTP的状态码500(”Internal Server Error”)给用户返回一条简单的错误信息。这样的代码量看来还能接收,但是增加更多的HTTP处理器,你就会看到大量的一致的错误处理代码。
要减少重复,我们可以定义我们自己的HTTP appHandler
类型,它包含一个error
返回值:
type appHandler func(http.ResponseWriter, *http.Request) error
如此一来,就可以修改函数viewRecord
返回错误:
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)
}
这个函数跟原始版本相比要简洁些,问题是http包不认识返回值类型为error
的函数。要解决这个问题,可以在appHandler
上实现http.Handler
接口的ServeHTTP
方法:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
ServeHTTP
方法调用appHandler
函数,并给出返回给用户的错误(如果有的话)。注意到这个方法的接收者是,fn
,是一个函数。(Go可以做到!)方法通过表达式fn(w,r)
最终调用函数fn
。
现在在http包中注册函数viewRecord
时,我们使用函数Handle
函数(而不是HandleFunc
),因为appHandler
是一个http.Handler
(而不是http.HandlerFunc
)。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
通过采用这种基本的错误处理结构,对用户来说变得更加友好。 除了简单的给出错误字符串,如果开发者在调试的错误日志中能看到一个简单的错误消息以及HTTP状态码就更好了。
要实现这个,我们声明了一个appError
结构,包含一个error
和其他域:
type appError struct {
Error error
Message string
Code int
}
接下来,我们修改appHandler的类型让它返回 *appError
:
type appHandler func(http.ResponseWriter, *http.Request) *appError
(通产来说,返回一个具体类型的错误而非一般的error
是不对的,具体原因见the Go FAQ,但是在这里这样做是可以的,因为ServeHTTP
是唯一看到这个错误并处理它的地方。)
修改appHandler
方法ServeHTTP
,使得用户能够获取appError
的Message
以及HTTP状态码,同时将Error
打印到控制台:
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)
}
}
最后,修改viewRecord
的返回值,这样当遇到错误时就可以返回更多上下文信息:
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
}
这个版本的viewRecord
跟原来那个一样长,但是每一行都有特殊的含义,同时用户体验得到了提升。
这并没有完,我们的应用还可以进一步改善错误处理。例如:
appError
写一个constructor函数来存储栈信息从而简化调试appHandler
的panic中recover,将错误以Critical
级别记录到控制台,同时告诉用户”a serious error has occurred.”。这有助于避免用户面对由于程序错误导致的不可预知错误。更多细节见 Defer, Panic, and Recover。正确的错误处理是良好的软件所必须的。通过应用本文介绍的方法,可以写出更可靠的,简介的Go代码。
作者 Andrew Gerrand
2013-10-02@深圳 坪洲
评论