一篇文章带你轻松搞懂Golang的error处理

目录
  • Golang中的error
  • error的几种玩法
    • 哨兵错误
    • 自定义错误类型
  • Wrap error
  • Golang1.13版本error的新特性
    • errors.UnWrap()
    • errors.Is()
    • errors.As()
  • error处理最佳实践
    • 优先处理error
    • 只处理error一次
    • 不要反复包装error
    • 不透明的错误处理
  • 简化错误处理
    • bufio.scan
    • errWriter
  • 何时该用panic
  • 小补充
  • 总结

Golang中的error

Golang中的 error 就是一个简单的接口类型。只要实现了这个接口,就可以将其视为一种 error

type error interface {
    Error() string
}

error的几种玩法

翻看Golang源码,能看到许多类似于下面的这两种error类型

哨兵错误

var EOF = errors.New("EOF")
var ErrUnexpectedEOF = errors.New("unexpected EOF")
var ErrNoProgress = errors.New("multiple Read calls return no data or error")

缺点:

1.让 error 具有二义性

error != nil不再意味着一定发生了错误
比如io.Reader返回io.EOF来告知调用者没有更多数据了,然而这又不是一个错误

2.在两个包之间创建了依赖

如果你使用了io.EOF来检查是否read完所有的数据,那么代码里一定会导入io包

自定义错误类型

一个不错的例子是os.PathError,它的优点是可以附带更多的上下文信息

type PathError struct {
    Op   string
    Path string
    Err  error
}

Wrap error

到这里我们可以发现,Golang 的 error 非常简单,然而简单也意味着有时候是不够用的

Golang的error一直有两个问题:

1.error没有附带file:line信息(也就是没有堆栈信息)

比如这种error,鬼知道代码哪一行报了错,Debug时简直要命

SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
       Error 1406: Data too long for column 'content' at row 1

2.上层error想附带更多日志信息时,往往会使用fmt.Errorf()fmt.Errorf()会创建一个新的error,底层的error类型就被“吞”掉了

var errNoRows = errors.New("no rows")

// 模仿sql库返回一个errNoRows
func sqlExec() error {
    return errNoRows
}

func serviceNoErrWrap() error {
    err := sqlExec()
    if err != nil {
        return fmt.Errorf("sqlExec failed.Err:%v", err)
    }

    return nil
}

func TestErrWrap(t *testing.T) {
    // 使用fmt.Errorf创建了一个新的err,丢失了底层err
    err := serviceNoErrWrap()
    if err != errNoRows {
        log.Println("===== errType don't equal errNoRows =====")
    }
}
-------------------------------代码运行结果----------------------------------
=== RUN   TestErrWrap
2022/03/26 17:19:43 ===== errType don't equal errNoRows =====

为了解决这个问题,我们可以使用github.com/pkg/error包,使用errors.withStack()方法将err保
存到withStack对象

// withStack结构体保存了error,形成了一条error链。同时*stack字段保存了堆栈信息。
type withStack struct {
    error
    *stack
}

也可以使用errors.Wrap(err, "自定义文本"),额外附带一些自定义的文本信息

源码解读:先将err和message包进withMessage对象,再将withMessage对象和堆栈信息包进withStack对象

func Wrap(err error, message string) error {
    if err == nil {
        return nil
    }
    err = &withMessage{
        cause: err,
        msg:   message,
    }
    return &withStack{
        err,
        callers(),
    }
}

Golang1.13版本error的新特性

Golang1.13版本借鉴了github.com/pkg/error包,新增了如下函数,大大增强了 Golang 语言判断 error 类型的能力

errors.UnWrap()

// 与errors.Wrap()行为相反
// 获取err链中的底层err
func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

errors.Is()

在1.13版本之前,我们可以用err == targetErr判断err类型
errors.Is()是其增强版:error 链上的任一err == targetErr,即return true

// 实践:学习使用errors.Is()
var errNoRows = errors.New("no rows")

// 模仿sql库返回一个errNoRows
func sqlExec() error {
    return errNoRows
}

