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

Golang中使用注释注解打印日志

  背景
  前一段时间线上出现了一个问题:在压测后偶尔会出现一台机器查询数据无结果但是没有返回  err   的情况,导致后续处理都出错。由于当时我们仅在最外层打印了 err   ,没有打印入参和出参,所以导致很难排查问题到底出现在哪一环节。
  经过艰难地排查出问题后,感觉需要在代码里添加打印关键函数的入参和出参数,但这个逻辑都是重复的,也不想将这一逻辑侵入开发流程,所以就想到了代码生成方式的注释注解。即我们提前定义好一个注释注解(例如: // @Log()  ),并且在 Docker 中编译前运行代码生成的逻辑,将所有拥有该注释注解的函数进行修改,在函数体前面添加打印入参和出参的逻辑。这样就不需要让日志打印侵入到业务代码中,并且后续可以很方便替换成其他的打印逻辑(例如根据@Log   内的参数或者返回值等自定义日志级别)。 编写代码
  我们可以使用 AST 的方式去解析、识别并修改代码,  go/ast   已经提供了相应的功能,我们查看我们关心的节点部分及其相关的信息结构,可以使用 goast-viewer  直接查看 AST ,当然也可以本地进行调试。 遍历 .go 文件
  首先需要使用  filepath.Walk   函数遍历指定文件夹下的所有文件,对每一个文件都会执行传入的 walkFn   函数。 walkFn   函数会将 .go   文件解析成 AST ,将其交由注释注解的处理器处理,然后根据是否修改了 AST 决定是否生成新的代码。 // walkFn 函数会对每个 .go 文件处理,并调用注解处理器 func walkFn(path string, info os.FileInfo, err error) error {     // 如果是文件夹,或者不是 .go 文件,则直接返回不处理     if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") {         return nil     }       // 将 .go 文件解析成 AST     fileSet, file, err := parseFile(path)     // 如果注解修改了内容,则需要生成新的代码     if logannotation.Overwrite(path, fileSet, file) {         buf := &bytes.Buffer{}         if err := format.Node(buf, fileSet, file); err != nil {             panic(err)         }           // 如果不需要替换,则生成到另一个文件         if !replace {             lastSlashIndex := strings.LastIndex(path, "/")             genDirPath :=path[:lastSlashIndex] + "/_gen/"             if err := os.Mkdir(genDirPath, 0755); err != nil && os.IsNotExist(err){                 panic(err)             }             path = genDirPath + path[lastSlashIndex+1:]         }           if err := ioutil.WriteFile(path, buf.Bytes(), info.Mode()); err != nil {             panic(err)         }     }       return nil }遍历 AST
  当注释注解处理器拿到 AST 后,就需要使用  astutil.Apply   函数遍历整颗 AST ,并对每个节点进行处理,同时为了方便修改时添加 import   ,我们包一层函数供内部调用,并把一些关键信息打包在一起。 // Overwrite 会对每个 file 处理,运行注册的注解 handler ,并返回其是否被修改 func Overwrite(filepath string, fileSet *token.FileSet, file *ast.File) (modified bool) {     // 初始化处理本次文件所需的信息对象     info := &Info{         Filepath: filepath,         NamedImportAdder: func(name string, path string) bool {             return astutil.AddNamedImport(fileSet, file, name, path)         },     }       // 遍历当前文件 ast 上的所有节点     astutil.Apply(file, nil, func(cursor *astutil.Cursor) bool {         // 处理 log 注解         info.Node = cursor.Node()         nodeModified, err := Handler.Handle(info)         if err != nil {             panic(err)         }           if nodeModified {             modified = nodeModified         }           return true     })       return }识别注释注解
  接下来我们就需要识别注释注解,跳过不相关的节点,示例中不做额外处理,仅当注释为  @Log()   才认为需要处理,可以根据需要添加相应的逻辑。 func (h *handler) Handle(info *Info) (modified bool, err error) {     // log 注解只用于函数     funcDecl, ok := info.Node.(*ast.FuncDecl)     if !ok {         return     }       // 如果没有注释,则直接处理下一个     if funcDecl.Doc == nil {         return     }       // 如果不是可以处理的注解,则直接返回     doc := strings.Trim(funcDecl.Doc.Text(), "	  ")     if doc != "@Log()" {         return     }     ... }获取函数入参和出参
  首先我们需要获取函数的入参和出参,这里我们以出参举例。出参定义在  funcDecl.Type.Results   ,并且可能没有指定名称,所以需要先为以 _0  , _1  , ...   这样的形式为没有名称的变量设置默认名称,然后按照顺序获取所有变量的名称列表。 // SetDefaultNames 给没有名字的 Field 设置默认的名称 // 默认名称格式:_0, _1, ... // true: 表示至少设置了一个名称 // false: 表示未设置过名称 func SetDefaultNames(fields ...*ast.Field) bool {     index := 0     for _, field := range fields {         if field.Names == nil {             field.Names = NewIdents(fmt.Sprintf("_%v", index))             index++         }     }       return index > 0 }获取打印语句
  假设我们所需的打印语句为:  log.Logger.WithContext(ctx).WithField("filepath", filepath).Infof(format, arg0, arg1)   ,那么函数选择器的表达式可以直接使用 parser.ParseExpr   函数生成,其中的参数 (format, arg0, arg1)   手动拼接即可。 // NewCallExpr 产生一个调用表达式 // 待产生表达式:log.Logger.WithContext(ctx).Infof(arg0, arg1) // 其中: //    funcSelector = "log.Logger.WithContext(ctx).Infof" //     args = ("arg0", "arg1") // 调用语句:NewCallExpr("log.Logger.WithContext(ctx).Infof", "arg0", "arg1") func NewCallExpr(funcSelector string, args ...string) (*ast.CallExpr, error) {     // 获取函数对应的表达式     funcExpr, err := parser.ParseExpr(funcSelector)     if err != nil {         return nil, err     }       // 组装参数列表     argsExpr := make([]ast.Expr, len(args))     for i, arg := range args {         argsExpr[i] = ast.NewIdent(arg)     }       return &ast.CallExpr{         Fun:  funcExpr,         Args: argsExpr,     }, nil }
  由于出参需要等函数执行完毕后执行,所以打印出参的语句还需要放在  defer   函数内执行。 // NewFuncLitDefer 产生一个 defer 语句,运行一个匿名函数,函数体是入参语句列表 func NewFuncLitDefer(funcStmts ...ast.Stmt) *ast.DeferStmt {     return &ast.DeferStmt{         Call: &ast.CallExpr{             Fun: NewFuncLit(&ast.FuncType{}, funcStmts...),         },     } }修改函数体
  至此我们已经获得了打印入参和出参的语句,接下来就是把他们放在原本函数体的最前面,保证开始和结束时执行。  toBeAddedStmts := []ast.Stmt{     &ast.ExprStmt{X: beforeExpr},     // 离开函数时的语句使用 defer 调用     NewFuncLitDefer(&ast.ExprStmt{X: afterExpr}), }   // 我们将添加的语句放在函数体最前面 funcDecl.Body.List = append(toBeAddedStmts, funcDecl.Body.List...)运行
  为了测试我们的注释注解是否工作正确,我们使用如下代码进行测试:  package main   import (     "context"     "logannotation/testdata/log" )   func main() {     fn(context.Background(), 1, "2", "3", true) }   // @Log() func fn(ctx context.Context, a int, b, c string, d bool) (int, string, string) {     log.Logger.WithContext(ctx).Infof("#fn executing...")     return a, b, c }
  运行  go run logannotation/cmd/generator /Users/idealism/Workspaces/Go/golang-log-annotation/testdata   执行代码生成,在 /Users/idealism/Workspaces/Go/golang-log-annotation/testdata/_gen   下可找到生成的代码: package main   import (     "context"     "logannotation/testdata/log" )   func main() {     fn(context.Background(), 1, "2", "3", true) }   func fn(ctx context.Context, a int, b, c string, d bool) (_0 int, _1 string, _2 string) {     log.Logger.WithContext(         ctx).WithField("filepath", "/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go").Infof("#fn start, params: %+v, %+v, %+v, %+v", a, b, c, d)     defer func() {         log.Logger.WithContext(             ctx).WithField("filepath", "/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go").Infof("#fn end, results: %+v, %+v, %+v", _0, _1, _2)     }()       log.Logger.WithContext(ctx).Infof("#fn executing...")     return a, b, c }
  可以看到已经按照我们的想法正确生成了代码,并且运行后能按照正确的顺序打印正确的入参和出参。实际使用时会在  ctx   中加入 apm 的 traceId   ,并且在 logrus   的 Hooks   中将其在打印前放入到 Fields   中,这样搜索的时候可以将同一请求的所有日志聚合在一起。 INFO[0000] #fn start, params: 1, 2, 3, true              filepath=/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go INFO[0000] #fn executing...                              INFO[0000] #fn end, results: 1, 2, 3                     filepath=/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go扩展
  以上代码是一种简单方式地定制化处理注解,仅处理了打印日志这一逻辑,当然还存在更多扩展的可能性和优化。  注册自定义注解(这样可以把更多重复逻辑抽出来,例如:参数校验、缓存等逻辑)  同时使用多个注解  注解解析成语法树,支持注解参数  生成的代码仅在需要时换行
  相关  Demo   可以在 golang-log-annotation  找到。

哈兰德认清现实,皇马开始犹豫了,他最大的敌人是自己?姆巴佩和哈兰德也是继梅西和C罗之后出现的非常优秀的潜力球员,他们的一举一动也是备受关注,特别是那个两个球员准备同时转会的消息,也是引起了很多豪门俱乐部的关注。他们有着高效的进球效率袁心玥被夸太可爱,与冰冰有最萌身高差,学心理学是为了了解对手近日,央视的一档综艺节目引起了广大女排球迷们的关注,只因这档节目中介绍了中国女排副攻袁心玥的相关情况,下面一起跟随小编来了解一下吧。首先,这档节目透露了袁心玥和王冰冰两人相见时的对冬奥吉祥物冰墩墩出自他笔下!广州设计师曹雪登天天向上讲述设计初心文羊城晚报全媒体记者艾修煜2008年天天向上因北京夏季奥运会而生,2022年天天向上重回周五档迎接北京冬奥会的到来!1月28日晚10点,湖南卫视天天向上北京冬奥欢迎您邀请中国短道速春节手机拍照完全指南,vivoS12系列轻松拍出浓浓年味不同的人,对年味有不同的理解。家人团聚是一种年味,张灯结彩是一种年味,放烟花看春晚也是一种年味。只要我们用心,总是能发现不同的年味。当然,我们也不要忘记拿起手机,将年味画面记录下来拒绝勇士挽留,投奔湖人却被弃用!在老詹身边打球,简单但不轻松本赛季NBA常规赛赛程推进了三分之一,湖人目前13胜12负排名西部第六,战绩算不上好,但也算勉强能够接受,毕竟球队人员不齐,老詹打打停停,球队一直在磨合,到现在,都没有形成一套固定古人话养生人皆注重养生,可是为什么不能长寿养生,分养身与养心两部分。养身,即通过运动和补养,增强体质,而养心则是通过修炼素养和心态,静心安神。通常人们养生,大多注重养身,而忽视了养心,所以虽然付出很多,却达不到长寿的效果。人老了,不想这3件事,多半会长寿书中断舍离里有一句话说断,断绝不需要东西,舍,舍去多余的废物,离,脱离对物品的执着,现在对自己来说不重要的就尽管放手。其实人老了也是一样,要想过得久一点,要想多多看看儿女子孙,就要春节美学新理解,华为折叠双星同台图赏眨眼间,华为的折叠屏产品都已经经过4代更迭了,从开端的MateX到最新的P50Pocket,华为用自己美学诠释了什么叫大气之美,每一款华为折叠屏都有着浓浓的自己的浓浓的设计风格,在假如科学的尽头是神学,科学家们会不会极力掩盖事实?这是一则我怀疑探索者在头条问答的解答,现在搬运到这里。转载请随意。提问者说如果说科学的尽头真的是神学,那么传说中的仙人是不是真实存在的,或者说曾经存在过,那么地球现在为什么没有传说英如镝出征北京冬奥会难忘父亲英达的艰辛和酸楚点击关注,每天都有名人故事感动您!英达与儿子英如镝2022年1月27日,北京冬奥会中国体育代表团名单正式官宣,英如镝入选中国冰球男队,将出征北京冬奥会为国争光。英如镝是著名演员导演科比特新一代智能追踪云台,为无人机行业应用带来变革随着科技的进步,在无人机行业应用上,人们对无人机挂载已经不再仅仅局限于航拍这一单一的摄影设备,还需要更多的功能相互辅助,就比如智能追踪功能。科比特继续在智能追踪相机领域探索,近日发
宋雷100预测准确率是概率的梦幻表达唐宋大数据创业十七年专访宋雷先生小编宋总好!非常荣幸在唐宋创业十七周年之际对您进行采访。我的第一个问题,是每一位企业家都无法回避的,就是企业核心竞争力的问题。十七年时间不长也不短,一加AcePro测评手游党的最优解,并不是只有游戏手机和往年每年只体验两到三款一加手机不一样,一加AcePro是今年我们测评的第四款产品。并且从命名上看,除了之前的一加10Pro之外,今年的四款一加新机中有三款都属于Ace系列。由此不从6799元跌至1758元,麒麟芯片鸿蒙系统256GB,售价更亲民了大家好,我是唐三,下半年的手机数码市场中,最值得期待的两款手机,莫过于新一代iPhone14和一直在预热的华为Mate50系列,iPhone系列几乎每年都一样,万年不变的刘海屏,挤明白这几点,让你打金效率提高一半复古传奇这个游戏已经在国内有十多年的厉史了,却从未在大家的视线之中消散,虽然不如当初那般火热,可是在中国的游戏界依然占有一席之地的。不论是当初还是现在,都是有很多小伙伴在复古传奇中2022年8月的新游是来给我省钱的吗?文kiko做一个小调查刚刚过去的七月,各位在Stray里撸猫了吗?在异度神剑3里吐槽简中翻译了吗?在冲洗模拟器里冲了个爽吗?如果以上都没有,没关系,五花八门的八月新游正跃跃欲试,想AG未央逐渐晨化,失去灵性的原因共有两点,AG现在没人能站出来了AG的打野位保质期太短,现在的未央逐渐变成初晨,慢慢失去还没有加入AG的灵性,未央晨化的原因主要有两点。KPL夏季赛已经到了白热化阶段,AG在这个赛季的表现还是虎头蛇尾,第一轮无人打嗝未必是吃撑!或是癌症在捣鬼相信不少癌症患者都有这样的困惑明明自己没有吃多少东西,但还是感觉肚子涨涨的,甚至还会不停地打嗝,嗝嗝嗝,一直停不下来。笔者在门诊时,就经常遇到这样的病人!今天就和大家一起来聊一聊打小米平板5Pro12。4小米手表S1Pro小米Buds4Pro耳机官宣8月9日,小米正式宣布将于8月11日发布小米平板5Pro12。4英寸版本。小米官方表示,小米平板5Pro12。4英寸平板采用超大屏幕,激发生产力的更多可能,将带来更宽阔的视野更便捷国产手机联手发大招,发布金标认证,对抗AppStore说来也是搞笑,每当黑马看见有人在讨论iPhone和安卓谁更好的时候,总会出现体验二字。大家争论最多的点无非就是iOS的软件体验更好,安卓的硬件体验更好。是,黑马也承认,因为苹果对A加密领域是如何影响电子竞技发展的?图片来源视觉中国文陀螺财经,作者章鱼哥近年来,加密公司一直在向电子竞技领域注入资金,见证了电子竞技的爆炸式增长,比特币也已被用于一些电竞联赛奖池。2022年电子竞技赛事总支出也已飙想要性价比高蓝牙耳机?震撼音质40H续航,QCYT13成多数人最爱?前言随着蓝牙耳机技术不断更新换代,用户对它的要求不仅仅是音质和续航能力,对它的外观要求也是越来越高。有那么一款蓝牙耳机,它不仅拥有震撼的音效,还拥有让人喜爱的迷你小巧外观,充电仓也