范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

深入剖析一套在Go中传递返回暴露错误,便于回查的解决方案

  作者:andruzhang,腾讯 IEG 后台开发工程师
  在使用 Go 开发的后台服务中,对于错误处理,一直以来都有多种不同的方案,本文探讨并提出一种从服务内到服务外的错误传递、返回和回溯的完整方案,还请读者们一起讨论。 问题提出
  在后台开发中,针对错误处理,有三个维度的问题需要解决: 函数内部的错误处理: 这是一个函数在执行过程中遇到各种错误时的错误处理。这是一个语言级的问题 函数/模块的错误信息返回: 一个函数在操作错误之后,要怎么将这个错误信息优雅地返回,方便调用方(也要优雅地)处理。这也是一个语言级的问题 服务/系统的错误信息返回: 微服务/系统在处理失败时,如何返回一个友好的错误信息,依然是需要让调用方优雅地理解和处理。这是一个服务级的问题,适用于任何语言 函数内部的错误处理
  一个面向过程的函数,在不同的处理过程中需要 handle 不同的错误信息;一个面向对象的函数,针对一个操作所返回的不同类型的错误,有可能需要进行不同的处理。此外,在遇到错误时,也可以使用断言的方式,快速中止函数流程,大大提高代码的可读性。
  在许多高级语言中都提供了 try ... catch 的语法,函数内部可以通过这种方案,实现一个统一的错误处理逻辑。而即便是 C 这种 "中级语言" 虽然没有,但是程序员也可以使用宏定义的方式,来实现某种程度上的错误断言。
  但是,对于 Go 的情况就比较尴尬了。 Go 的错误断言
  我们先来看断言,我们的目的是,仅使用一行代码就能够检查错误并终止当前函数。由于没有 throw,没有宏,如果要实现一行断言,有两种方法。
  第一种是把 if 的错误判断写在一行内,比如: if err != nil { return err }
  第二种方法是借用 panic 函数,结合 recover 来实现: func SomeProcess() (err error)     defer func() {         if e := recover(); e != nil {             err = e.(error)         }     }()      assert := func(cond bool, f string, a ...interface{}) {         if !cond {             panic(fmt.Errorf(f, a...))         }     }      // ...      err = DoSomething()     assert(err == nil, "DoSomething() error: %w", err)      // ... }
  这两种方法都值得商榷。
  首先,将 if 写在同一行内的问题有: 这种写法,虽然理论上符合 Go 代码规范,但是在实操中,花括号不换行这一点还是有点争议的,笔者在实际代码中也很少见到过 不够直观,而且在花括号中也不方便写其他语句,原因是 Go 的规范中强烈不建议使用 ; 来分隔代码语句(if 判断除外)
  至于第二种方法,我们要分情况看; 首先 panic 的设计原意,是在当程序或协程遇到严重错误,完全无法继续运行下去的时候,才会调用(比如段错误、共享资源竞争错误)。这相当于 Linux 中 FATAL 级别的错误日志。仅仅用来进行普通的错误处理(ERROR 级别),杀鸡用牛刀了。 panic 调用本身,相比于普通的业务逻辑,的系统开销是比较大的。而错误处理这种事情,可能是常态化逻辑,频繁的 panic - recover 操作,也会大大降低系统的吞吐。
  不过使用 panic 来断言的方案,虽然在业务逻辑中基本上不用,但在测试场景下则是非常常见的。测试嘛,用牛刀有何不可?稍微大一点的系统开销也没啥问题。对于 Go 来说,非常热门的单元测试框架  goconvey  就是使用 panic 机制来实现单元测试中的断言,用的人都说好。
  综上,在 Go 中,对于业务代码,笔者不建议采用断言,遇到错误的时候建议还是老老实实采用这种格式: if err := DoSomething(); err != nil {     // ... }
  而在单测代码中,则完全可以大大方方地采用类似于 goconvey 之类基于 panic 机制的断言。 Go 的try ... catch
  众所周知 Go 是没有 try ... catch 的,而且从官方的态度来看,短时间内也没有考虑的计划。但程序员有这个需求呀。笔者采用的方法,是将需要返回的 err 变量在函数内部全局化,然后结合 defer 统一处理: func SomeProcess() (err error) { // <-- 注意,err 变量必须在这里有定义     defer func() {         if err == nil {             return         }          // 这下面的逻辑,就当作 catch 作用了         if errors.Is(err, somepkg.ErrRecordNotExist) {             err = nil       // 这里是举一个例子,有可能捕获到某些错误,对于该函数而言不算错误,因此 err = nil         } else if errors.Like(err, somepkg.ErrConnectionClosed) {             // ...          // 或者是说遇到连接断开的操作时,可能需要做一些重连操作之类的;甚至乎还可以在这里重连成功之后,重新拉起一次请求         } else {             // ...         }     }()      // ...      if err = DoSomething(); err != nil {         return     }      // ... }
  这种方案要特别注意变量作用域问题.比如前面的 if err = DoSomething(); err != nil { 行,如果我们将 err = ... 改为 err := ...,那么这一行中的 err 变量和函数最前面定义的 (err error) 不是同一个变量,因此即便在此处发生了错误,但是在 defer 函数中无法捕获到 err 变量了。
  在 try ... catch 方面,笔者其实没有特别好的方法来模拟,即便是上面的方法也有一个很让人头疼的问题:defer 写法导致错误处理前置,而正常逻辑后置了,从可读性的角度来说非常不友好。因此也希望读者能够指教。同时还是希望 Go 官方能够继续迭代,支持这种语法。 函数/模块的错误信息返回
  这一点在 Go 里面,一开始看起来还是比较统一的,这就是 Go 最开始就定义的 error 类型,以系统标准的方式,统一了进程内函数级的错误返回模式。调用方使用 if err != nil 的统一模式,来判断一个调用是不是成功了。
  但是随着 Go 的逐步推广,由于 error 接口的高自由度,程序员们对于 "如何判断该错误是什么错误" 的时候,出现了分歧。 Go 1.13 之前
  在 Go 1.13 之前,对于 error 类型的传递,有三种常见的模式: ==流派
  这个流派很简单,就是将各种错误信息直接定义为一个类枚举值的模式,比如: var (     ErrRecordNotExist   = errors.New("record not exist")     ErrConnectionClosed = errors.New("connection closed")     // ... )
  当遇到相应的错误信息时,直接返回对应的 error 类枚举值就行了。对于调用方也非常方便,可以采用 switch - case 来判断错误类型: switch err {     case nil:         // ...     case ErrRecordNotExist:         // ...     default:         // ...     }
  个人觉得这种设计模式本质上还是 C error code 模式。 类型断言流派
  这种流派则是充分使用了 "error 是一个 interface" 的特性,重新自定义一个 error 类型。一方面是用不同的类型来表示不同的错误分类,另一方面则能够实现对于同一错误类型,能够给调用方提供更佳详尽的信息。举个例子,我们可以定义多个不同的错误类型如下: type ErrRecordNotExist errImpl  type ErrPermissionDenined errImpl  type ErrOperationTimeout errImpl  type errImpl struct {     msg string }  func (e *errImpl) Error() string {     return e.msg }
  对于调用方,则通过以下代码来判断不同的错误: if err == nil {         // OK     } else if _, ok := err.(*ErrRecordNotExist); ok {         // 处理记录不存在的错误     } else if _, ok := err.(*ErrPermissionDenined); ok {         // 处理权限错误     } else {         // 处理其他类型的错误     } fmt.Errorf流派if err := DoSomething(); err != nil {         return fmt.Errorf("DoSomething() error: %v", err)     }
  这种模式,一方面可以透传底层错误,另一方面又可以添加自定义的信息。但对于调用方而言,灾难在于如果要判断某一个错误的具体类型,只能用 strings.Contains() 来实现,而错误的具体描述文字是不可靠的,同一类型的信息可能会有不同的表达;而在 fmt.Errorf 的过程中,各个业务添加的额外信息也可能会有不同的文字,这带来了极大的不可靠性,提高了模块之间的耦合度。 Go 1.13 之后
  在 go 1.13 版本发布之后,针对 fmt.Errorf 增加了 wraping 功能,并在 errors 包中添加了 Is() 和 As() 函数。关于这个模式的原理和使用已经有很多文章了,本文就不再赘述。
  这个功能,合并并改造了前文的所谓 "== 流派" 和 "fmt.Errorf" 流派,统一使用 errors.Is() 函数;此外,也算是官方对类型断言流派的认可(专门用 As() 函数来支持)。
  在实际应用中,函数/模块透传错误时,应该采用 Go 的 error wrapping 模式,也就是 fmt.Errorf() 配合 %w 使用,业务方可以放心地添加自己的错误信息,只要调用方统一采用 errors.Is() 和 errors.As() 即可。 服务/系统的错误信息返回传统方案
  服务/系统层面的错误信息返回,大部分协议都可以看成是 code - message 模式或者是其变体: code 是数字或者预定义的字符串,可以视为整型或者是字符串类型的枚举值 如果是数字的话,大部分情况下是使用 0 表示成功,小部分则采用一个比较规整的十进制数字表示成功,比如 1000、10000 等 如果是预定义的字符串,那么是使用 "success"、"OK" 等字符串表示成功,或者是直接以空字符串、甚至是不返回字符串字段来表示成功
  message 字段则是错误信息的具体描述,大部分情况下都是一个人类可读的句子 一般而言,只有当 code 表示错误的时候,这个 message 字段才有返回的必要。
  这种模式的特点是:code 是给程序代码使用的,代码判断这是一个什么类型的错误,进入相应的分支处理;而 message 是给人看的,程序可以以某种形式抛出或者记录这个错误信息,供用户查看。 存在问题
  在这一层面有什么问题呢?code for computer,message for user,好像挺好的。
  但有时候,我们可能会收到用户/客户反馈一个问题:"XXX 报错了,帮忙看看什么问题?"。用户看不懂我们的错误提示吗?
  在笔者的经验中,我们在使用 code - message 机制的时候,特别是业务初期,难以避免的是前后端的设计文案没能完整地覆盖所有的错误用例,或者是错误极其罕见。因此当出现错误时,提示暧昧不清(甚至是直接提示错误信息),导致用户从错误信息中找到解决方案
  在这种情况下,尽量覆盖所有错误路径肯定是最完美的方法。不过在做到这一点之前,码农们往往有下面的解决方案: 遇到未定义错误时,后端在 code 中返回一个统一的错误码,并且将详细的错误信息记录在 message 中。不过这个模式有下面的问题: 客户端提示此类信息时,如果将 message 信息直接展示,可能会展示很多让用户看不懂(也没必要看懂)的文字,而且文字可能会很长(万一是一个 panic 信息),这对用户来说非常不友好 如果开发者不注意,message 信息可能会暴露程序细节,比如连接 DB 失败的信息里可能会涉及数据库的用户名、IP。敏感信息一旦暴露,轻则安全教育,重则高压线伺候 还是类似上面的方法,返回统一的错误码,message 则直接用一个通用的 "unknown error" 或 "未知错误,请联系 XXX" 之类的提示信息。但是这个时候,我们要怎么查错呢? 如果主调方是另一个模块的话还好,用户肯定是个程序员,这个时候只要对对方提供 requestID / trackID 过来就行了。 如果对方是个普通用户,难道让用户 F12 看控制台吗?(别笑,我们还真让用户这么干过……)如果是移动端,那可一点看的机会都没;如果将 traceID 暴露给用户,那么长的 ID,谁记得住啊。
  既要隐藏信息,又要暴露信息,我可以摔盘子吗…… 解决方案
  这里,笔者从日益普及的短信验证码有了个灵感——人的短期记忆对 4 个字符还是比较强的,因此我们可以考虑把错误代码缩短到 4 个字符——不区分大小写,因为如果人在记忆时还要记录大小写的话,难度会增加不少。
  怎么用 4 个字符表示尽量多的数据呢?数字+字母总共有 36 个字符,理论上使用 4 位 36 进制可以表示 36x36x36x36 = 1679616 个值。因此我们只要找到一个针对错误信息字符串的哈希算法,把输出值限制在 1679616 范围内就行了。
  这里我采用的是 MD5 作为例子。MD5 的输出是 128 位,理论上我可以取 MD5 的输出,模 1679616 就可以得到一个简易的结果。实际上为了减少除法运算,我采用的是取高 20 位(0xFFFFF)的简易方式(20 位二进制的最大值为 1048575),然后将这个数字转成 36 进制的字符串输出。
  当出现异常错误时,我们可以将 message 的提示信息如下展示:"未知错误,错误代码 30EV,如需协助,请联系 XXX"。顺带一提,30EV 是 "Access denied for user "db_user"@"127.0.0.1"" 的计算结果,这样一来,我就对调用方隐藏了敏感信息。
  至于后台侧,还是需要实实在在地将这个哈希值和具体的错误信息记录在日志或者其他支持搜索的渠道里。当用户提供该代码时,可以快速定位。
  这种方案的优点很明显: 能够提供足够的信息,用户可以记住代码,从而反馈给开发侧进行 debug。 对于同一个错误,由于哈希的特点,计算结果是相同的。即便出现了碰撞,那么只要输入的数据不至于太多,还是能够快速区分的。 由于不论多长的错误信息,反馈到前端都只有四个字符,因此后端在记录错误信息的时候,可以放心地基于 Go 1.13 的 error wraping 机制进行嵌套,从而记录足够的错误信息
  简易的错误码生成代码如下: import (     // ...     "github.com/martinlindhe/base36" )  var (     replacer = strings.NewReplacer(         " ", "0",         "O", "0",         "I", "1",     ) )  // ...  func Err2Hashcode(err error) (uint64, string) {     u64 := hash(err.Error())     codeStr := encode(u64)     u64, _ = decode(codeStr)     return u64, codeStr }  func encode(code uint64) string {     s := fmt.Sprintf("%4s", base36.Encode(code))     return replace.Replace(s) }  func decode(s string) (uint64, bool) {     if len(s) != 4 {         return 0, false     }     s = strings.Replace(s, "l", "1", -1)     s = strings.ToUpper(s)     s = replace.Replace(s)     code := base36.Decode(s)     return code, code > 0 }  // hash 函数可以自定义 func hash(s string) uint64 {     h := md5.Sum([]byte(s))     u := binary.BigEndian.Uint32(h[0:16])     return uint64(u & 0xFFFFF) }
  当然这种方案也有局限性,笔者能想到的是需要注意以下两点: 生成 error 时要避免记录随机数据、不可重放数据、千人千面的数据,比如说时间、账户号、流水 ID 等等信息,尽可能使用户进行统一操作时,能够生成相同的错误码。 由于数字 1 和字母 I、数字 0 和字母 O 很类似,因此需要进行统一转换,避免出现歧义。这就是为什么在 Err2Hashcode 中,对 hash 结果 encode 之后要重新 decode 一次再返回的原因。
  此外,笔者需要再强调的是:在开发中,针对各种不同的、正式的错误用例依然需要完整覆盖,尽可能通过已有的 code - message 机制将足够清晰的信息告知主调方。这种 hashcode 的错误代码生成方法,仅适用于错误用例遗漏、或者是快速迭代过程中,用于发现和调试遗漏的错误用例的临时方案。

大清仓的骁龙865手机,2K屏IP68防水双OIS防抖,已跳水2100元赶在苹果iPhone13系列发布之前,很多国产厂商的旗舰机都会在本月发布,前几天小米和荣耀也是发布了下半年的旗舰机了,包括小米MIX4,荣耀Magic3和荣耀Magic3Pro,而手机充电一夜不拔,对电池到底有没有坏处呢?直接上结论没有坏处。手机充电一夜不拔对手机电池没什么影响,前提是使用的是安全通过认证的充电器或原装充电器。现在市面上的手机以及充电器都有防过充的智能保护措施,不会出现反复充电的情况为什么近几年勒索软件攻击激增?如何防范它目前我们正在与两种流行病毒作斗争冠状病毒和勒索软件攻击。两者都会影响到部分经济,前者肉眼可见,后者则普通人不可遇见。然而,就网络安全而言,现在我们的安全防范意识,让黑客们有了一个轻软银集团媒体对孙正义发言有误解,软银会继续投资中国市场IT之家8月16日消息据证券时报报道,软银集团在回复证券时报记者采访时表示,媒体对公司董事长孙正义在业绩交流会上的发言有误解,软银集团对中国的承诺没有变化,并会继续投资中国市场,以这才叫安全!静脉图像对应才能解锁,OPPO新专利或在这些设备应用为了提升用户的产品使用体验,各大科技公司可谓使劲浑身解数。正如当下备受关注的隐私安全问题,就有OPPO从解锁出发,安排了全新的静脉解锁专利。从天眼查App显示的内容中,OPPO这一支付宝突然崩了?!官方紧急回应8月14日,支付宝崩了迅速登上热搜。13时14分左右,由于大量用户涌入支付宝参与抢七夕红包活动,页面出现了短暂的不稳定,红包被抢完后就恢复了。其他支付宝服务一切正常。支付宝随后在微共享衣橱平台衣二三停运,共享经济的灯又灭了一盏7月15日,共享衣橱平台衣二三APP主页上写着感谢再见,后会有期八个大字。这周日,创立于2015年的衣二三将正式关闭服务器,并停止运营,这是不是就意味着共享衣橱这个商业模式将黯然离前7月新能源汽车销量超2020年全年水平商务部消费促进司负责人近日表示,1至7月,我国汽车消费总体保持回稳向好态势,其中,新能源汽车销量创历史新高,二手车交易量报废机动车回收量大幅增长。数据显示,1至7月,生产企业新能源65英寸OLED其他家9999元,小米6999元,市场格局要变?今天,小米电视6OLED正式登场,55英寸售价4999元,65英寸售价6999元,正式开启OLED普及潮。实际上,在此前,6999元只能买到55英寸OLED,65英寸价格在9999广西家政智联网全国范围性消费增值平台广西家政智联网是由锐动数据有限公司合资打造。锐动数据全国消费增值电商平台,致力于成为最具世界影响力的电商企业。广西家政智联网融合电商社交电商网红直播和直播带货四大板块,开创全新的电顶级配置,王者存在,这三款顶级旗舰,价格是硬伤当今时代,科技的迅速发展使得手机层出不穷,当然这中间也是参差不齐,劣质机掺杂其中,而且手机的性价比也不尽相同。尤其对于国产品牌来说,很喜欢在手机的配置上进行阉割,来实现自己所谓的性
致敬要做linux运维工程师的朋友,必须要掌握以下几个工具才行本人是linux资深运维工程师,对这方面有点心得,运维必须要掌握的工具。linux系统如果是学习选centos,特别是centos在企业中用得最多,当然还会有其它版本的,但学习者还推荐运行快续航强拍照好的5000元左右的5G手机?感谢邀请推荐运行快续航强拍照好的5000元左右的5G手机?手机运行速度快,这个实际和性能有很大的关系,安卓手机端基本上系统都是相差不多的,你想要运行速度快,直接看性能和散热就可以了百元机性能怪兽白菜价续航够强性价比神机推荐第六弹几天不见,我又来给大家推荐性价比神机啦,继续上期的话题,也就是百元机。百元机的地位也是十分重要的,用来当做备用机是非常香的一个选择,但是又面临性能不够,性价比不高的问题。那么看见这马斯克第一批4680电池汽车将于本季度交付今年将实现全自动驾驶马斯克第一批4680电池汽车将于本季度交付今年将实现全自动驾驶财联社1月27日电,特斯拉CEO埃隆马斯克表示,预计2022年交付量将轻松增长至50以上,仅凭弗里蒙特和上海工厂即可实07年推出的飞信,10年推出的米聊为何都败给了11年才推出的微信?飞信可能很多人对其没有什么印象了,我第一次使用也是刚上大学时。大学报到后,辅导员便让大伙都下载一个飞信app,通知事宜都在飞信群发方便大家联系。要知道当时短信一毛钱一条,当时第一次迎接超算时代加码构建元宇宙,Meta公布下一代新款AI超级计算机1月26日,资本邦了解到,Meta在官网宣布,其研究团队建造了一款新的人工智能超级计算机AIResearchSuperCluster(RSC)。Meta表示,如无意外,一旦RSC在研究速递美智库为全球AI竞赛把脉1hr美中领跑全球AI竞赛原题各国如何利用计算能力实现其人工智能(AI)国家战略来源美国布鲁金斯学会2022年1月作者澳大利亚昆士兰理工大学学者萨马尔法蒂玛等摘要AI的发展在很大程出门游玩,带入门单反,还是拍照手机?出门游玩,又不是摄影采风,带啥单反,拍照手机就够了。何况,入门单反,和拍照手机比,很多地方差得远呢。手机拍摄于济南英雄山如果想出门游玩轻松快乐,又笨重性能又差的入门单反就不要带了,新年家电换新计划,日立冰箱洗衣机气波炉第一步,先关注我大家好,我是胡侃侃。前言要过年了,大家开始囤菜没有。现在各地都有零星的疫情发生,凡事就怕万一,有些粮米油盐都还是要囤一些,特别是新鲜蔬菜和肉制品。这里我想围绕着年货新能源行业发展前景广阔光伏产业成长潜力和空间巨大众所周知,当前我国的电力供应主要依靠传统火电。在碳达峰碳中和战略目标背景下,我国能源消费结构正在向绿色低碳转型中,未来光伏风电水电等可再生清洁能源占我国一次能源总量比重将会不断提升B轮融资落地长安新能源将于2025年前后完成公开上市北京商报讯(记者刘晓梦)1月24日,重庆长安新能源汽车科技有限公司(以下简称长安新能源)B轮容易正式完成。此次B轮融资,包括长安汽车在内的10家企业投资,共募集资金约49。773亿