文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Golang的error处理方法有哪些

2023-07-02 16:56

关注

这篇文章主要介绍“Golang的error处理方法有哪些”,在日常操作中,相信很多人在Golang的error处理方法有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang的error处理方法有哪些”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

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

缺点:

让 error 具有二义性

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

在两个包之间创建了依赖

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

自定义错误类型

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

type PathError struct {    Op   string    Path string    Err  error}

Wrap error

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

Golang的error一直有两个问题:

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

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

var errNoRows = errors.New("no rows")// 模仿sql库返回一个errNoRowsfunc 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   TestErrWrap2022/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链中的底层errfunc 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库返回一个errNoRowsfunc 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   TestErrIs2022/03/25 18:35:00 ===== errors.Is() succeeded =====

例子解读:

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

源码解读:

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库返回一个errNoRowsfunc 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   TestErrIs2022/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}// 返回一个sqlErrorfunc sqlExec() error {    return sqlError{}}// errors.WithStack包装sqlErrorfunc 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   TestErrAs2022/03/25 18:09:02 ===== errors.As() succeeded =====

例子解读:

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

error处理最佳实践

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

优先处理error

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

只处理error一次

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

要么return err,在上层处理err

反例:

// 试想如果writeAll函数出错,会打印两遍日志// 如果整个项目都这么做,最后会惊奇的发现我们在处处打日志,项目中存在大量没有价值的垃圾日志// unable to write:io.EOF// could not write config:io.EOFtype 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   TestXXXX2022/03/25 22:05:40 error1 != error2

到此,关于“Golang的error处理方法有哪些”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