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()时就会调用自定义解析函数。

go 复制代码
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接口定义如下:

go 复制代码
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
} 


其中的type Alias DevData,是为了避免套娃无线递归问题。

但是引入了新的问题: 对象多拷贝一次,当数据量比较大,api调用比较频繁的时候,浪费cpu时间。

可以用解析之后,用map遍历,我最开始写golang代码时就用过这个笨方法。

好的解决办法是:限定拷贝的数据范围,将需要特殊处理的时间戳,单独封装在一个结构体里面。这样只有特殊字段会多拷贝一次,其他字段不会出现拷贝。

go 复制代码
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
}
相关推荐
JaguarJack几秒前
PHP 标准库的价值远不止少写几行代码
后端·php·服务端
鶴哥只手遮天20 分钟前
《Redis 8.10.3 深度剖析》系列 · 第 1 章·全景与源码地图:版本谱系、构建体系、全局状态与启动链路
redis·后端·开源
用户4003150433520 分钟前
《从 skynet.timeout 到回调执行:Skynet 分层时间轮源码剖析》
后端
一条Go25 分钟前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
用户19961435599636 分钟前
插件是怎么被安全装进系统的
后端
Pniubi1 小时前
Java为什么要分堆和栈
java·开发语言
愚农搬码1 小时前
自研表单引擎、开源组件二开和商业产品,企业应该如何选择?
后端
用户298698530141 小时前
Python 自动化处理 Word 页眉页脚的几种典型场景
后端·python·api
小小晓.1 小时前
Qt 对话框、QLineEdit 与布局管理实战详解
开发语言·qt
愚农搬码2 小时前
一款企业级电子表单系统应该具备哪些功能?
后端