前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言技巧 - 4.【错误的三种处理】探索不同代码风格背后的哲学

Go语言技巧 - 4.【错误的三种处理】探索不同代码风格背后的哲学

作者头像
junedayday
发布2021-08-05 10:28:34
2890
发布2021-08-05 10:28:34
举报
文章被收录于专栏:Go编程点滴

背景介绍

先来模拟一个背景故事:

我们要去动物园进行一次游玩,主要行为有:

  • 进入动物园
  • 参观熊猫
  • 参观老虎
  • 离开动物园

第一种风格 - 经典Go语言的处理模式

代码语言:javascript
复制
// 一次旅游
type ZooTour1 interface {
 Enter() error // 进入
 VisitPanda(panda *Panda) error // 看熊猫
 VisitTiger(tiger *Tiger) error // 看老虎
 Leave() error // 离开
}

func Tour1(t ZooTour1, panda *Panda, tiger *Tiger) error {
 if err := t.Enter(); err != nil {
  return errors.WithMessage(err, "Enter failed")
 }

 if err := t.VisitPanda(panda); err != nil {
  return errors.WithMessagef(err, "VisitPanda failed, panda is %v", panda)
 }

 if err := t.VisitTiger(tiger); err != nil {
  return errors.WithMessagef(err, "VisitTiger failed, tiger is %v", tiger)
 }

 if err := t.Leave(); err != nil {
  return errors.WithMessage(err, "Leave failed")
 }

 return nil
}

这个处理风格非常经典。我们先不深入讨论,看完下一种后再做对比。

第二种风格 - 类似Try-Exception的代码风格

代码语言:javascript
复制
type ZooTour2 interface {
 Enter()
 VisitPanda(panda *Panda)
 VisitTiger(tiger *Tiger)
 Leave()

 Err() error // 统一处理
}

func Tour2(t ZooTour2, panda *Panda, tiger *Tiger) error {
 t.Enter()
 t.VisitPanda(panda)
 t.VisitTiger(tiger)
 t.Leave()

 if err := t.Err(); err != nil {
  return errors.WithMessage(err, "ZooTour failed")
 }
 return nil
}

这一整块的代码风格非常类似Try Exception,即先写业务逻辑,在最后对错误进行集中处理。

标准库中的bufio.Scanner就是参考这种方式实现的。

不过,由于Go语言对error的处理没有往外抛的机制,所以需要专门针对error做处理:

新手千万不要把panic的机制和错误处理混为一谈。

代码语言:javascript
复制
// ZooTour的具体实现,需要保存一个error
type myZooTour struct {
 err error
}

func (t *myZooTour) VisitPanda(panda *Panda) {
  // 遇到错误就要直接返回,再处理其余逻辑
 if t.err != nil {
  return
 }
 // ...
}

两种风格的对比

如果分别用一个词来形容前两种风格,我倾向于:

  1. 过程式的调用
  2. 集中处理错误

两种风格无法说清孰优孰劣,但有各自适宜的场景,我们来列举两种:

不关注错误的发生,而关注错误发生后的统一处理

内部存在大量的VisitXXX的函数,业务不关注发生错误的处理逻辑,而是关注整个流程完成后对error的处理。

例如,调用过程中如果出现了某个动物不在的问题,我们不关心,继续访问下一个,最后统一处理一下,看看有多少动物是不在的,打印一下即可。

这时,第二种处理方式明显会更简洁。

一般推荐在工具类采用这种方式,处理的内容比较直观,不会有太多异常case

错误有多种分类,会影响到程序的运行逻辑

例如VisitPanda(panda *Panda) 可能产生的错误分2类:

  • 不影响主流程:例如发现panda不见了,但还要接着继续参观其余动物
  • 影响主流程:例如突然收到动物园闭园的通知,不能参观其余动物了

这时,如果我们采用第二种风格,就得在每个函数内部加上很多特殊的业务逻辑:

代码语言:javascript
复制
func (t *myZooTour) VisitTiger(tiger *Tiger) {
  // 要针对特定error进行处理
 if t.err != nil && t.err != ErrorPandaMissing {
  return
 }
 // ...
}

很有可能出现一个问题:把Panda相关的error放到了Tiger里

