如何愉快的自动生成错误码本文利用go ast来自动定义错误码,减少重复编码。如果你厌倦了手动定义错误码,希望这篇文章可以
引言
错误码是一组遵循某种规范的数字,用于快速定位异常类型和异常代码的位置。对于排查业务问题有很大的帮助。但是每次标识一个异常位置都要我们手动的定义一个错误码,这项工作着实是有些乏味,而且还会打断我们当前的操作,在一个公共文件里写下自己的错误码,然后回到业务中自己引用。
最终错误码文件越来越大,而我们自己定义的错误码除了自己引用一次之外,其他时间更是无人问津。当我们在日志中拿到一个数字错误码后,还需要去公共文件中找到数字错误码对应的定义,看看具体是哪一行代码引用。
一种常用的错误码定义:
type ErrCode int //错误码
// 定义errorCode
const (
// ServerError 1开头为服务级错误码
ServerError ErrCode = 10001
ParamBindError ErrCode = 10002
// IllegalDatasetName 2开头为业务级错误码
// 其中数据集管理为201开头
IllegalDatasetName ErrCode = 20101 // 无效的数据集名称
ParamNameError ErrCode = 20102 // 参数name错误
)
//错误提示含义
var errorMsg = map[ErrCode]string{
ServerError: "服务内部错误",
ParamBindError: "参数信息有误",
IllegalDatasetName: "无效的数据集名称",
ParamNameError: "参数name错误",
}
// Text 根据错误码获取错误信息
func Text(code ErrCode) string {
return errorMsg[code]
}
func NewBaseError(code ErrCode) *self_error.BaseErr {
return &self_error.BaseErr{
Code: int(code),
Msg: Text(code),
}
}
里面的错误码和错误提示含义基本都只在某一个代码处引用,也只有在新增错误引用的时候才会在这里新增错误码和错误提示。
效果
我们要实现的效果是随用随写,自动生成符合我们规则的错误码;无需人工定义和维护错误码,无需中断我们写代码的流程,只需要在需要用的错误码的位置使用TodoBaseErr 声明我们想要返回的错误信息即可,错误码自动补齐生成
package main
import "github.com/heucoder/billow_err/billow_err"
//go:generate go get github.com/heucoder/billow_err/gen
//go:generate go run github.com/heucoder/billow_err/gen -path ./
func main() {
billow_err.TodoBaseErr("test1")
billow_err.TodoBaseErr("test1")
billow_err.TodoBaseErr("test2")
}
在命令行执行go generate 后,生成如下代码,:
package main
import "github.com/heucoder/billow_err/billow_err"
//go:generate go get github.com/heucoder/billow_err/gen
//go:generate go run github.com/heucoder/billow_err/gen -path ./
func main() {
billow_err.NewBaseError(1, "test1")
billow_err.NewBaseError(2, "test1")
billow_err.NewBaseError(3, "test2")
}
元编程
如果能够将代码看做数据,那么代码就可以像数据一样在运行时被修改、更新和替换,这就是元编程的理念:使用代码来操作代码;元编程赋予了编程语言更加强大的表达能力,能够让我们将一些计算过程从运行时挪到编译时、通过编译期间的展开生成代码或者允许程序在运行时改变自身的行为。
现代的编程语言大都会为我们提供不同的元编程能力,从总体来看,根据生成代码的时机不同,我们将元编程能力分为两种类型,其中一种是编译期间的元编程,例如:宏和模板;另一种是运行期间的元编程,也就是运行时,它赋予了编程语言在运行期间修改行为的能力,当然也有一些特性既可以在编译期实现,也可以在运行期间实现。
Go 语言作为编译型的编程语言,它提供了比较有限的运行时元编程能力,例如:反射特性,然而由于性能的问题,反射在很多场景下都不被推荐使用。当然除了反射之外,Go 语言还提供了另一种编译期间的代码生成机制 — go generate,它可以在代码编译之前根据源代码生成代码。
在错误码定义中,我们使用代码来自动生成错误码。
AST
ast将源代码解析成结构化的数据,使我们可以用代码来操作源代码。
一个go代码文件的返回的*ast.File结构体如下:
-
Name:表示文件对应包的名字
-
Impoorts:表示文件导入的第三方包信息
-
Decls:是结构体中最重要的成员,里面包含了Import信息、const定义、type定义、函数定义
利用AST我们可以将源代码解析成树状结构。文件名、引用的包、声明的变量、调用的函数等等都是一个节点。
func main() {
expr, _ := parser.ParseExpr(`1+2*3`)
ast.Print(nil, expr)
}
一个二元表达式经过ast解析之后结构如下,具体类型含义在这里不做详细讲解。借由此例我们可以看到变量、表达式、函数经过ast解析后的结果,可以定义ast的结构反向生成变量、表达式、函数等。
0 *ast.BinaryExpr {
1 . X: *ast.BasicLit {
2 . . ValuePos: 1
3 . . Kind: INT
4 . . Value: "1"
5 . }
6 . OpPos: 2
7 . Op: +
8 . Y: *ast.BinaryExpr {
9 . . X: *ast.BasicLit {
10 . . . ValuePos: 3
11 . . . Kind: INT
12 . . . Value: "2"
13 . . }
14 . . OpPos: 4
15 . . Op: *
16 . . Y: *ast.BasicLit {
17 . . . ValuePos: 5
18 . . . Kind: INT
19 . . . Value: "3"
20 . . }
21 . }
22 }
代码实现
我们要做的就是将TodoBaseErr这个node替换成NewBaseError这个node。所以要有两个核心问题:
- 识别并替换原来的node:
ast.Inspect(file, func(node ast.Node) bool {
if exprStmt, ok := node.(*ast.ExprStmt); ok {
if callExpr, ok := (exprStmt.X).(*ast.CallExpr); ok {
if selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok && selectorExpr.Sel.Name == "TodoBaseErr" {
if errorMsg, ok := callExpr.Args[0].(*ast.BasicLit); ok {
exprStmt.X = CreateNewBaseErrorExpr(errorMsg.Value) //用新的node替换旧的node
}
}
}
}
return true
})
为什么要这么识别呢,就是把这段代码用ast解析并打印出来,找到TodoBaseErr对应的node,按照值进行匹配:
package main
import "code.byted.org/ecom/billow_err/billow_err"
//go:generate go get code.byted.org/ecom/billow_err/gen
//go:generate go run code.byted.org/ecom/billow_err/gen -path ./
func main() {
billow_err.TodoBaseErr("test1")
billow_err.TodoBaseErr("test1")
billow_err.TodoBaseErr("test2")
}
- 生成新的node,生成方法也和上述一样,将代码用ast解析并打印,就知道应该生成一个什么类型的node了:
func CreateNewBaseErrorExpr(msg string) *ast.CallExpr {
printFunc := &ast.SelectorExpr{
X: ast.NewIdent("billow_err"),
Sel: ast.NewIdent("NewBaseError"),
}
errorN += 1
codeLit := &ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprintf("%v", errorN),
}
msgLit := &ast.BasicLit{
Kind: token.STRING,
Value: msg,
}
callExpr := &ast.CallExpr{
Fun: printFunc,
Args: []ast.Expr{codeLit, msgLit},
}
return callExpr
}
- 将ast解析出的结果输出
func saveASTToFile(fileName string, fset *token.FileSet, node *ast.File) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
var buf bytes.Buffer
err = format.Node(&buf, fset, node)
if err != nil {
return err
}
_, err = file.WriteString(buf.String())
return err
}
转载自:https://juejin.cn/post/7410216555236114473