最近在日志中发现一些奇怪的日志,大致长这样:

Error 2020-08-28 06:59:38.813+00:00 ... _msg=get immersion context, fetch tracks failed, error: <nil>

打印了 Error 日志,error 打印出来却是 <nil>,乍眼一看,以为又遇到了 Go 里面 nil != nil 的问题,但找到对应的那行代码是这样的:

tracks, errResult = TrackService.GetRpcTracks(httpCtx, trackIds)
if errResult != nil {
logs.CtxError(httpCtx.RpcContext(), "get immersion context, fetch tracks failed, error: %v", errResult)
return
}

errResult 的类型是 *ErrorResult ,GetRpcTracks 函数返回的类型也是*ErrorResult,经过仔细研究,排除了这种可能性。

那就很奇怪了,errResult != nil 显然要成立才会往下走,一个非 nil 的结构体指针打印出来却是 nil ???

就在挠头搔耳也找不到答案时,决定再根据日志上下文来查找答案。GetRpcTracks 函数是根据 TrackIDs 来获取Track信息,如果找不到会返回错误。根据日志流,找到该请求的全部日志,其中有一条显示

Error 2020-08-28 06:59:38.815+00:00 ... _msg=no tracks found, track ids: [676648224091215xxxx]

对应的代码是:

if len(tracks) == 0 {
logs.CtxError(httpCtx.RpcContext(), "no tracks found, track ids: %v", trackIds)
errResult = ginf_action.NewErrorResult(errors.ResourceNotFound, nil, "")
}

上数据库里查了一下对应的TrackID,发现状态确实为不可用,所以接口肯定查不出来数据,这样的话 GetRpcTracks 函数返回的就是由 ginf_action.NewErrorResult(errors.ResourceNotFound, nil, "") 所构建的结构体指针。NewErrorResult 的代码其实很简单:

func NewErrorResult(catalog ErrorCatalog, err error, message string, params ...interface{}) *ErrorResult {
return &ErrorResult{
Catalog: catalog,
Err: err,
Message: fmt.Sprintf(message, params...),
}
}

所以可以肯定,tracks, errResult = TrackService.GetRpcTracks(httpCtx, trackIds) 这里返回的 errResult 确实没什么问题,那么问题应该就出在打印日志的时候了。

于是我构建了一个新的结构体来进行了测试:

type CustomNil struct {
msg string
err error
} func TestErr(t *testing.T) {
c := &CustomNil{
msg: "",
err: nil,
} fmt.Printf("CustomNil:%v", c)
}

打印出来的日志为:

CustomNil:&{ <nil>}

跟之前日志里打印的很像,但是不一样,前面日志是这样的:error: <nil> 没有 &,也没有大括号。于是我跟同事讨论了一下,为什么会出现这样的情况,同事说可能是String方法导致的。于是我给 CustomNil 加了一个方法:

func (c *CustomNil) String() string {
return "test"
}

重新跑一下发现日志变成了这样:

CustomNil:test

显然,使用 %v 占位符时会调用 String() 方法,所以有可能是 *ErrorResult 的 String() 方法里返回了 nil。但很快发现 *ErrorResult 根本没有实现 String() 方法,但是实现了 Error() 方法,便想会不会是这家伙导致的,于是继续进行实验,再添加一个方法:

func (c *CustomNil) Error() string {
return fmt.Sprint(c.err)
}

重新跑代码之后,日志长这样:

CustomNil:<nil>

这下终于找到原因了,%v 占位符会优先调用 Error() 方法来打印日志,没有 Error() 方法时会调用 String() 方法来打印,这两个函数都没有的情况下,会将结构体内的各个变量顺序打印。

那么问题来了,为什么是这样的呢?于是继续往下翻代码:

func (l *Logger) CtxError(ctx context.Context, format string, v ...interface{}) {
if LevelError < l.CtxLevel(ctx) {
return
}
if len(v) == 0 {
l.fmtLog(ctx, LevelError, "", format)
return
}
l.fmtLog(ctx, LevelError, "", fmt.Sprintf(format, v...))
}

CtxError 方法里调用了 fmt.Sprintf(format, v...) 来处理参数,fmt 包的 Sprintf 就很复杂了,经过一番研究,终于找到了关键代码:

func (p *pp) handleMethods(verb rune) (handled bool) {
if p.erroring {
return
}
...
// If we're doing Go syntax and the argument knows how to supply it, take care of it now.
if p.fmt.sharpV {
...
} else {
// If a string is acceptable according to the format, see if
// the value satisfies one of the string-valued interfaces.
// Println etc. set verb to %v, which is "stringable".
switch verb {
case 'v', 's', 'x', 'X', 'q':
// Is it an error or Stringer?
// The duplication in the bodies is necessary:
// setting handled and deferring catchPanic
// must happen before calling the method.
switch v := p.arg.(type) {
case error:
handled = true
defer p.catchPanic(p.arg, verb, "Error")
p.fmtString(v.Error(), verb)
return case Stringer:
handled = true
defer p.catchPanic(p.arg, verb, "String")
p.fmtString(v.String(), verb)
return
}
}
}
return false
}

看到这里,就豁然开朗了,如果使用了 %v 占位符,会依次判断它是否实现了 error 接口和 Stinger 接口并调用 Error() 或 String() 方法来进行输出。

