golang自定义MarshalJSON、UnmarshalJSON 原理和技巧
问题出现的原因:在前后端分离的项目中,经常出现的问题是时间戳格式的问题。
后端的日期格式兼容性强,比较完善。前端由于各种原因,日期格式不完善。
就会产生矛盾。
ms int64比较通用,但是unix时间没有可读性,不方便db运维的工作。
以golang为例,讲一下时间戳转换,在内存里面转换时间戳。
golang的时间戳类型叫做:time.Time,标准是RFC 3339。
RFC 3339 是一种日期 - 时间格式的互联网标准,它基于 ISO 8601 格式。其基本格式为YYYY - MM - DDTHH:MM:SS±HH:MM,其中:
- YYYY表示四位年份,例如2024。
- MM表示两位月份,范围是01 - 12。
- DD表示两位日期,范围是01 - 31。
- T是日期和时间部分的分隔符,它是一个固定的字符,用于区分日期和时间。
- HH表示两位小时数,采用 24 小时制,范围是00 - 23。
- MM表示两位分钟数,范围是00 - 59。
- SS表示两位秒数,范围是00 - 59。
- ±HH:MM表示时区偏移量,其中+或-表示相对于 UTC(协调世界时)的偏移方向,HH和MM分别表示小时和分钟的偏移量。例如,+08:00表示东八区,比 UTC 快 8 小时;-05:00表示西五区,比 UTC 慢 5 小时。
例如:
- 2024-06-15T14:30:00+00:00:表示 2024 年 6 月 15 日,下午 2 点 30 分(14:30),时区为 UTC(偏移量为+00:00)。
- 2024-12-31T23:59:59-05:00:表示 2024 年 12 月 31 日,晚上 11 点 59 分 59 秒(23:59:59),时区为西五区(偏移量为-05:00)。
为方便展示,我绘制了下面的草图,一个https请求从客户端到达数据库,需要经历的最短路径。 nginx作为入口时,但是数据没有格式化,不适合作为拦截点。 往右边继续看,很明显的发现数据流只有在json 序列化/反序列化的时候开始汇聚。 那RFC 3339 标准的time.Time转换为毫秒在这里实现,无疑是工作量最小的修改方式。
(就像小时候,灌酿造的酱油时,在瓶口处放置一个滤网,过滤掉杂质。 也好像查干湖捕鱼时,工人站在冰面出口处将一个一个的鱼勾起来。 又好像,蜀黍办案,在高速路口排兵布阵,是一样的道理。 在数据处理,找到数据的入口 或 出口,然后轻松拿捏。)
(golang语法或代码没有什么好讲的,总可以借鉴或自定义,属于闻道有先后性质的,没有高低之分。 但是一种思维方式,值得推而广之,用在工作和生活的方方面面,减少你前行的阻力。)
自定义MarshalJSON, UnmarshalJSON。当应用调用json.Marshal(), json.UnMarshal()时就会调用自定义解析函数。
type DevData struct {
QrCodeStr string `json:"qrCodeStr"`
StartTime time.Time `json:"StartTime,omitempty" swaggerignore:"false"`
StartTimeStamp int64 `json:"startTimeStamp"`
}
func (c *DevData) MarshalJSON() ([]byte, error) {
type Alias DevData
aux := &Alias{}
*aux = Alias(*c)
aux.StartTime = time.Unix(aux.StartTimeStamp, 0)
aux.EndTime = time.Unix(aux.EndTimeStamp, 0)
if data, err := json.Marshal(aux); err == nil {
return data, nil
} else {
return nil, err
}
}
func (c *DevData) UnmarshalJSON(data []byte) error {
type Alias DevData
aux := &Alias{}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
aux.StartTime = time.Unix(aux.StartTimeStamp, 0)
aux.EndTime = time.Unix(aux.EndTimeStamp, 0)
*c = DevData(*aux)
return nil
}
在哪里产生关联:
当一个类型实现了encoding/json包中的json.Marshaler接口的MarshalJSON方法时,json.Marshal函数就会调用这个自定义的MarshalJSON方法来进行 JSON 序列化。json.Marshaler接口定义如下:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
其中的type Alias DevData,是为了避免套娃无线递归问题。
但是引入了新的问题: 对象多拷贝一次,当数据量比较大,api调用比较频繁的时候,浪费cpu时间。
可以用解析之后,用map遍历,我最开始写golang代码时就用过这个笨方法。
好的解决办法是:限定拷贝的数据范围,将需要特殊处理的时间戳,单独封装在一个结构体里面。这样只有特殊字段会多拷贝一次,其他字段不会出现拷贝。
type TimeInfo struct {
StartTime time.Time `json:"StartTime,omitempty"` // 设置标签:omitempty,客户端传入空值时,忽略该字段解析不会报错
StartTimeStamp int64 `json:"startTimeStamp,omitempty" bson:"-,omitempty"` // 应用传入空值时,解析不报错。 bson注解表示,StartTimeStamp 是一个内存临时变量,不会写入mongodb数据库。
}
func (c *TimeInfo) MarshalJSON() ([]byte, error) {
type Alias TimeInfo
aux := &Alias{}
*aux = Alias(*c)
aux.StartTime = time.Unix(aux.StartTimeStamp, 0)
aux.EndTime = time.Unix(aux.EndTimeStamp, 0)
if data, err := json.Marshal(aux); err == nil {
return data, nil
} else {
return nil, err
}
}
func (c *TimeInfo) UnmarshalJSON(data []byte) error {
type Alias TimeInfo
aux := &Alias{}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
aux.StartTime = time.Unix(aux.StartTimeStamp, 0)
aux.EndTime = time.Unix(aux.EndTimeStamp, 0)
*c = TimeInfo(*aux)
return nil
}
type DevData struct {
QrCodeStr string `json:"qrCodeStr"`
TimeInfo
}