DarrenWang

心向往之


  • 首页

  • 归档

错误处理

发表于 2018-08-03 | 分类于 go开发

11.1 错误处理

Go语言主要的设计准则是:简洁、明白,简洁是指语法和C类似,相当的简单,明白是指任何语句都是很明显的,不含有任何隐含的东西,在错误处理方案的设计中也贯彻了这一思想。我们知道在C语言里面是通过返回-1或者NULL之类的信息来表示错误,但是对于使用者来说,不查看相应的API说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如:返回0是成功,还是失败,而Go定义了一个叫做error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。例如os.Open函数在打开文件失败时将返回一个不为nil的error变量

1
2
3
<!-- more -->

func Open(name string) (file *File, err error)

下面这个例子通过调用os.Open打开一个文件,如果出现错误,那么就会调用log.Fatal来输出错误信息:

1
2
3
4
5

f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}

类似于os.Open函数,标准包中所有可能出错的API都会返回一个error变量,以方便错误处理,这个小节将详细地介绍error类型的设计,和讨论开发Web应用中如何更好地处理error。

Error类型

error类型是一个接口类型,这是它的定义:

1
2
3
4

type error interface {
Error() string
}

error是一个内置的接口类型,我们可以在/builtin/包下面找到相应的定义。而我们在很多内部包里面用到的 error是errors包下面的实现的私有结构errorString

1
2
3
4
5
6
7
8
9

// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

你可以通过errors.New把一个字符串转化为errorString,以得到一个满足接口error的对象,其内部实现如下:

1
2
3
4
5

// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}

下面这个例子演示了如何使用errors.New:

1
2
3
4
5
6
7

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方法)被调用,以输出错误,请看下面调用的示例代码:

1
2
3
4
5

f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}

自定义Error

通过上面的介绍我们知道error是一个interface,所以在实现自己的包的时候,通过定义实现此接口的结构,我们就可以实现自己的错误定义,请看来自Json包的示例:

1
2
3
4
5
6
7

type SyntaxError struct {
msg string // 错误描述
Offset int64 // 错误发生的位置
}

func (e *SyntaxError) Error() string { return e.msg }

Offset字段在调用Error的时候不会被打印,但是我们可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子:

1
2
3
4
5
6
7
8

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类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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秒之后重试:

1
2
3
4
5
6
7
8

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}

错误处理

Go在错误处理上采用了与C类似的检查返回值的方式,而不是其他多数主流语言采用的异常方式,这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是我们通过复用检测函数来减少类似的代码。

请看下面这个例子代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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详解)。

1
2
3
4
5
6
7
8

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)
}
}

上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数:

1
2
3
4

func init() {
http.Handle("/view", appHandler(viewRecord))
}

当请求/view的时候我们的逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。

1
2
3
4
5
6
7
8
9
10

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错误码,然后打印出来相应的错误代码,其实我们可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,我们可以自定义返回的错误类型:

1
2
3
4
5
6

type appError struct {
Error error
Message string
Code int
}

这样我们的自定义路由器可以改成如下方式:

1
2
3
4
5
6
7
8
9
10

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)
}
}

这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13

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

  • 目录
  • 上一节: 错误处理,调试和测试
  • 下一节: 使用GDB调试

使用GDB调试

发表于 2018-08-03 | 分类于 go开发

11.2 使用GDB调试

开发程序过程中调试代码是开发者经常要做的一件事情,Go语言不像PHP、Python等动态语言,只要修改不需要编译就可以直接输出,而且可以动态的在运行环境下打印数据。当然Go语言也可以通过Println之类的打印数据来调试,但是每次都需要重新编译,这是一件相当麻烦的事情。我们知道在Python中有pdb/ipdb之类的工具调试,Javascript也有类似工具,这些工具都能够动态的显示变量信息,单步调试等。不过庆幸的是Go也有类似的工具支持:GDB。Go内部已经内置支持了GDB,所以,我们可以通过GDB来进行调试,那么本小节就来介绍一下如何通过GDB来调试Go程序。

