扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • Go语言Web编程

    • 课程介绍
  • Web基础

  • 表单

  • 访问数据库

  • session和数据存储

  • 文本处理

  • Web服务

  • 安全与加密

  • 国际化和本地化

  • 错误处理,调试和测试

    • 第1节:错误处理,调试和测试
    • 第2节:错误处理
    • 第3节:使用 GDB 调试
    • 第4节:Go 怎么写测试用例
    • 第5节:小结
  • 部署与维护

  • 如何设计一个Web框架

  • 扩展Web框架

扫码订阅《 》或入驻星球,即可阅读文章!

第2节:错误处理


ASTA谢

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

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

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

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

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

# Error 类型

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

type error interface {
	Error() string
}
1
2
3

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
}
1
2
3
4
5
6
7
8

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

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

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

func Sqrt(f float64) (float64, error) {
	if f < 0 {
		return 0, errors.New("math: square root of negative number")
	}
	// implementation
}
1
2
3
4
5
6

在下面的例子中,我们在调用 Sqrt 的时候传递的一个负数,然后就得到了non-nil 的 error 对象,将此对象与 nil 比较,结果为 true,所以 fmt.Println ( fmt 包在处理 error 时会调用 Error 方法 ) 被调用,以输出错误,请看下面调用的示例代码:

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

# 自定义 Error

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

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

func (e *SyntaxError) Error() string { return e.msg }
1
2
3
4
5
6

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
}
1
2
3
4
5
6
7

需要注意的是,函数返回自定义错误时,返回值推荐设置为 error 类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如:

func Decode() *SyntaxError { // 错误,将可能导致上层调用者 err!=nil 的判断永远为 true。
        var err *SyntaxError     // 预声明错误变量
        if 出错条件 {
            err = &SyntaxError{}
        }
        return err               // 错误,err 永远等于非 nil,导致上层调用者 err!=nil 的判断始终为 true
    }
1
2
3
4
5
6
7

原因见 http://golang.org/doc/faq#nil_error

上面例子简单的演示了如何自定义 Error 类型。但是如果我们还需要更复杂的错误处理呢?此时,我们来参考一下 net 包采用的方法:

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

1
2
3
4
5
6
7
8

在调用的地方,通过类型断言 err 是不是 net.Error, 来细化错误的处理,例如下面的例子,如果一个网络发生临时性错误,那么将会 sleep 1 秒之后重试:

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

# 错误处理

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

请看下面这个例子代码:

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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的例子中获取数据和模板展示调用时都有检测错误,当有错误发生时,调用了统一的处理函数 http.Error,返回给客户端 500 错误码,并显示相应的错误数据。但是当越来越多的 HandleFunc 加入之后,这样的错误处理逻辑代码就会越来越多,其实我们可以通过自定义路由器来缩减代码 ( 实现的思路可以参考第三章的 HTTP 详解 )。

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
5
6
7

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

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

当请求 /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)
}
1
2
3
4
5
6
7
8
9

上面的例子错误处理的时候所有的错误返回给用户的都是 500 错误码,然后打印出来相应的错误代码,其实我们可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,我们可以自定义返回的错误类型:

type appError struct {
	Error   error
	Message string
	Code    int
}
1
2
3
4
5

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

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

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

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
}
1
2
3
4
5
6
7
8
9
10
11
12

如上所示,在我们访问 view 的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。

# 总结

在程序设计中,容错是相当重要的一部分工作,在 Go 中它是通过错误处理来实现的,error 虽然只是一个接口,但是其变化却可以有很多,我们可以根据自己的需求来实现不同的处理,最后介绍的错误处理方案,希望能给大家在如何设计更好 Web 错误处理方案上带来一点思路。

  • Error 类型
  • 自定义 Error
  • 错误处理
  • 总结