• 发生时间:2022-08-25

  • 问题描述:

    1. 在开发产线平台的日志管理模块时,我定义了结构体 FtLog 来记录日志信息(如下所示)。
    2. 当上传日志失败时,我希望在错误日志中打印 FtLog 的详细信息。 默认的打印格式是只会打印结构体每个字段的取值,而我希望同时打印字段的名字和取值。
    3. 因此,我为 FtLog 实现了 String 方法,采用了Go 标准库 fmt 的 “%+v” 格式
    4. 对产线平台进行测试,构造上传日志失败的场景,发现程序会崩溃,报错 fatal error: stack overflow
    type FtLog struct {
        TestSuccess  bool   `form:"success"      binding:"required"` // 测试是否成功
        FactoryName  string `form:"factory"      binding:"required"` // 工厂名字
        StationName  string `form:"station"      binding:"required"` // 工位名字
        ProductModel string `form:"productModel" binding:"required"` // 产品型号
        DeviceType   string `form:"deviceType"   binding:"required"` // 设备类型
        DeviceId     string `form:"deviceId"     binding:"required"` // 设备ID
        FileName     string `form:"fileName"`                        // 日志文件名
        SavePath     string `form:"savePath"`                        // 日志保存路径
    }
    
    func (l *FtLog) String() string {
        return fmt.Sprintf("%+v", l)
    }
    
  • 问题类别:软件开发

  • 原因分析:

    1. Go 的源码保存在 Go 安装目录的 src 子目录中, 我们打开 src/fmt/print.go 文件即可查看 fmt.Sprintf 的实现。
    2. 查看源码得知,当 Sprintf 函数的第一个参数含有 v 字符时,Sprintf 函数会调用输入变量的 String 方法进行打印。
    3. 也就是说,我们在 String 中调用 Sprintf,而 Sprintf 又会调用 String,这构成了递归。函数不停被递归调用,导致栈溢出。
  • 解决方案:改用 json.marshal 以代替 fmt.Sprintf,既避免了递归,也达到了同时打印字段的名字和取值的目的。

    func (l *FtLog) String() string {
        data, _ := json.Marshal(l)
        return string(data)
    }
    
  • 实施结果:修改后程序可以正常运行,打印结果符合预期。

  • 经验总结:

    1. Go 语言中为结构体自定义 String 方法时,要慎用 fmt 库,并注意避免递归。
    2. Go 语言中实现一个接口时,要了解该接口的使用场景(会被谁调用、怎么调用等),避免踩坑。
    3. Go 语言标准库的源码保存在 Go 安装目录的 src 子目录中,查看起来特别方便。

fmt 通用格式化字符串

%v    the value in a default format
      when printing structs, the plus flag (%+v) adds field names
%#v   a Go-syntax representation of the value
%T    a Go-syntax representation of the type of the value
%%    a literal percent sign; consumes no value