介绍
如果你写过 Go 的代码,就一定遇到过 Go 的内置类型 error。一个 error 类型的值可用于指明程序的某种不正常的状态,比如,当打开文件失败时,os.Open 函数会返回一个非 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
只要知道上述一点关于 error 的内容,在 Go 中就可以做很多事,但在这篇文章中,我们会进一步地讨论 error 类型以及 Go 处理错误的最佳实践。
error 类型
error 类型是一种接口类型,error 值可以是任意被字符串所能表示的值,下方是接口的定义:
type error interface {
Error() string
}
与其它内置类型一样,error 类型也是提前定义,并且全局有效的。
使用的最多的 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 函数构建一个 error 值,该函数会把一个字符串转换成一个 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
}
当调用者传入一个负数时,函数会返回一个非 nil 的 error 值(实际类型为 errorString)。同时,调用者可以使用 error 的 Error 方法得到错误字符串,或者直接打印:
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt 包内部会调用 error 的 Error 方法。
error 值有必要对一次代码执行做一次总结,应当尽可能描述错误的细节,如“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 是一个接口类型,所以任意数据结构都可以作为 error 的值(只要实现了 Error 方法)。这样的话,调用者可以尽可能知道错误的具体信息。
假设,调用者想通过 recover 函数捕获到传入 Sqrt 函数的无效参数,那可以自定义一种错误类型,而不是使用默认的 errors.errorString。
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
在捕获到错误时,我们可以使用类型断言还得到无效参数并对其进行适当的处理。与之相对的是,获得错误时仅仅使用 fmt.Println 或 log.Fatal 进行打印,这并不会改变程序的行为。
正如另一个示例,json 包中定义了 SyntaxError 类型,该类型在 json.Decode 函数解析到错误的 JSON 语法时返回。
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
在 Error 方法中,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 方法,当然 error 实现也可以有其它的方法。比如在 net 包中,Error 实现了 error 接口,同时自己也是一个接口并定义了其它方法:
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
客户端可以为 net.Error 做测试并通过类型断言来判定当前的网络错误是不是暂时的。比如,web 爬虫遇到临时的网络错误时可以选择休眠或者停止执行。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
简化重复错误处理
在 Go 中,错误处理非常重要。同时,Go 的语言设计和约定也鼓励开发者显式地处理错误。在某些情况下,显式处理错误会使代码冗余,但幸运的是,一些编码技术可以最小化重复错误处理的代码。
试想一个 HTTP handler 的应用引擎(App Engine),handler 从数据库中获得数据并渲染到视图:
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 函数返回的 error。在这两种情况下,服务端给用户返回一个 HTTP 状态为 500 的消息。这看上去是组织良好的代码,但如果添加更多的 handler,你会发现存在大量的重复代码。
为了减少重复,我们可以定义 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 的 handler,为了解决这一问题,该类型可以实现 http.Hanlder 接口的 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。
现在我们要注册 handler 函数时,不使用默认的 http.HanlderFunc 类型。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
在这种方式下,可以对 500 的错误进行统一管理。程序可以给用户一个友好的 500 消息,同时我们还需要更好地记录错误信息。
我们可以定义 appError 结构,该结构包含了一个 error 以及其它字段:
type appError struct {
Error error
Message string
Code int
}
接下来,我们修改 appHandler 的返回值类型:
type appHandler func(http.ResponseWriter, *http.Request) *appError
然后,让 appHandler.ServeHTTP 方法给用户显示 appError.Message,并把错误信息打印在终端:
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 与上一个版本有相同行数的代码,但不同行错误处理又包含其具体的含义。 不止于此,在程序中,我们可以进一步提高错误处理的效率,如:
- 以 HTML 模板的方式展示错误;
- 使用栈追踪技术更好地进行调试;
- 为 appError 编写一个构造函数来存储栈信息;
- 使用 recover 和 panic 设计错误恢复机制;
结论
合适的错误处理是好软件的必要项,运用本文介绍的技术一定可以编写更可靠的 Go 代码。
评论