• 发生时间:2022-10-26

  • 问题描述:同事在测试产线平台前端时,发现后端返回的数据的「创建日期」格式为 2022-10-25T07:05:02.063Z,可读性不好,希望后端修改一下。

  • 问题类别:软件开发

  • 原因分析:

    • 产线平台后端使用 gorm 来完成数据库相关操作,其中需要为每张数据库表定义一个对应的结构体。 由于每张数据库表都拥有一些共同的字段,因此这些结构体都内嵌了一个公共的结构体 Model(如下所示)。
    // Model represents the common information of every model.
    type Model struct {
        ID        uint      `gorm:"primarykey" json:"id"`
        CreatedAt time.Time `                  json:"create_time"`
        UpdatedAt time.Time `                  json:"update_time"`
    }
    
    • 产线平台接收到前端的请求后,会进行相关的数据库操作,并将结果保存到对应的结构体中。 该结构体会作为响应结果的 data 字段,被 gin 框架进行 json 序列化后再返回给用户。

    • go 将 time.Time 类型的变量进行 json 序列化后的格式便是 2022-10-25T07:05:02.063Z, 其中 T 为日期与时间的分隔符,,Z代表零时区,详见 What exactly does the T and Z mean in timestamp?

  • 解决方案:

    • 比较直接的思路是在 json 序列化后再调整 data 字段中所有 time.Time 字段的时间格式, 但如果 data 字段比较复杂时处理起来会比较麻烦,效率较低,代码显得也不优雅。
    • 因此我一直在考虑如何在 json 序列化的时候就采用自定义的时间格式,而非序列化完成后再调整时间格式。
    • 经过网上调查,在 stack overflow 找到一个解决方案(如下所示)。 其思路是将 time.Time 封装成自定义类型,然后为新类型定义 MarshalJSON 方法,在该方法内修改时间格式即可。
    type JSONTime time.Time
    
    func (t JSONTime)MarshalJSON() ([]byte, error) {
    stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("2006-01-02 15:04:05"))
        return []byte(stamp), nil
    }
    
    • 于是我将 Model 结构体中 CreatedAtUpdatedAt 修改为 JSONTime 类型,如下所示。
    // Model represents the common information of every model.
    type Model struct {
        ID        uint     `gorm:"primarykey" json:"id"`
        CreatedAt JSONTime `                  json:"create_time"`
        UpdatedAt JSONTime `                  json:"update_time"`
    }
    
    • 不幸的是,修改后,虽然编译可以通过,但在运行时会报错:sql: converting argument $1 type: unsupported type model.JSONTime, a struct
    • 原来,gorm 只能处理 in64, float64, bool, []byte, string, time.Time, nil 这些类型,默认不能识别自定义类型。 如果要使用自定义类型,那么自定义类型必须实现 ScannerValuer 接口,详见 gorm 官方文档。 简单解释一下,Scanner 接口是告诉 gorm 如何将数据库支持的数据类型转换为自定义类型,Valuer 接口是告诉 gorm 如何将自定义类型转换为数据库支持的数据类型。
    • 因此,我还需要为 JSONTime 定义 ScanValue 这两个方法,如下所示。
    // Scan scans value into JSONTime, implements sql.Scanner interface.
    func (t *JSONTime) Scan(value interface{}) error {
        v, ok := value.(time.Time)
        if !ok {
            return fmt.Errorf("invalid value: %s. type: %T", value, value)
        }
    
        *t = JSONTime(v)
    
        return nil
    }
    
    // Value returns json value, implement driver.Valuer interface.
    func (t JSONTime) Value() (driver.Value, error) {
        return time.Time(t), nil
    }
    
    • 再次不幸的是,修改后,gorm 不再自动给 CreatedAtUpdatedAt 字段赋值了,需要用户每次创建或更新条目时自行给条目中的时间字段的赋值。 虽然失去了一点便利性,但相比每次查询都要手动调整时间格式而言,这种方案还是更优的选择。
  • 实施结果:采用上述解决方案后,产线平台后端返回的数据中的时间格式调整为2022-10-25 07:05:02,可读性更好。

  • 经验总结:

    • Go 语言可以为自定义类型定义 json 序列化方法,也可以在 gorm 中使用自定义类型,只需要实现一些相应的接口即可。