Go 博客
错误处理与 Go
引言
如果您编写过 Go 代码,很可能遇到过内置的 error
类型。Go 代码使用 error
值来指示异常状态。例如,os.Open
函数在打开文件失败时会返回一个非零值的 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
类型,与其他所有内置类型一样,在 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
时,会收到一个非零值的 error
值(其具体表示是一个 errors.errorString
值)。调用者可以通过调用 error
的 Error
方法,或者直接打印它来访问错误字符串(“math: square root of…”)
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt 包通过调用其 Error() string
方法来格式化 error
值。
错误实现负责总结上下文。os.Open
返回的错误格式为“open /etc/passwd: permission denied”,而不仅仅是“permission denied”。我们的 Sqrt
返回的错误缺少有关无效参数的信息。
要添加该信息,一个有用的函数是 fmt
包的 Errorf
。它根据 Printf
的规则格式化字符串,并将其作为由 errors.New
创建的 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))
}
复杂的调用者可以使用类型断言来检查 NegativeSqrtError
并进行特殊处理,而那些仅将错误传递给 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 }
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
}
(这是 Camlistore 项目中一些实际代码的一个略微简化版本。)
error
接口只需要一个 Error
方法;特定的错误实现可能具有其他方法。例如,net 包按照惯例返回 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 处理程序,该处理程序从数据存储中检索记录并使用模板对其进行格式化。
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(“内部服务器错误”)的简单错误消息。这看起来代码量 manageable,但添加更多 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)
调用接收者来调用函数。
现在,在向 http 包注册 viewRecord
时,我们使用 Handle
函数(而不是 HandleFunc
),因为 appHandler
是一个 http.Handler
(而不是 http.HandlerFunc
)。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
有了这个基本的错误处理基础设施,我们可以使其更加用户友好。与其仅仅显示错误字符串,不如给用户提供带有适当 HTTP 状态码的简单错误消息,同时将完整的错误记录到 App Engine 开发者控制台以进行调试。
为此,我们创建一个包含 error
和其他一些字段的 appError
结构体
type appError struct {
Error error
Message string
Code int
}
接下来我们修改 appHandler 类型以返回 *appError
值
type appHandler func(http.ResponseWriter, *http.Request) *appError
(通常将错误的具体类型而不是 error
传回是错误的,原因在Go FAQ 中有讨论,但在这里是正确的做法,因为 ServeHTTP
是唯一看到该值并使用其内容的地方。)
并使 appHandler
的 ServeHTTP
方法将 appError
的 Message
与正确的 HTTP 状态码 Code
一起显示给用户,并将完整的 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
代码长度与原始版本相同,但现在每一行都有特定的含义,并且我们提供了更友好的用户体验。
改进不止于此;我们可以进一步改进应用程序中的错误处理。一些想法包括:
-
为错误处理程序提供一个漂亮的 HTML 模板,
-
当用户是管理员时,通过将堆栈跟踪写入 HTTP 响应来简化调试,
-
为
appError
编写一个构造函数,用于存储堆栈跟踪以便更轻松地调试, -
从
appHandler
内部的 panic 中恢复,将错误记录到控制台标记为“Critical”,同时告诉用户“发生了严重错误”。这是一个很好的细节,可以避免向用户暴露由编程错误引起的晦涩难懂的错误消息。有关更多详细信息,请参阅 Defer、Panic 和 Recover 文章。
结论
适当的错误处理是优秀软件的基本要求。通过采用本文介绍的技术,您应该能够编写更可靠、更简洁的 Go 代码。
下一篇文章:App Engine 上的 Go 现已正式可用
上一篇文章:Go 中的头等函数
博客索引