func service() error {
    err := sqlExec()
    if err != nil {
        return errors.WithStack(err)    // 包装errNoRows
    }

    return nil
}

func TestErrIs(t *testing.T) {
    err := service()

    // errors.Is递归调用errors.UnWrap,命中err链上的任意err即返回true
    if errors.Is(err, errNoRows) {
        log.Println("===== errors.Is() succeeded =====")
    }

    //err经errors.WithStack包装,不能通过 == 判断err类型
    if err == errNoRows {
        log.Println("err == errNoRows")
    }
}
-------------------------------代码运行结果----------------------------------
=== RUN   TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====

例子解读:

因为使用errors.WithStack包装了sqlErrorsqlError位于error链的底层,上层的error已经不再是sqlError类型,所以使用==无法判断出底层的sqlError

源码解读:

  • 我们很容易想到其内部调用了err = Unwrap(err)方法来获取error链中底层的error
  • 自定义error类型可以实现Is接口来自定义error类型判断方法
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        // 支持自定义error类型判断
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

下面我们来看看如何自定义error类型判断:

自定义的errNoRows类型,必须实现Is接口,才能使用erros.Is()进行类型判断

type errNoRows struct {
    Desc string
}

func (e errNoRows) Unwrap() error { return e }

func (e errNoRows) Error() string { return e.Desc }

func (e errNoRows) Is(err error) bool {
    return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
}

// 模仿sql库返回一个errNoRows
func sqlExec() error {
    return &errNoRows{"Kaolengmian NB"}
}

func service() error {
    err := sqlExec()
    if err != nil {
        return errors.WithStack(err)
    }

    return nil
}

func serviceNoErrWrap() error {
    err := sqlExec()
    if err != nil {
        return fmt.Errorf("sqlExec failed.Err:%v", err)
    }

    return nil
}

func TestErrIs(t *testing.T) {
    err := service()

    if errors.Is(err, errNoRows{}) {
        log.Println("===== errors.Is() succeeded =====")
    }
}
-------------------------------代码运行结果----------------------------------
=== RUN   TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====

errors.As()

在1.13版本之前,我们可以用if _,ok := err.(targetErr)判断err类型
errors.As()是其增强版:error 链上的任一err与targetErr类型相同,即return true

// 通过例子学习使用errors.As()
type sqlError struct {
    error
}

func (e *sqlError) IsNoRows() bool {
    t, ok := e.error.(ErrNoRows)
    return ok && t.IsNoRows()
}

type ErrNoRows interface {
    IsNoRows() bool
}

// 返回一个sqlError
func sqlExec() error {
    return sqlError{}
}

// errors.WithStack包装sqlError
func service() error {
    err := sqlExec()
    if err != nil {
        return errors.WithStack(err)
    }

    return nil
}

func TestErrAs(t *testing.T) {
    err := service()

    // 递归使用errors.UnWrap,只要Err链上有一种Err满足类型断言,即返回true
    sr := &sqlError{}
    if errors.As(err, sr) {
        log.Println("===== errors.As() succeeded =====")
    }

    // 经errors.WithStack包装后,不能通过类型断言将当前Err转换成底层Err
    if _, ok := err.(sqlError); ok {
        log.Println("===== type assert succeeded =====")
    }
}
----------------------------------代码运行结果--------------------------------------------
=== RUN   TestErrAs
2022/03/25 18:09:02 ===== errors.As() succeeded =====

例子解读:

因为使用errors.WithStack包装了sqlErrorsqlError位于error链的底层,上层的error已经不再是sqlError类型,所以使用类型断言无法判断出底层的sqlError

error处理最佳实践

上面讲了如何定义error类型,如何比较error类型,现在我们谈谈如何在大型项目中做好error处理

优先处理error

当一个函数返回一个非空error时,应该优先处理error,忽略它的其他返回值

只处理error一次

  • 在Golang中,对于每个err,我们应该只处理一次。
  • 要么立即处理err(包括记日志等行为),return nil(把错误吞掉)。此时因为把错误做了降级,一定要小心处理函数返回值。

