Go 中的错误处理
Andrew Gerrand
2011 年 7 月 12 日
介绍
如果你编写过任何 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)
}
// 打开 *File f 后干点什么
你可以在 Go 中了解到更多关于 error
类型的内容, 但在本文中, 我们将着重于 error
并讨论一些错误处理的好方法.
错误类型
error
类型是一种接口类型. error
变量表示可以将自身描述为字符串的任何值. 这是接口的声明:
type error interface {
Error() string
}
跟所有内置类型一样, error
类型在 universe block 中是 predeclared.
最常用的 error
实现是 errors 包的未导出的 errorString
类型.
// errorString 是 error 的简单实现.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
你可以使用 errors.New
函数来构造这些值. 其接收一个字符串并将字符串转化为 errors.errorString
并作为 error
值返回.
// New 函数返回一个给定文本的错误
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")
}
// 实现
}
调用者将负参数传递给 Sqrt
比接收一个非零的 error
值 (其具体表现为 errors.errorString
值). 调用者可以通过调用 error
的 Error
函数来访问或仅打印错误字符串 (“math: square root of negative number”):
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 // 错误描述
Offset int64 // 读取偏移量字节后发生错误
}
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 // 是否超时错误?
Temporary() bool // 是否暂时性错误?
}
客户端代码可以使用类型声明来测试 net.Error
, 然后将瞬态网络错误与永久性网络错误区分开来. 例如, 网络搜索器在遇到临时错误时可能会休眠并重试, 否则会直接放弃.
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
简化重复错误处理
在 Go 中, 错误处理很重要. 该语言的设计和约定鼓励你明确检查错误发生的位置 (与其他语言中抛出异常并捕获的约定不同). 在某些情况下, 这会使 Go 代码变得冗长, 但幸运的是, 可以使用某些技巧来减少重复性错误处理.
思量下一个有 HTTP 处理器的 App Engine 应用程序, 该处理程序从数据存储中检索记录并使用模板对其进行格式化.
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 (“内部服务器错误”). 这看起来像是数量可控的代码, 但是添加更多的 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)
中调用接收者来调用函数.
现在当我们使用 Handle
函数 (HandleFunc
函数的替代) 注册 viewRecord
函数时, 使用 Handle
函数使 appHandler
是一个 http.Handler
(而不是一个 http.HandlerFunc
处理器函数).
func init() {
http.Handle("/view", appHandler(viewRecord))
}
有了这个基本的错误处理结构, 我们可以使其对用户更友好. 与仅显示错误字符串相比, 给用户提供带有适当 HTTP 状态码的简单错误信息, 同时将完整的错误记录到 App Engine 开发者控制台来进行调试对用户而言更加友好.
为此我们创建了一个 appError
结构体来包含一个 error
和一些其他字段:
type appError struct {
Error error
Message string
Code int
}
接下来我们修改 appHandler 类型来返回 *appError
值:
type appHandler func(http.ResponseWriter, *http.Request) *appError
(出于 Go 问答 中讨论的原因, 通常传回具体类型的错误而不是一个 error
, 这是正确的做法, 因为 ServeHTTP
是唯一可以看到该值并使用其内容的地方.)
并使 appHandler
的 ServeHTTP
方法具有向正确 HTTP 状态 Code
的用户显示 appError
的 Message
, 并将完整的 Error
记录到开发者控制台:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e 是 *appError, 不是 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
内部的恐慌状态中恢复, 并将错误记录为 “严重”, 同时告知用户 “发生了严重错误”. 这样可以很好的避免用户暴露由编程错误引起的难以理解的错误信息. 有关更多信息, 请参阅 Defer, Panic, and Recover.
总结
正确的错误处理是好软件的基本要求. 通过采用本文描述的技术, 你应该能够编写更加可靠和简洁的 Go 代码了.
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: