96.Go设计优雅的错误处理(带堆栈信息)

在之前的两篇文章中,我们已经介绍过错误的一些优雅处理
75.错误码设计、实现统一异常处理和封装统一返回结果
88.Go设计优雅的错误处理

本文想继续写一篇,可以作为工具包直接使用。也是记录一种新的思路和编码技巧,同时创建错误的时候会自动打印日志,还能提供堆栈信息。

目标

  1. 避免所有错误前都需要手动打印日志,最好自动打印规范化的日志;
  2. 完整的上下文信息,便于排查定位;
  3. 方便response封装,返回标准三元组;
  4. 高扩展性;

代码如下

代码地址:https://gitee.com/lymgoforIT/golang-trick/blob/master/42-bizerror/bizerror/bizerror.go

注:"google.golang.org/appengine/log",需要在谷歌的云计算平台才能使用,所以下面的代码需要根据实际情况替换日志包。否则会报 not an App Engine context错误

package bizerrorimport ("bytes""context""fmt""google.golang.org/appengine/log""path/filepath""runtime""strconv""strings"
)// BizError 自定义Error类型(实现了go内嵌error接口)
// 特性:
//  1. 包含服务返回三元组(Code + Msg + status), 便于封装response
//  2. 自动日志打印(NewBizError时打印)
//  3. 根据可选参数可控制是否打印堆栈信息
//  4. 其他option拓展见使用说明
type BizError struct {code        string                           // 错误码msg         string                           // 错误信息status      string                           // 状态level       BizErrLevel                      // 日志级别,默认是Error级别detail      string                           // 需要打印的补充信息fnName      string                           // 函数名storeStack  bool                             // 是否打印堆栈信息stack       []byte                           // 堆栈信息stackRows   int                              // 堆栈信息最大打印层次depth       int                              // 函数调用深度channelCode string                           // 下游错误码channelMsg  string                           // 下游错误信息asyncFn     func(context.Context, *BizError) // 异步执行函数
}// BizErrLevel 错误等级, 会影响日志打印时的level
type BizErrLevel int8// BizErrOption BizError属性设置函数
type BizErrOption func(*BizError)const (// InfoLevel Info级别, 使用logs.CtxInfo打印日志InfoLevel BizErrLevel = iota// WarnLevel Warn级别, 使用logs.CtxWarn打印日志WarnLevel// ErrorLevel Error级别, 使用logs.CtxError打印日志ErrorLevel
)func (e BizError) Error() string {errInfo := fmt.Sprintf("[%s] code=%s, msg=%s, channelCode=%s, channelMsg=%s, detail=%s",e.fnName, e.code, e.msg, e.channelCode, e.channelMsg, e.detail)if e.storeStack {errInfo = errInfo + "\n" + string(e.stack)}return errInfo
}func (e BizError) GetCode() string {return e.code
}func (e BizError) GetStatus() string {return e.status
}func (e BizError) GetMsg() string {return e.msg
}
func (e BizError) GetDetail() string {return e.detail
}func (e BizError) GetChannelCode() string {return e.channelCode
}func (e BizError) GetChannelMsg() string {return e.channelMsg
}func NewBizError(ctx context.Context, code, status, msg string, opts ...BizErrOption) *BizError {bizErr := &BizError{code:       code,msg:        msg,status:     status,level:      ErrorLevel,storeStack: true,depth:      2, // 为0时是getCurrentFunc,为1时是NewBizError,为2时则是调用NewBizError的函数stackRows:  10,}for _, opt := range opts {opt(bizErr)}if len(bizErr.fnName) == 0 {bizErr.fnName = getCurrentFunc(bizErr.depth)}if bizErr.storeStack {bizErr.stack = getStack(bizErr.depth, bizErr.stackRows)}bizErr.ctxLog(ctx)if bizErr.asyncFn != nil {go safeGo(ctx, func() {bizErr.asyncFn(ctx, bizErr)})}return bizErr
}func safeGo(ctx context.Context, f func()) {defer func() {if err := recover(); err != nil {}}()f()
}// WithLogLevelOption 设置日志打印等级, 不设置时默认为ErrorLevel
func WithLogLevelOption(level BizErrLevel) BizErrOption {return func(e *BizError) {e.level = level}
}// WithDetailOption 设置报错详细信息, 如单号/Uid等参数
func WithDetailOption(format string, v ...interface{}) BizErrOption {return func(e *BizError) {e.detail = fmt.Sprintf(format, v...)}
}// WithFuncNameOption 设置打印日志时的报错函数名, 不设置时默认打印调用NewBizError的函数名
func WithFuncNameOption(funcName string) BizErrOption {return func(e *BizError) {e.fnName = funcName}
}// WithStackOption 设置是否保存函数栈信息, 不设置时默认保存
func WithStackOption(storeStack bool) BizErrOption {return func(e *BizError) {e.storeStack = storeStack}
}// WithSkipDepthOption 设置跳过的函数栈深度, 当你封装NewBizError时应该设置
func WithSkipDepthOption(skipDepth int) BizErrOption {return func(e *BizError) {e.depth += skipDepth}
}// WithChannelRespOption 设置下游返回的错误码/消息, 当异常是下游导致的可以设置
func WithChannelRespOption(channelCode, channelMsg string) BizErrOption {return func(e *BizError) {e.channelCode = channelCodee.channelMsg = channelMsg}
}// WithAsyncExecutor 产生错误后异步执行器, 如进行上报metrics打点
func WithAsyncExecutor(fn func(context.Context, *BizError)) BizErrOption {return func(e *BizError) {e.asyncFn = fn}
}// WithStackRows 函数堆栈保存的行数, 默认保存10行
func WithStackRows(stackRows int) BizErrOption {return func(e *BizError) {if stackRows > 0 {e.stackRows = stackRows}}
}// logFunc 定义日志打印函数,根据getLogFunc返回的实际指定的日志等级决定使用哪个函数
type logFunc func(ctx context.Context, format string, v ...interface{})// ctxLog 实际打印日志
func (e BizError) ctxLog(ctx context.Context) {e.getLogFunc()(ctx, "%s", e.Error())
}// getLogFunc 根据日志等级获取日志打印函数,默认为Error级别
func (e BizError) getLogFunc() logFunc {switch e.level {case InfoLevel:return log.Infofcase WarnLevel:return log.Warningfcase ErrorLevel:return log.Errorf}return log.Errorf
}// getCurrentFunc 返回文件路径,函数所在行数以及函数名
func getCurrentFunc(skip int) string {pc, file, line, ok := runtime.Caller(skip)if !ok {return "??:0:??()"}funcName := runtime.FuncForPC(pc).Name()// 如 函数为/XXX/util.CallerTest,则扩展名为.CallerTest,去掉左侧的.后为CallerTestfuncName = strings.TrimLeft(filepath.Ext(funcName), ".") + "()"return filepath.Base(file) + ":" + strconv.Itoa(line) + ":" + funcName
}// getStack 返回一个格式良好的堆栈帧,跳过跳过帧
func getStack(skip, rows int) []byte {buf := new(bytes.Buffer) // 返回数据// 在循环时,打开文件并读取它们,使用变量记录当前加载的文件for i := skip; i-skip < rows; i++ { // 跳过最里层的skip帧pc, file, line, ok := runtime.Caller(i)if !ok {break}// 拼接当前所在栈的信息,并换回,继续去循环下一栈信息,直到堆栈信息都打完或者达到rows层fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)}return buf.Bytes()
}

单元测试

package bizerrorimport ("context""github.com/smartystreets/goconvey/convey""google.golang.org/appengine/log""testing""time"
)func TestBizError(t *testing.T) {ctx := context.Background()convey.Convey("NewBizError-无额外选项", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg")convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")})convey.Convey("NewBizError-增加详情", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithDetailOption("query failed with order id: %d", 123))convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")convey.So(bizErr.GetDetail(), convey.ShouldEqual, "query failed with order id: 123")})convey.Convey("NewBizError-设置日志打印级别", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithLogLevelOption(InfoLevel))convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")bizErr2 := NewBizError(ctx, "code", "status", "msg", WithLogLevelOption(WarnLevel))convey.So(bizErr2, convey.ShouldNotBeNil)})convey.Convey("NewBizError-设置函数名称", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithFuncNameOption("TestBizError"))convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")})convey.Convey("NewBizError-设置不存储堆栈信息", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithStackOption(false))convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")})convey.Convey("NewBizError-设置堆栈行数", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithStackRows(2))convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")})convey.Convey("NewBizError-设置忽略的函数栈深度", t, func() {newCodeErr := func() *BizError {return NewBizError(ctx, "code01", "status01", "msg01", WithSkipDepthOption(1))}bizErr := newCodeErr()convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code01")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status01")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg01")})convey.Convey("NewBizError-设置下游错误码/信息", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithChannelRespOption("channelCode", "channelMsg"))convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")convey.So(bizErr.GetChannelCode(), convey.ShouldEqual, "channelCode")convey.So(bizErr.GetChannelMsg(), convey.ShouldEqual, "channelMsg")})convey.Convey("NewBizError-设置异步执行器", t, func() {bizErr := NewBizError(ctx, "code", "status", "msg", WithAsyncExecutor(func(ctx context.Context, bizError *BizError) {log.Infof(ctx, "AsyncExecutor executed: bizError=%s", bizError.Error())}))time.Sleep(1 * time.Second)convey.So(bizErr, convey.ShouldNotBeNil)convey.So(bizErr.GetCode(), convey.ShouldEqual, "code")convey.So(bizErr.GetStatus(), convey.ShouldEqual, "status")convey.So(bizErr.GetMsg(), convey.ShouldEqual, "msg")})
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/723687.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Dubbo的集群容错方案