比如下面例子json.Marshal(conf)没有return err ,那么在使用buf时一定要小心空指针等错误

要么return err,在上层处理err

反例:

// 试想如果writeAll函数出错,会打印两遍日志
// 如果整个项目都这么做,最后会惊奇的发现我们在处处打日志,项目中存在大量没有价值的垃圾日志
// unable to write:io.EOF
// could not write config:io.EOF

type config struct {}

func writeAll(w io.Writer, buf []byte) error {
    _, err := w.Write(buf)
    if err != nil {
        log.Println("unable to write:", err)
        return err
    }

    return nil
}

func writeConfig(w io.Writer, conf *config) error {
    buf, err := json.Marshal(conf)
    if err != nil {
        log.Printf("could not marshal config:%v", err)
    }

    if err := writeAll(w, buf); err != nil {
        log.Println("count not write config: %v", err)
        return err
    }

    return nil
}

不要反复包装error

我们应该包装error,但只包装一次

上层业务代码建议Wrap error,但是底层基础Kit库不建议

如果底层基础 Kit 库包装了一次,上层业务代码又包装了一次,就重复包装了 error,日志就会打重

比如我们常用的sql库会返回sql.ErrNoRows这种预定义错误,而不是给我们一个包装过的 error

不透明的错误处理

在大型项目中,推荐使用不透明的错误处理(Opaque errors):不关心错误类型,只关心error是否为nil

好处:

耦合小,不需要判断特定错误类型,就不需要导入相关包的依赖。
不过有时候,这种处理error的方式不够用,比如:业务需要对参数异常error类型做降级处理,打印Warn级别的日志

type ParamInvalidError struct {
    Desc string
}

func (e ParamInvalidError) Unwrap() error { return e }

func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc }

func (e ParamInvalidError) Is(err error) bool {
    return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
}

func NewParamInvalidErr(desc string) error {
    return errors.WithStack(&ParamInvalidError{Desc: desc})
}
------------------------------顶层打印日志---------------------------------
if errors.Is(err, Err.ParamInvalidError{}) {
    logger.Warnf(ctx, "%s", err.Error())
    return
}
if err != nil {
    logger.Errorf(ctx, " error:%+v", err)
}

简化错误处理

Golang因为代码中无数的if err != nil被诟病,现在我们看看如何减少if err != nil这种代码

bufio.scan

CountLines() 实现了"读取内容的行数"功能

可以利用 bufio.scan() 简化 error 的处理:

func CountLines(r io.Reader) (int, error) {
    var (
        br    = bufio.NewReader(r)
        lines int
        err   error
    )

    for {
        _, err := br.ReadString('\n')
        lines++
        if err != nil {
            break
        }
    }

    if err != io.EOF {
        return 0, nilsadwawa
    }

    return lines, nil
}

func CountLinesGracefulErr(r io.Reader) (int, error) {
    sc := bufio.NewScanner(r)

    lines := 0
    for sc.Scan() {
        lines++
    }

    return lines, sc.Err()
}

bufio.NewScanner() 返回一个 Scanner 对象,结构体内部包含了 error 类型,调用Err()方法即可返回封装好的error

Golang源代码中蕴含着大量的优秀设计思想,我们在阅读源码时从中学习,并在实践中得以运用

type Scanner struct {
    r            io.Reader // The reader provided by the client.
    split        SplitFunc // The function to split the tokens.
    maxTokenSize int       // Maximum size of a token; modified by tests.
    token        []byte    // Last token returned by split.
    buf          []byte    // Buffer used as argument to split.
    start        int       // First non-processed byte in buf.
    end          int       // End of data in buf.
    err          error     // Sticky error.
    empties      int       // Count of successive empty tokens.
    scanCalled   bool      // Scan has been called; buffer is in use.
    done         bool      // Scan has finished.
}

func (s *Scanner) Err() error {
    if s.err == io.EOF {
        return nil
    }
    return s.err
}

errWriter

WriteResponse()函数实现了"构建HttpResponse"功能