到此,问题就已经研究清楚了,所以使用 fmt 包来进行日志格式化时还是要注意这一点,否则就会出现一些奇奇怪怪的日志,增加不必要的麻烦。当然,在这个 case 下,这样的情况打 Error 等级的日志是否合适也是值得商讨的。

这次问题排查没有花太多时间,但整个过程就像解密一样酣畅淋漓,感觉十分有趣,最后还能从中学到一些新东西,可谓收获颇丰。特此记录下来,希望能与君共勉。

【Go语言探险】线上奇怪日志问题的排查的更多相关文章

  1. Linux(2)---记录一次线上服务 CPU 100%的排查过程

    Linux(2)---记录一次线上服务 CPU 100%的排查过程 当时产生CPU飙升接近100%的原因是因为项目中的websocket时时断开又重连导致CPU飙升接近100% .如何排查的呢 是通过 ...

  2. 一次线上CPU高的问题排查实践

    一次线上CPU高的问题排查实践 前言 近期某一天上班一开电脑,就收到了运维警报,有两台服务CPU负载很高,同时收到一线同事反馈 系统访问速度非常慢,几乎无响应. 一个美好的早晨,最怕什么就来什么.只好 ...

  3. 线上CPU飙升100%问题排查

    本文转载自线上CPU飙升100%问题排查 引子 对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!特此整理排查方法一篇,供大家参考 ...

  4. 线上Redis偶发性链接失败排查记

    问题过程 输入法业务于12月12日上线了词库接受业务,对部分用户根据用户uuid判断进行回传,在12月17日早上8点多开始出现大量的php报错(Redis went away),报错导致了大量的链接积 ...

  5. 一次性搞清楚线上CPU100%,频繁FullGC排查套路

    “ 处理过线上问题的同学基本上都会遇到系统突然运行缓慢,CPU 100%,以及 Full GC 次数过多的问题. 当然,这些问题最终导致的直观现象就是系统运行缓慢,并且有大量的报警. 本文主要针对系统 ...

  6. 一次线上Redis类转换异常排查引发的思考

    之前同事反馈说线上遇到Redis反序列化异常问题,异常如下: XxxClass1 cannot be cast to XxxClass2 已知信息如下: 该异常不是必现的,偶尔才会出现: 出现该异常后 ...

  7. 原创 记录一次线上Mysql慢查询问题排查过程

    背景 前段时间收到运维反馈,线上Mysql数据库凌晨时候出现慢查询的报警,并把原始sql发了过来: --去除了业务含义的sql update test_user set a=1 where id=1; ...

  8. 一则线上MySql连接异常的排查过程

    Mysql作为一个常用数据库,在互联网系统应用很多.有些故障是其自身的bug,有些则不是,这里以前段时间遇到的问题举例. 问题 当时遇到的症状是这样的,我们的应用在线上测试环境,JMeter测试过程中 ...

  9. 线上CPU飙升100%问题排查,一篇足矣

    一.引子 对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!特此整理排查方法一篇,供大家参考讨论提高. 二.问题复现 线上系统突然 ...

随机推荐

  1. PHP password_get_info() 函数

    password_get_info() 函数用于返回指定散列(hash)的相关信息. PHP 版本要求: PHP 5 >= 5.5.0, PHP 7高佣联盟 www.cgewang.com 语法 ...

  2. C/C++编程笔记:C语言打造中国象棋游戏,项目源代码分享!

    中国象棋是起源于中国的一种棋,属于二人对抗性游戏的一种,在中国有着悠久的历史.由于用具简单,趣味性强,成为流行极为广泛的棋艺活动. 它是中国棋文化,也是中华民族的文化瑰宝,它源远流长,趣味浓厚,基本规 ...

  3. PyCharm2020激活破解教程

    本文内容皆为作者原创,如需转载,请注明出处:https://www.cnblogs.com/xuexianqi/p/12767075.html 免责声明:本方法只做学习研究之用,不得用于商业用途 若经 ...

  4. Navicat15安装教程

    本文内容皆为作者原创,如需转载,请注明出处:https://www.cnblogs.com/xuexianqi/p/12797170.html 一:简介 Navicat是一套快速.可靠的数据库管理工具 ...

  5. JDK11.0.7下载及安装详细教程(win10)

    0.背景知识 JRE: Java Runtime Environment JDK:Java Development Kit JRE顾名思义是java运行时环境,包含了java虚拟机,java基础类库. ...

  6. 14、Cahin of Responsibility 责任链 COR设计模式

    1.责任链模式 chain of responsibility 责任链模式 责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某 ...

  7. Weblogic 连接 RMI 服务报错 Connection refused

    WebLogic 连接 RMI 服务报错 Connection refused 访问 WebLogic RMI 服务报错,连接被拒绝,连接超时. 奇怪的是,报错的 host 根本不是我要访问的. 报错 ...

  8. Java单元测试 Junit TestNG之介绍

    Junit是Java中默认的单元测试框架,通过注解的方式去识别测试方法 JUnit4 JUnit4通过注解的方式来识别测试方法.目前支持的主要注解有: @BeforeClass 全局只会执行一次,而且 ...

  9. Java 变量及运算符

    Java概述 Java的发展可以归纳如下的几个阶段: (1)第一阶段(完善期):JDK 1.0 ( 1995年推出)一JDK 1.2 (1998年推出,Java更名为Java 2): (2)第二阶段( ...

  10. NodeJs path.resolve的使用

    __dirname __dirname 指向运行代码的文件夹 console.info('__dirname', __dirname) // C:\Leslie\Web_learning\Daily- ...