95.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/722545.shtml

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

相关文章

vue3基础教程(2)——创建vue3+vite项目

博主个人微信小程序已经上线&#xff1a;【中二少年工具箱】。欢迎搜索试用 正文开始 专栏简介1. 前言2.node版本检测3.创建vue项目 专栏简介 本系列文章由浅入深&#xff0c;从基础知识到实战开发&#xff0c;非常适合入门同学。 零基础读者也能成功由本系列文章入门&#x…

javascript数组排序的方法

目录 基本用法 按照数字大小排序 按照降序排序 按照字符串长度排序 按照对象属性排序 在JavaScript中&#xff0c;数组排序通常使用Array.prototype.sort()方法。这个方法会按照指定的顺序对数组的元素进行排序&#xff0c;并返回排序后的数组。如果未指定比较函数&#x…

Springboot配置MySQL数据库

Springboot配置MySQL数据库 一、创建springboot项目&#xff0c;并添加如下依赖 <dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope> </dependency>二、在applica…

基于springboot+vue的酒店管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

实现session共享的方法总结完整版

文章目录 实现session共享的方法总结完整版1、使用共享数据库&#xff1a;2、使用粘性会话&#xff08;Sticky Session&#xff09;&#xff1a;3、使用缓存系统&#xff1a;4、使用分布式文件系统&#xff1a;5、使用中央认证服务&#xff1a;6、使用会话复制&#xff1a;7、使…

MongoDB聚合运算符:$dateFromParts

文章目录 语法使用值域值大于范围值小于范围时区 举例 语法 {$dateFromParts : {year: <year>, month: <month>, day: <day>,hour: <hour>, minute: <minute>, second: <second>,millisecond: <ms>, timezone: <tzExpression>…

100%开源大模型OLMo:代码/权重/数据集/训练全过程公开,重定义AI共享

前言 近日&#xff0c;艾伦人工智能研究所联合多个顶尖学术机构发布了史上首个100%开源的大模型“OLMo”&#xff0c;这一举措被认为是AI开源社区的一大里程碑。OLMo不仅公开了模型权重&#xff0c;还包括了完整的训练代码、数据集和训练过程&#xff0c;为后续的开源工作设立…

三星成功研发出业界首款12层堆叠HBM3E

三星电子有限公司成功研发出业界首款12层堆叠HBM3E DRAM——HBM3E 12H&#xff0c;这是迄今为止容量最大的HBM产品。这款新型HBM3E 12H内存模块提供了高达1,280GB/s的史上最高带宽&#xff0c;并拥有36GB的存储容量&#xff0c;相较于之前的8层堆叠HBM3 8H&#xff0c;在带宽和…

运维随录实战(1)

docker安装mongo 1,新建数据卷目录 mkdir -p /my/own/datadir 2,拉取镜像 docker pull mongo:latest 3,运行 docker run -d -p 27017:27017 -v /my/own/datadir:/data/db --name mongodb -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=123456 mongo …

ECMAScript6

课程链接 目录 相关介绍什么是ECMA什么是ECMAScript为什么学习ES6 letconst变量解构赋值模板字符串对象简化写法箭头函数函数参数的默认值rest参数扩展运算符Symbol迭代器生成器函数与调用Promise介绍与基本用法Promise封装读取文件Promise.prototype...then方法Promise.catch…

CCDP.01.寄主机SSH连接虚拟机的QA

V0.0 初始版本-2024.2.29 检查VM的网卡配置 如上图&#xff0c;如果enp0s3网卡没有出现形如10.0.0.??/24的ip配置&#xff0c;说明该网卡配置存在错误&#xff0c;或者没有“使能”该网卡。在RockyLinux8.X中可检查“ifcfg-enp0s&#xff1f;” vi /etc/sysconfig/network-…

智能系统引领的未来时代

智能系统引领的未来时代 随着人工智能技术的不断发展和普及&#xff0c;智能系统已经成为引领未来时代的关键力量。智能系统能够模拟人类的智能行为&#xff0c;实现自主学习、自主决策&#xff0c;并在各个领域展现出强大的应用潜力。在未来时代&#xff0c;智能系统将在各个…

【Python 图像处理 PIL 系列 13.1 -- 从列表中读取图像数据并生成图像】

文章目录 从列表中读取图像数据并生成图像代码示例 从列表中读取图像数据并生成图像 在Python中&#xff0c;可以使用Pillow库来创建和保存图像。这里我们将展示如何从列表list_num中读取RGB颜色数据&#xff0c;并根据row_num和column_num生成RGB格式的图像。 安装Pillow库&…

【JS】WebSocket实现简易聊天室

【JS】WebSocket实现简易聊天室 聊天室思路示例 聊天室思路 聊天室思路 1、连接服务器先建立连接&#xff0c;默认生成匿名用户(admin01) 2、客户端发送消息&#xff0c;其它客户端用户都会同步接收消息(服务端接受消息广播所有连接用户) 3、客户端修改昵称&#xff0c;其它客…

数据结构之七大排序

&#x1d649;&#x1d65e;&#x1d658;&#x1d65a;!!&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦ &#x1f44f;&#x1f3fb;‧✧̣̥̇:Solitary_walk ⸝⋆ ━━━┓ - 个性标签 - &#xff1a;来于“云”的“羽球人”。…

FreeRTOS操作系统学习——FreeRTOS工程创建

FreeROTS工程创建 详细步骤 如无特殊情况&#xff0c;大部人都要配置为外部高速时钟 另外&#xff0c;本实验使用了FreeRTOS&#xff0c;FreeRTOS的时基使用的是Systick&#xff0c;而 STM32CubeMX中默认的HAL库时基也是Systick&#xff0c;为了避免可能的冲突&#xff0c;最…

渗透测试工具 nmap 详解

官网&#xff1a;Nmap: the Network Mapper - Free Security Scanner -p&#xff1c;端口范围&#xff1e;&#xff1a;仅扫描指定的端口 用于扫描指定端口是否开放&#xff0c;在 -p 后输入指定的端口&#xff0c;以英文","进行拼接多个指定端口。 nmap -p 80&…

2024年3月产品认证基础考试简答题及答案

产品认证基础 46.产品认证的工厂检查有哪几种路线&#xff1f;各有什么优缺点&#xff1f; 答案&#xff1a;两种常用的检查路线&#xff1a; 1.按照要素或过程检查 按照认证规则规定的工厂应满足的要素要求&#xff08;包括质量保证能力要求&#xff09;&#xff0c;结合部…

TestNG @Test注释属性- threadPoolSize属性

本文将讨论TestNG中Test annotation的threadPoolSize属性。那么&#xff0c;我们开始吧。 那么&#xff0c;threadPoolSize属性有什么用处呢&#xff1f;答案是&#xff0c;无论何时您想要多次并行地运行一个测试方法&#xff0c;您都需要这个属性。该方法将从invocationCount…

java014 - Java继承

1、继承 1.1 继承概述 继承是面向对象的三大特征之一&#xff0c;可以使得子类具有父类的属性和方法&#xff0c;还可以在子类中重新定义&#xff0c;追加属性和方法。 1.2 继承格式 public class 子类类名 extends 父类类名 {} 范例&#xff1a;public class son extends …