Dubbo提供了多种集群容错方案来保证分布式环境下的高可用性。这些容错方案可以在服务提供者不可用时&#xff0c;根据不同的业务需求和场景&#xff0c;选择不同的策略来处理。以下是Dubbo支持的一些主要集群容错方案&#xff1a; 1. Failover Cluster&#xff08;失败自动切换…

Ubuntu 20.04 ROS1 与 ROS2 通讯

激光雷达和3d视觉传感器驱动很多都是基于ros1开发的&#xff0c;由于自己项目在ros2环境开发&#xff0c;需要获取从驱动出来的点云数据流&#xff0c;所以尝试订阅ros1出来的点云topic话题&#xff0c;固需要ros1与ros2之间建立通讯连接。 项目环境&#xff1a; Legion-Y7000…

Android Intent 传递数据过大问题

/** * 通过Intent传递或者返回的数据是存放在一个叫做Binder transaction buffer的缓存区&#xff0c;这个缓冲区的大小为1Mb(Android 28 Platform)&#xff0c;当缓冲区不够用时就会抛出异常 * 如果有多个数据传递同时进行&#xf…

【Linux】linux | 验证端口是否开放 | 基于linux工具

一、场景 1、多台服务器&#xff0c;做负载均衡 2、应用服务访问基础服务&#xff0c;端口不通&#xff1b;比如&#xff1a;redis 3、验证是防火墙问题还是redis配置问题 二、操作 1、下载工具nmap yum -y install nmap 2、验证命令 nmap -p 80,6380,8848,3306 192.168.1.…

精通Docker Compose: 在docker-compose.yml中配置硬件资源访问

精通Docker Compose: 在docker-compose.yml中配置硬件资源访问 引言Docker与硬件资源访问简介为何需要在Docker容器中访问硬件资源Docker如何与硬件资源交互 准备工作安装Docker和Docker Compose确保硬件资源可被主机识别小结 在docker-compose.yml中配置硬件资源访问显卡访问配…

HarmonyOS创建项目和应用—设置数据处理位置

项目和应用介绍 关于项目 项目是资源、应用的组织实体。资源包括服务器、数据库、存储&#xff0c;以及您的应用、终端用户的数据等。在您使用部分服务时&#xff0c;您是数据的控制者&#xff0c;数据将按照您设置的数据处理位置来存储在指定区域。 通常&#xff0c;您不需…

java数据结构与算法刷题-----LeetCode230. 二叉搜索树中第K小的元素

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 1. 中序遍历2. 预处理 1. 中序遍历 解题思路:时间复杂度O(Hk)其…

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:特效绘制合并)

用于对背景模糊等特效进行绘制合并。 说明&#xff1a; 从API Version 10开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 该接口为系统接口。 useEffect useEffect(value: boolean) 用于对背景模糊等特效进行绘制合并。 系统能力&#…

华为荣耀终端机试真题

文章目录 一 、字符展开(200分)1.1 题目描述1.2 解题思路1.3 解题代码二、共轭转置处理(100分)2.1 题目描述2.3 源码内容一 、字符展开(200分) 1.1 题目描述 // 64 位输出请用 printf(“%lld”)给定一个字符串,字符串包含数字、大小写字母以及括号(包括大括号、中括号…

分布式数字身份:通往Web3.0世界的个人钥匙

数字化时代&#xff0c;个人身份已不再仅仅局限于传统形式&#xff0c;分布式数字身份&#xff08;Decentralized Identity&#xff0c;简称DID&#xff09;正崭露头角&#xff0c;它允许个人通过数字签名等加密技术&#xff0c;完全掌握和控制自己的身份信息。研究报告显示&am…

HarmonyOS NEXT应用开发——Navigation开发 页面切换场景范例

简介 在应用开发时&#xff0c;我们常常遇到&#xff0c;需要在应用内多页面跳转场景时中使用Navigation导航组件做统一的页面跳转管理&#xff0c;它提供了一系列属性方法来设置页面的标题栏、工具栏以及菜单栏的各种展示样式。除此之外还拥有动态加载&#xff0c;navPathSta…

Linux 桌面版系统问题分析及CDH 6影响分析,CDH做HA模式配置教程

一、问题 由于之前是第一次安装CentOS系统&#xff0c;基本上是按照网上的教程安装&#xff0c;安装了桌面版&#xff0c;配置默认&#xff0c;在安装CDH6.3后&#xff0c;在使用中发现没有预期的那么好用&#xff0c;然后请教专业人士后&#xff0c;得出以下2个原因 1、桌面…

在Linux中使用docker【下】(常见命令下)

在Linux中使用docker【下】&#xff08;常见命令下&#xff09; 一、Docker介绍二、在Linux中使用Docker的意义2.1 轻量级与资源高效2.2 快速部署与版本控制2.3 隔离与安全2.4 简化运维2.5 跨平台兼容2.6 持续集成与持续部署&#xff08;CI/CD&#xff09; 三、Docker的安装3.1…

JVM 启动参数

jvm的启动参数随版本发展可以分为三大类&#xff1a;标准参数、非标准参数和非Stable参数。 1. 标准参数&#xff1a;所有JVM实现都必须支持这些参数&#xff0c;且向下兼容。 如&#xff1a;-jar -cp -verbose 2. 非标准参数&#xff1a;大多数JVM实现都支持这些参数&#…

将圆环区域展开成矩形长条

在机器视觉领域&#xff0c;经常会遇到圆环型的检测目标&#xff0c;比如瓶口&#xff0c;轮胎&#xff0c;橡皮圈等等&#xff0c;比如想检测轮胎上的瑕疵&#xff0c;就可以通过把环形区域展开成矩形形状&#xff0c;然后对胎侧进行瑕疵检测&#xff0c;再比如对圆环扣上的字…

2024大厂Android面试集合,安卓开发面试书籍

前言 早在2017年我们就建了第一个进击BAT的Android开发进阶交流群&#xff0c;两年期间很多群友都分享了自己的Android面试经历。其中就有很多群友已经斩获蚂蚁金服&#xff0c;天猫&#xff0c;高德&#xff0c;盒马等阿里系offer 收集反馈的面经资料比较乱&#xff0c;最近…

2024BAT大厂Java社招最全面试题,成功入职字节跳动

前言 现在Java程序员面试都是因为没有丰富的工作经验和自己过硬的技术&#xff0c;所有都不知道一般互联网应该会问什么技术问题&#xff0c;加上自己可能去面试的时候没有准备的太充分&#xff0c;一面试刚跟面试官扯几个面试题就不知道自己在哪里了&#xff0c;被怼的体无完…

● 198.打家劫舍 ● 213.打家劫舍II ● 337.打家劫舍III

● 198.打家劫舍 动规五部曲。 1、dp[j]含义。前j个房屋偷到的金额之和最大是dp[j]。 2、递推公式。递推公式要得出dp[i]&#xff0c;就是要确定第i个房屋是否打劫&#xff0c;那么也跟之前的背包问题一样&#xff0c;放与不放&#xff0c;对应的是两种结果&#xff0c;我们只需…

旺泓_光感WH3620_数字RGBW-IR色彩传感器

由工采网代理的WH3620是一种基于颜色的光到数字转换器;它集光电二极管、电流放大器、模拟电路和数字信号处理器于一体&#xff1b;提供红、绿、蓝、白和红外光传感&#xff1b;能调节屏幕或灯光白平衡&#xff1b;各通道同时并行输出&#xff0c;因此在白光LED、CWF、TL84、D65…

防患未然,OceanBase巡检工具应用实践——《OceanBase诊断系列》之五

1. OceanBase为什么要做巡检功能 尽管OceanBase拥有很好的MySQL兼容性&#xff0c;但在长期的生产环境中&#xff0c;部署不符合标准规范、硬件支持异常&#xff0c;或配置项错误等问题&#xff0c;这些短期不会出现的问题&#xff0c;仍会对数据库集群构成潜在的巨大风险。为…