Files
build-web-application-with-…/zh/11.1.md
2017-06-10 12:23:19 +08:00

221 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 11.1 错误处理
Go语言主要的设计准则是简洁、明白简洁是指语法和C类似相当的简单明白是指任何语句都是很明显的不含有任何隐含的东西在错误处理方案的设计中也贯彻了这一思想。我们知道在C语言里面是通过返回-1或者NULL之类的信息来表示错误但是对于使用者来说不查看相应的API说明文档根本搞不清楚这个返回值究竟代表什么意思比如:返回0是成功还是失败,而Go定义了一个叫做error的类型来显式表达错误。在使用时通过把返回的error变量与nil的比较来判定操作是否成功。例如`os.Open`函数在打开文件失败时将返回一个不为nil的error变量
```Go
func Open(name string) (file *File, err error)
```
下面这个例子通过调用`os.Open`打开一个文件,如果出现错误,那么就会调用`log.Fatal`来输出错误信息:
```Go
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
```
类似于`os.Open`函数标准包中所有可能出错的API都会返回一个error变量以方便错误处理这个小节将详细地介绍error类型的设计和讨论开发Web应用中如何更好地处理error。
## Error类型
error类型是一个接口类型这是它的定义
```Go
type error interface {
Error() string
}
```
error是一个内置的接口类型我们可以在/builtin/包下面找到相应的定义。而我们在很多内部包里面用到的 error是errors包下面的实现的私有结构errorString
```Go
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
```
你可以通过`errors.New`把一个字符串转化为errorString以得到一个满足接口error的对象其内部实现如下
```Go
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
```
下面这个例子演示了如何使用`errors.New`:
```Go
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
```
在下面的例子中我们在调用Sqrt的时候传递的一个负数然后就得到了non-nil的error对象将此对象与nil比较结果为true所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:
```Go
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
```
## 自定义Error
通过上面的介绍我们知道error是一个interface所以在实现自己的包的时候通过定义实现此接口的结构我们就可以实现自己的错误定义请看来自Json包的示例
```Go
type SyntaxError struct {
msg string // 错误描述
Offset int64 // 错误发生的位置
}
func (e *SyntaxError) Error() string { return e.msg }
```
Offset字段在调用Error的时候不会被打印但是我们可以通过类型断言获取错误类型然后可以打印相应的错误信息请看下面的例子:
```Go
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类型而非自定义错误类型特别需要注意的是不应预声明自定义错误类型的变量。例如
```Go
func Decode() *SyntaxError { // 错误将可能导致上层调用者err!=nil的判断永远为true。
var err *SyntaxError // 预声明错误变量
if 出错条件 {
err = &SyntaxError{}
}
return err // 错误err永远等于非nil导致上层调用者err!=nil的判断始终为true
}
```
原因见 http://golang.org/doc/faq#nil_error
上面例子简单的演示了如何自定义Error类型。但是如果我们还需要更复杂的错误处理呢此时我们来参考一下net包采用的方法
```Go
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
```
在调用的地方通过类型断言err是不是net.Error,来细化错误的处理例如下面的例子如果一个网络发生临时性错误那么将会sleep 1秒之后重试
```Go
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
```
## 错误处理
Go在错误处理上采用了与C类似的检查返回值的方式而不是其他多数主流语言采用的异常方式这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是我们通过复用检测函数来减少类似的代码。
请看下面这个例子代码:
```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加入之后这样的错误处理逻辑代码就会越来越多其实我们可以通过自定义路由器来缩减代码(实现的思路可以参考第三章的HTTP详解)。
```Go
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)
}
}
```
上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数:
```Go
func init() {
http.Handle("/view", appHandler(viewRecord))
}
```
当请求/view的时候我们的逻辑处理可以变成如下代码和第一种实现方式相比较已经简单了很多。
```Go
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错误码然后打印出来相应的错误代码其实我们可以把这个错误信息定义的更加友好调试的时候也方便定位问题我们可以自定义返回的错误类型
```Go
type appError struct {
Error error
Message string
Code int
}
```
这样我们的自定义路由器可以改成如下方式:
```Go
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)
}
}
```
这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式:
```Go
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虽然只是一个接口但是其变化却可以有很多我们可以根据自己的需求来实现不同的处理最后介绍的错误处理方案希望能给大家在如何设计更好Web错误处理方案上带来一点思路。
## links
* [目录](<preface.md>)
* 上一节: [错误处理,调试和测试](<11.0.md>)
* 下一节: [使用GDB调试](<11.2.md>)