所以,当错误的类型会影响到代码的运行逻辑,更适合第一种方案

一般情况下,我们的业务代码都是复杂的,这时候更适合写过程性的代码。

第三种风格 - 函数式编程

借用1中的接口定义,我们将它改造成函数式的风格:

代码语言:javascript
复制
type MyFunc func(t ZooTour1) error

func NewEnterFunc() MyFunc {
 return func(t ZooTour1) error {
  return t.Enter()
 }
}

func NewVisitPandaFunc(panda *Panda) MyFunc {
 return func(t ZooTour1) error {
  return t.VisitPanda(panda)
 }
}

func NewVisitTigerFunc(tiger *Tiger) MyFunc {
 return func(t ZooTour1) error {
  return t.VisitTiger(tiger)
 }
}

func NewLeaveFunc() MyFunc {
 return func(t ZooTour1) error {
  return t.Leave()
 }
}

然后调用代码示例如下:

代码语言:javascript
复制
func Tour3(t ZooTour1, panda *Panda, tiger *Tiger) error {
 var actions = []MyFunc{
  NewEnterFunc(),
  NewVisitPandaFunc(panda),
  NewVisitTigerFunc(tiger),
  NewLeaveFunc(),
 }

 return ContinueOnError(t, actions)
}

func ContinueOnError(t ZooTour1, funcs []MyFunc) error {
 for _, f := range funcs {
    if err := f(t);err != nil {
      // continue
    }
 }
}

func BreakOnError(t ZooTour1, funcs []MyFunc) error {
 for _, f := range funcs {
  if err := f(t);err != nil {
      // break
    }
 }
}

值得一提的是

  • ContinueOnError表示遇到了error只记录下来,但整个流程继续往下跑
  • BreakOnError表示遇到了error就直接break,不再跑接下来的MyFunc

方案三背后的思想与延伸

函数式编程最直观的一个特点是 延迟执行,也就是在引用MyFunc处不运行,在ContinueOnErrorBreakOnError里才是真正执行的地方。

这个延迟执行的特性,在这里还能达到一个很有意思的效果 - 分离关注点

关注点1 - 数据结构

样例中的[]MyFunc是一个切片,可以简单地理解为串行执行,也就是MyFunc执行完一个,再执行下一个。

我们可以引入更多的数据结构,例如[][]MyFunc,那就可以理解为增加了一层:

每一层中的[]MyFunc,代表这里面的所有MyFunc是平级的,也就可以采用一定的并发模式来加速执行。

关注点2 - 执行逻辑

ContinueOnErrorBreakOnError为例,它们都是对各种MyFunc的处理逻辑。我们还可以引入更多的执行逻辑,比如:

  • 容忍特定错误的情况
  • 对错误发生的数量有容忍上限
  • 保证一定的并发模式

流水线的模式

以我们常见的开发流水线为例,常见的包括:代码检查、单元测试、编译、CodeReview、自动化部署等。

这时,数据结构可以用来表示流水线的结构,执行逻辑可以用来表示流水线对异常的处置

比如说,我们可以编排为一种串行执行的逻辑:

  1. 代码检查
  2. 单元测试
  3. 编译
  4. CodeReview
  5. 自动化部署

我们想要加速整个流程,可以考虑修改为:

  1. 检查
    1. 代码检查
    2. 单元测试
    3. 编译
  2. CodeReview
  3. 自动化部署

结束语

本文介绍了三种对error的处理方式,代码实现相对简单,大家更需要关注背后的适用场景。

其中,第三种方式是一个很有意思的设计模式,可以帮助大家理解函数式编程的价值。

Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192 公众号: golangcoding

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go编程点滴 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景介绍
    • 第一种风格 - 经典Go语言的处理模式
      • 第二种风格 - 类似Try-Exception的代码风格
        • 两种风格的对比
          • 不关注错误的发生,而关注错误发生后的统一处理
          • 错误有多种分类,会影响到程序的运行逻辑
        • 第三种风格 - 函数式编程
          • 方案三背后的思想与延伸
            • 关注点1 - 数据结构
            • 关注点2 - 执行逻辑
            • 流水线的模式
          • 结束语
          相关产品与服务
          腾讯云代码分析
          腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档