阅读全文 »

Go怎么写测试用例

发表于 2018-08-03 | 分类于 go开发

11.3 Go怎么写测试用例

开发程序其中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。

阅读全文 »

小结

发表于 2018-08-03 | 分类于 go开发

11.4 小结

本章我们通过三个小节分别介绍了Go语言中如何处理错误,如何设计错误处理,然后第二小节介绍了如何通过GDB来调试程序,通过GDB我们可以单步调试、可以查看变量、修改变量、打印执行过程等,最后我们介绍了如何利用Go语言自带的轻量级框架testing来编写单元测试和压力测试,使用go test就可以方便的执行这些测试,使得我们将来代码升级修改之后很方便的进行回归测试。这一章也许对于你编写程序逻辑没有任何帮助,但是对于你编写出来的程序代码保持高质量是至关重要的,因为一个好的Web应用必定有良好的错误处理机制(错误提示的友好、可扩展性)、有好的单元测试和压力测试以保证上线之后代码能够保持良好的性能和按预期的运行。

阅读全文 »

部署与维护

发表于 2018-08-03 | 分类于 go开发

12 部署与维护

到目前为止,我们前面已经介绍了如何开发程序、调试程序以及测试程序,正如人们常说的:开发最后的10%需要花费90%的时间,所以这一章我们将强调这最后的10%部分,要真正成为让人信任并使用的优秀应用,需要考虑到一些细节,以上所说的10%就是指这些小细节。

阅读全文 »

应用日志

发表于 2018-08-03 | 分类于 go开发

12.1 应用日志

我们期望开发的Web应用程序能够把整个程序运行过程中出现的各种事件一一记录下来,Go语言中提供了一个简易的log包,我们使用该包可以方便的实现日志记录的功能,这些日志都是基于fmt包的打印再结合panic之类的函数来进行一般的打印、抛出错误处理。Go目前标准包只是包含了简单的功能,如果我们想把我们的应用日志保存到文件,然后又能够结合日志实现很多复杂的功能(编写过Java或者C++的读者应该都使用过log4j和log4cpp之类的日志工具),可以使用第三方开发的日志系统:logrus和seelog,它们实现了很强大的日志功能,可以结合自己项目选择。接下来我们介绍如何通过该日志系统来实现我们应用的日志功能。

阅读全文 »

网站错误处理

发表于 2018-08-03 | 分类于 go开发
# 12.2 网站错误处理 我们的Web应用一旦上线之后,那么各种错误出现的概率都有,Web应用日常运行中可能出现多种错误,具体如下所示:
阅读全文 »

应用部署

发表于 2018-08-03 | 分类于 go开发

12.3 应用部署

程序开发完毕之后,我们现在要部署Web应用程序了,但是我们如何来部署这些应用程序呢?因为Go程序编译之后是一个可执行文件,编写过C程序的读者一定知道采用daemon就可以完美的实现程序后台持续运行,但是目前Go还无法完美的实现daemon,因此,针对Go的应用程序部署,我们可以利用第三方工具来管理,第三方的工具有很多,例如Supervisord、upstart、daemontools等,这小节我介绍目前自己系统中采用的工具Supervisord。

daemon

阅读全文 »

小结

发表于 2018-08-03 | 分类于 go开发

12.5 小结

本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要,要创建一个能够基于最小维护平滑运行的应用,必须考虑这些问题。

阅读全文 »

备份和恢复

发表于 2018-08-03 | 分类于 go开发

12.4 备份和恢复

这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。

阅读全文 »
1…78910

王梦涛

100 日志
4 分类
4 标签
GitHub Weibo
© 2016 – 2018 王梦涛
由 Hexo 强力驱动 v3.6.0
|
主题 – NexT.Pisces v6.3.0