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错误

go 复制代码
package bizerror

import (
	"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 = channelCode
		e.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.Infof
	case WarnLevel:
		return log.Warningf
	case 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,去掉左侧的.后为CallerTest
	funcName = 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()
}

单元测试

go 复制代码
package bizerror

import (
	"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")
	})
}
相关推荐
&岁月不待人&4 分钟前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove8 分钟前
G1垃圾回收器日志详解
java·开发语言
无尽的大道15 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒19 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
binishuaio28 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE30 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻34 分钟前
WPF中的依赖属性
开发语言·wpf
洋24043 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙44 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式