利用上面学到的思路,我们可以自己实现一个errWriter对象,简化对 error 的处理

type Header struct {
    Key, Value string
}

type Status struct {
    Code   int
    Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
    _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
    if err != nil {
        return err
    }

    for _, h := range headers {
        _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
        if err != nil {
            return err
        }
    }

    if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
        return err
    }

    _, err = io.Copy(w, body)
    return err
}

type errWriter struct {
    io.Writer
    err error
}

func (e *errWriter) Write(buf []byte) (n int, err error) {
    if e.err != nil {
        return 0, e.err
    }

    n, e.err = e.Writer.Write(buf)

    return n, nil
}

func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error {
    ew := &errWriter{w, nil}

    fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

    for _, h := range headers {
        fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
    }

    fmt.Fprintf(w, "\r\n")

    io.Copy(ew, body)

    return ew.err
}

何时该用panic

在 Golang 中panic会导致程序直接退出,是一个致命的错误。

建议发生致命的程序错误时才使用 panic,例如索引越界、不可恢复的环境问题、栈溢出等等

小补充

errors.New()返回的是errorString对象的指针,其原因是防止字符串产生碰撞,如果发生碰撞,两个 error 对象会相等。
源码:

func New(text string) error {
    return &errorString{text}
}

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

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

实践:error1error2的text都是"error",但是二者并不相等

func TestErrString(t *testing.T) {
    var error1 = errors.New("error")
    var error2 = errors.New("error")

    if error1 != error2 {
        log.Println("error1 != error2")
    }
}
---------------------代码运行结果--------------------------
=== RUN   TestXXXX
2022/03/25 22:05:40 error1 != error2

参考文献
《Effective GO》
《Go程序设计语言》
https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling

总结

