不想Go 错误处理太臃肿,可以参考这个代码设计

2022-10-27 10:44:03 浏览数 (2)

最近写了个程序,因为是急活(貌似没有不急的...),所以这个程序又是我东拷一段,西粘一块拼出来的。代码写完了后,感觉这代码屎一样,都快把自己看哭了。真的是在心里边写别骂,先是骂以前做这个项目的人蠢,项目搞的跟屎一样,后来代码跑起来了,顺利交工后,变成了骂我自己蠢,这么写又不是不能用!

又不是不能用

不过在这个过程中,先不提项目里的业务逻辑、接口设计合不合理的事儿,这个我觉得在时间紧,加上人员更迭快的时候,正常人都会能粘就粘,不行了就再包一层,别改出线上问题了就行。有一点我把自己蠢哭的是,Go 的这个错误处理也太TM蠢了,一个程序我写了七八个错误判断,我给你们用伪代码描述一下:

代码语言:javascript复制
err, file :=  接收传文件(文件)
if err != nil {
  记日志
  返回错误码相应
}

err, fh :=  打开上传文件(file)
if err != nil {
  记日志
  返回错误码相应
}

err, data := 把文件里的行记录解析/转换一下(row)
if err != nil {
  记日志
  返回错误码相应
}

err, data3 := 调一下第三方接口拿数据
if err != nil {
  记日志
  返回错误码相应
}

err, data2 := 调一下内部其他服务拿数据
if err != nil {
  记日志
  返回错误码相应
}

err := 写库
if err != nil {
  记日志
  返回错误码相应
}

上面这个例子毫不夸张,我相信各位在自己的项目里一定见过,如果你是做业务开发的会更常见。

这里有人肯定会问,Go的错误处理就这样你难道第一天见吗,还能被蠢哭。诶,这不是降本提效后人员少了一半,我们这帮级别没混上去的虚线Leader,这不又开始自己写代码了嘛,以前蠢又蠢不到自己。再加上以前的系统、项目分层、服务隔离整的还凑活,不会像上面这样,在控制层调这么多业务对象,把蠢瓜代码集中在了一起…… 官感马上不一样了。

于是乎我就在思考,有没有什么设计模式什么的,能把这些东西隐藏下去,应该有吧,没有什么是包一层代码解决不了的吧,实在不行就包两层……诶,咋一不小心把设计模式的精髓给说出来了。

Go 优雅处理错误的几种方案

我这几天在网上看了不少说,Go 错误处理的,但基本上都是说怎么自定义包装 error 、传递error 之类的,讲怎么在写 Go 代码时能更优雅更好看的文章比较少,写的最好的是左耳朵耗子老师在自己博客里介绍的两种方式。

下面的部分代码参考自老师的博客:https://coolshell.cn/articles/21140.html

一种是用函数式编程的 Closure 把相同的 if err !=nil 之类的代码抽象出来重新定义一个函数,但是这种方式会导致新的问题--在每个函数里都需要引入内部函数和一个 error 变量,所以咱就不多说了,有兴趣的可以去原博文查看。

这里直接介绍另外一种更好的,对项目侵入不是很大的方案给大家。在 Go 语言官方库 bufioScanner对象的错处理的实现方式可以给我们一点启发,它大概是这么实现的。

代码语言:javascript复制
scanner := bufio.NewScanner(input)

for scanner.Scan() {
    token := scanner.Text()
    // process token
}

if err := scanner.Err(); err != nil {
    // process the error
}

上面的代码我们可以看到,scanner在操作底层的I/O的时候,那个for-loop中没有任何的 if err !=nil 的情况,退出循环后有一个 scanner.Err() 的检查。看来使用了结构体的方式。

我们来看一下 Scanner类型的定义

代码语言:javascript复制
type Scanner struct {
 r            io.Reader
  ...//其他字段省略
 err          error    
}

这个类型内部持有一个error 在迭代执行 Scan 方法时,遇到错误后会往这个 error 中记录错误。

代码语言:javascript复制
func (s *Scanner) Scan() bool {
  ...// 其余代码省略
 for {
   if err != nil {
    s.setErr(err)
    return false
   }
}
  
func (s *Scanner) Err() error {
 if s.err == io.EOF {
  return nil
 }
 return s.err
}

所以我们可以参考这个思路继续搞下去。比如来一个读取业务对象的

上面这个示例相信大家很容易看懂,不过,其使用场景也就只能在对于同一个业务对象的不断操作下可以简化错误处理,对于多个业务对象的话,还是得需要各种 if err != nil的方式。

那有什么办法呢,咱们之前说过一次:没有什么是包一层代码解决不了的吧,实在不行就包两层。那么接下来我们再做一层包装,以下是我对解决这个问题的一点点理解,会借鉴一点DDD中分层的概念解决这个事情。

更容易落地的方案

刚才那个例子的问题是只适合减少单个业务对象逻辑操作中的 if err != nill 判断,那么针对这块呢,咱们可以把涉及多个业务对象的操作放在一个应用服务里,把刚才在业务对象做的错误处理判断拿到应用服务里,这样业务对象里,比如Model之类的下层模块里,就还能按照正常的流程写代码了,不用每个方法开头都要先判断一下。

这里提前说一下,在一些架构设计里会分应用服务和领域服务,这两者的概念完全不一样,应用服务是面向产品需求的用例实现的,负责业务用例流的任务协调,就是我们实现API时,往往会控制层调应用服务,多个不同的业务对象可以放到一个应用服务里。而领域服务是专一给一个领域的,这块我就不多解释了,DDD这些我也是看了几本书,看过COLA框架的实现,还在似懂非懂的水平。

总之记住一点,通过应用服务可以协调多个业务对象执行任务,同时我们上面业务对象加的那些错误处理抽离到应用服务层里,让业务对象更专注自己的职责。这样的话,你的服务层代码,可能就得变成了这样

然后我们的控制层呢,调用应用服务层拿到结果,并且在这个时候判断整个需求任务执行的过程中有没有错误,有的话记录错误,返回错误响应给客户端。

Go 错误处理的基础

之前分享过一篇文章 关于Go程序错误处理的一些建议 说的是我们应该怎么用好 Go 的error 接口,自定义错误,包装整个错误链等相关的技能。跟本文的内容关联起来看,可能会对错误处理有个更全局的理解,在这里也推荐给大家。

总结

今天给大家分享了一些在让Go代码的错误处理更优雅上,我学到和​想到的一些东西。其实大家可以发现,我们是把多个 if err != nil 分散到了多个方法里,这样代码最起码从感官上看起来比在一个方法里写七八个错误判断更好一点。

对错误处理方面你有哪些见解呢,欢迎在评论区里积极发言,喜欢这篇文章还请帮忙来个点赞在看加分享吧,接下来内容还在向你们招手

0 人点赞