188 lines
8.0 KiB
Markdown
188 lines
8.0 KiB
Markdown
# 11.1 错误处理
|
||
Go语言设计的时候主要的特点是:简洁、明白,简洁是指语法和C类似,相当的简单,明白是指任何语句都是很明显的,不含有任何隐式的东西,Go在设计错误的时候也是一样。我们知道C语言里面返回错误是使用-1或者nil之类的返回信息表示错误,但是对于使用者来说这个返回值根本不知道什么意思,而Go里面当发生异常时返回一个Error类型,通过前面编写的代码,我们发现在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)
|
||
}
|
||
|
||
其实这样的error返回在Go语言的很多内置包里面有很多,我们这个小节将详细的介绍这些error是怎么设计的,以及在我们设计的Web应用如何更好的处理error。
|
||
## Error类型
|
||
error类型是一个接口类型,这是它的定义:
|
||
|
||
type error interface {
|
||
Error() string
|
||
}
|
||
|
||
error是一个内置的类型变量,我们可以在/builtin/包下面找到相应的定义。而我们在很多内部包里面用到的 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`把一个字符串转化为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的值,那么我们就可以根据判断调用fmt.Println打印出来错误,fmt包在处理error时会调用Error方法打印出来错误信息,请看下面调用的示例代码:
|
||
|
||
f, err := Sqrt(-1)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
}
|
||
|
||
## 自定义Error
|
||
通过上面的介绍我们知道error是一个interface定义,那么在一些我们自定义的包里面我们可以实现这接口定义,从而实现自己的错误定义,请看下面Json包中定义的自定义错误这个例子:
|
||
|
||
type SyntaxError struct {
|
||
msg string // 错误描述
|
||
Offset int64 // 错误发生的位置
|
||
}
|
||
|
||
func (e *SyntaxError) Error() string { return e.msg }
|
||
|
||
offset字段在调用Error的时候不会被打印,但是我们可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子:
|
||
|
||
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包下面的错误定义:
|
||
|
||
package net
|
||
|
||
type Error interface {
|
||
error
|
||
Timeout() bool // Is the error a timeout?
|
||
Temporary() bool // Is the error temporary?
|
||
}
|
||
|
||
这样我们在调用的地方可以通过类型断言判断是否是net.Error,然后根据错误的类型判断如何处理,例如下面的例子,如果一个网络发生临时性错误,那么可以sleep几秒之后重试:
|
||
|
||
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
|
||
time.Sleep(1e9)
|
||
continue
|
||
}
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
## 错误处理
|
||
Go语言里面错误处理是非常重要的,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加入之后,这样的错误处理逻辑代码就会越来越多,其实我们可以通过自定义路由器来缩减代码。
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数:
|
||
|
||
func init() {
|
||
http.Handle("/view", appHandler(viewRecord))
|
||
}
|
||
|
||
当请求/view的时候我们的逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。
|
||
|
||
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错误码,然后打印出来相应的错误代码,其实我们可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,我们可以自定义返回的错误类型:
|
||
|
||
type appError struct {
|
||
Error error
|
||
Message string
|
||
Code int
|
||
}
|
||
|
||
这样我们的自定义路由器可以改成如下方式:
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式:
|
||
|
||
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的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个现实的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。
|
||
|
||
## 总结
|
||
错误处理在Go语言是相当重要的,error虽然只是一个接口,但是他的变化可以很多,我们可以根据自己的需求来实现不同程度的处理,而且自定义错误处理也是相当的方便,最后介绍的错误处理机制也是给大家提供了一个如何友好的提示错误,并且如何做到更好的扩展性的一种设计,对于我们后期的运维,错误定位都起到了相当好的作用。
|
||
|
||
## links
|
||
* [目录](<preface.md>)
|
||
* 上一节: [错误处理,调试和测试](<11.md>)
|
||
* 下一节: [使用GDB调试](<11.2.md>)
|