到此这篇关于Golang中error处理的文章就介绍到这了,更多相关Golang error处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅析golang开发Error的使用详解

    Error是Go语言开发中最基础也是最重要的部分,跟其他语言的try catch的作用基本一致,想想在PHP JAVA开发中,try catch 不会使用,或者使用不灵活,就无法感知到程序运行中出现了什么错误,是特别可怕的一件事. Error 基础 Golang中 error类型就是一个最基本interface,定义了一个Error()的方法 type error interface { Error() string } 平常使用最多的是这样的 errors.New("error")

  • golang常用库之pkg/errors包第三方错误处理包案例详解

    目录 golang常用库之-pkg/errors包 背景 关于官方errors包 官方errors包使用demo 什么是pkg/errors包 pkg/errors包使用demo 优秀开源项目使用案例 参考 golang常用库之-pkg/errors包 背景 golang自带了错误信息包error 只提供了简单的用法, 如errors.New(),和errors.Error()用来传递和获取错误信息. 明显官方的包已经不能满足了, 只能采取其他方法补救, 如:采用三方errors包. 关于官方e

  • golang 打印error的堆栈信息操作

    众所周知,目前的golang error只关注Error()信息,而不关注它的堆栈路径,对错误的定位大多数通过 log.SetFlags(log.Llongfile| log.LstdFlags) log.Println(e) 一旦代码分层,为了定位错误,可能出现每一个层次的同一个error,都得log好几次,比如: func DB()error{ return errors.New("time out") } func Dao()error{ if er:= DB();er!=nil

  • 一篇文章带你轻松搞懂Golang的error处理

    目录 Golang中的error error的几种玩法 哨兵错误 自定义错误类型 Wrap error Golang1.13版本error的新特性 errors.UnWrap() errors.Is() errors.As() error处理最佳实践 优先处理error 只处理error一次 不要反复包装error 不透明的错误处理 简化错误处理 bufio.scan errWriter 何时该用panic 小补充 总结 Golang中的error Golang中的 error 就是一个简单的接

  • 一篇文章带你彻底搞懂VUE响应式原理

    目录 响应式原理图 编译 创建compile类 操作fragment 获取元素节点上的信息 获取文本节点信息 操作fragment 响应式 数据劫持 收集依赖 响应式代码完善 Dep类 全局watcher用完清空 依赖的update方法 需要注意的一个地方 双剑合璧 总结 首先上图,下面这张图,即为MVVM响应式原理的整个过程图,我们本篇都是围绕着这张图进行分析,所以这张图是重中之重. 响应式原理图 一脸懵逼?没关系,接下来我们将通过创建一个简单的MVVM响应系统来一步步了解这个上图中的全过程.

  • 一篇文章带你彻底搞懂Redis 事务

    目录 Redis 事务简介 Redis 事务基本指令 实例分析 Redis 事务与 ACID 总结 Redis 事务简介 Redis 只是提供了简单的事务功能.其本质是一组命令的集合,事务支持一次执行多个命令,在事务执行过程中,会顺序执行队列中的命令,其他客户端提交的命令请求不会插入到本事务执行命令序列中.命令的执行过程是顺序执行的,但不能保证原子性.无法像 MySQL 那样,有隔离级别,出了问题之后还能回滚数据等高级操作.后面会详细分析. Redis 事务基本指令 Redis 提供了如下几个事

  • 一篇文章让你彻底搞懂js中的位置计算

    目录 引言 scroll Element.scroll() Element.scrollHeight/scrollWidth Element.scrollLeft/scrollTop 判断当前元素是否存在滚动条 判断用户是否滚动到底部 client MouseEvent.clientX/Y Element.clientHeight/clientWidth Element.clientTop/clientLeft offset MouseEvent.offsetX/offsetY offsetWi

  • 一篇文章带你轻松了解C# Lock关键字

    相信绝大多数.NET玩家和我一样,常常使用Timer这个对象,而在WPF中使用DispatcherTimer的人也是很多,DispatcherTimer是在UI线程跑的.我们的程序中大多数都会充斥很多Timer,可以理解它是一个线程,它继承自 System.Windows.Threading . 程序中也许会有一些静态变量或是单例模式的对象来让不同的页面进行交互,但也就是这样让每个线程之间打架提供了基础.因为资源是单独的,就像是脚踩两只船的人,必定会翻车.例如一个List集合,你在一个线程中对它

  • 一篇文章带你搞懂Python类的相关知识

    一.什么是类 类(class),作为代码的父亲,可以说它包裹了很多有趣的函数和方法以及变量,下面我们试着简单创建一个吧. 这样就算创建了我们的第一个类了.大家可以看到这里面有一个self,其实它指的就是类aa的实例.每个类中的函数只要你不是类函数或者静态函数你都得加上这个self,当然你也可以用其他的代替这个self,只不过这是python中的写法,就好比Java 中的this. 二.类的方法 1.静态方法,类方法,普通方法 类一般常用有三种方法,即为static method(静态方法),cl

  • 一篇文章带你搞懂Java线程池实现原理

    目录 1. 为什么要使用线程池 2. 线程池的使用 3. 线程池核心参数 4. 线程池工作原理 5. 线程池源码剖析 5.1 线程池的属性 5.2 线程池状态 5.3 execute源码 5.4 worker源码 5.5 runWorker源码 1. 为什么要使用线程池 使用线程池通常由以下两个原因: 频繁创建销毁线程需要消耗系统资源,使用线程池可以复用线程. 使用线程池可以更容易管理线程,线程池可以动态管理线程个数.具有阻塞队列.定时周期执行任务.环境隔离等. 2. 线程池的使用 /** *

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

  • 一篇文章带你搞定SpringBoot不重启项目实现修改静态资源

    一.通过配置文件控制静态资源的热部署 在配置文件 application.properties 中添加: #表示从这个默认不触发重启的目录中除去static目录 spring.devtools.restart.exclude=classpath:/static/** 或者使用: #表示将static目录加入到修改资源会重启的目录中来 spring.devtools.restart.additional-paths=src/main/resource/static 此时对static 目录下的静态

  • 一篇文章带你搞定 springsecurity基于数据库的认证(springsecurity整合mybatis)

    一.前期配置 1. 加入依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> &

随机推荐