修改go的时间类型time.Time序列化为时间戳——以及更通用的自定义json序列化方式

0. 问题

go的json对Time类型的序列化结果是2020-07-16T14:49:50.3269159+08:00这种类型。我们希望改成时间戳。

1. 网上有各种现成的做法

1.1 辅助结构体

package main_test

import (
    "encoding/json"
    "log"
    "testing"
    "time"
)

type SelfUser struct {
    ID         int64     `json:"id"`
    Name       string    `json:"name"`
    CreateTime time.Time `json:"createTime"`
}

func (u *SelfUser) MarshalJSON() ([]byte, error) {
    return json.Marshal(&struct {
        ID       int64  `json:"id"`
        Name     string `json:"name"`
        CreateTime int64  `json:"createTime"`
    }{
        ID:       u.ID,
        Name:     u.Name,
        CreateTime: u.CreateTime.Unix(),
    })
}

func (s *SelfUser) UnmarshalJSON(data []byte) error {
    tmp := &struct{
        ID         int64     `json:"id"`
        Name       string    `json:"name"`
        CreateTime int64 `json:"createTime"`
    } {}
    err := json.Unmarshal(data, tmp)
    if err != nil {
        return err
    }
    s.ID = tmp.ID
    s.Name = tmp.Name
    s.CreateTime = time.Unix(tmp.CreateTime, 0)
    return nil
}

func TestJson3(t *testing.T) {
    user := &SelfUser{
        ID:         0,
        Name:       "testUser",
        CreateTime: time.Now(),
    }
    res, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%v", string(res))
}

每个结构体都要写一个辅助结构体,码字量翻倍,如果公司按照代码行数算kpi这倒是一个好方法

1.2 使用别名

在1.1的基础上把MarshalJSON和UnmarshalJSON方法修改一下:

func (s *SelfUser) MarshalJSON() ([]byte, error) {
    type Alias SelfUser
    return json.Marshal(&struct {
        CreateTime int64 `json:"createTime"`
        *Alias
    }{
        CreateTime: s.CreateTime.Unix(),
        Alias:    (*Alias)(s),
    })
}

func (s *SelfUser) UnmarshalJSON(data []byte) error {
    type Alias SelfUser
    tmp := &struct{
        *Alias
        CreateTime int64 `json:"createTime"`
    } {}
    err := json.Unmarshal(data, tmp)
    if err != nil {
        return err
    }
    s.ID = tmp.ID
    s.Name = tmp.Name
    s.CreateTime = time.Unix(tmp.CreateTime, 0)
    return nil
}

本质上和1.1没有什么区别,就是代码行数少了。
注意一个问题,如果这里不用别名而直接用SelfUser类

tmp := &struct{
        *SelfUser
        CreateTime int64 `json:"createTime"`
    } {}

会造成SelfUser反序列化调用无限嵌套,最后栈溢出。

1.3 受1.2启发,缩小修改范围,直接创建一个Time的别名类

上面的方法需要在每个结构体里面去做一个Time的别名类,为什么不直接做一个公共的Time别名类呢?

package main_test

import (
    "encoding/json"
    "log"
    "strconv"
    "testing"
    "time"
)

type Time time.Time

func (t *Time) UnmarshalJSON(data []byte) (err error) {
    num, err := strconv.Atoi(string(data))
    if err != nil {
        return err
    }
    *t = Time(time.Unix(int64(num), 0))
    return
}

func (t Time) MarshalJSON() ([]byte, error) {
    return ([]byte)(strconv.FormatInt(time.Time(t).Unix(), 10)), nil
}

func TestJson3(t *testing.T) {
    dateTime := Time(time.Now())
    res, err := json.Marshal(dateTime)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(string(res))
    dateTime2 := Time(time.Time{})
    err = json.Unmarshal(res, &dateTime2)
    log.Printf("%v\n", time.Time(dateTime2).String())
}

执行输出:

=== RUN   TestJson3
2020/07/16 16:07:28 1594886848
2020/07/16 16:07:28 {0 63730483648 0x9b26c0}
--- PASS: TestJson3 (0.01s)
PASS

我们在SelfUser中使用这个类:

package main_test

import (
    "encoding/json"
    "log"
    "strconv"
    "testing"
    "time"
)

type Time time.Time

func (t *Time) UnmarshalJSON(data []byte) (err error) {
    num, err := strconv.Atoi(string(data))
    if err != nil {
        return err
    }
    *t = Time(time.Unix(int64(num), 0))
    return
}

func (t Time) MarshalJSON() ([]byte, error) {
    return ([]byte)(strconv.FormatInt(time.Time(t).Unix(), 10)), nil
}

type SelfUser struct {
    ID         int64  `json:"id"`
    Name       string `json:"name"`
    CreateTime Time   `json:"createTime"`
}

func TestJson3(t *testing.T) {
    user := &SelfUser{
        ID:         0,
        Name:       "testUser",
        CreateTime: Time(time.Now()),
    }
    res, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%v\n", string(res))
    user2 := &SelfUser{}
    err = json.Unmarshal(res, user2)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%v\n", *user2)
}

执行输出:

=== RUN   TestJson3
2020/07/16 16:06:19 {"id":0,"name":"testUser","createTime":1594886779}
2020/07/16 16:06:19 {0 testUser {0 63730483579 0x9b26c0}}
--- PASS: TestJson3 (0.01s)
PASS

这个方法有一个问题,log.Printf("%v\n", *user2)输出的是{0 testUser {0 63730481503 0x9b26c0}},而如果直接使用time.Time类则会输出{0 testUser 2020-07-16 15:33:56.9806447 +0800 CST},修改之后不直观了。
这个问题可以忽略不计,或者自己写一下Time的String方法,如下:

const (
    timeFormart = "2006-01-02 15:04:05"
)

func (t Time) String() string{
    b := make([]byte, 0, len(timeFormart))
    b = time.Time(t).AppendFormat(b, timeFormart)
    return string(b)
}

这个方法还有一个很大的优点就是不影响现有框架例如ORM框架在映射数据库日期类时对日期类的解析。

1.4 直接创建一个Time的匿名继承类

package main_test

import (
    "encoding/json"
    "log"
    "strconv"
    "testing"
    "time"
)

type Time struct {
    time.Time
}

func (t *Time) UnmarshalJSON(data []byte) error {
    num, err := strconv.Atoi(string(data))
    if err != nil {
        return err
    }
    t.Time = time.Unix(int64(num), 0)
    return nil
}

func (t Time) MarshalJSON() ([]byte, error) {
    return ([]byte)(strconv.FormatInt(t.Time.Unix(), 10)), nil
}

func TestJson3(t *testing.T) {
    var dateTime Time
    dateTime.Time = time.Now()
    res, err := json.Marshal(dateTime)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(string(res))
    var dateTime2 Time
    err = json.Unmarshal(res, &dateTime2)
    log.Printf("%v\n", dateTime2)
}

执行输出:

=== RUN   TestJson3
2020/07/16 15:47:59 1594885679
2020/07/16 15:47:59 2020-07-16 15:47:59 +0800 CST
--- PASS: TestJson3 (0.01s)
PASS

我们在SelfUser中使用这个类:

package main_test

import (
    "encoding/json"
    "log"
    "strconv"
    "testing"
    "time"
)

type Time struct {
    time.Time
}

func (t *Time) UnmarshalJSON(data []byte) error {
    num, err := strconv.Atoi(string(data))
    if err != nil {
        return err
    }
    t.Time = time.Unix(int64(num), 0)
    return nil
}

func (t Time) MarshalJSON() ([]byte, error) {
    return ([]byte)(strconv.FormatInt(t.Time.Unix(), 10)), nil
}

type SelfUser struct {
    ID         int64     `json:"id"`
    Name       string    `json:"name"`
    CreateTime Time `json:"createTime"`
}

func TestJson3(t *testing.T) {
    user := &SelfUser{
        ID:         0,
        Name:       "testUser",
    }
    var dateTime Time
    dateTime.Time = time.Now()
    user.CreateTime = dateTime
    res, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%v\n", string(res))
    user2 := &SelfUser{}
    err = json.Unmarshal(res, user2)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%v\n", *user2)
}

执行输出:

=== RUN   TestJson3
2020/07/16 15:58:51 {"id":0,"name":"testUser","createTime":1594886331}
2020/07/16 15:58:51 {0 testUser 2020-07-16 15:58:51 +0800 CST}
--- PASS: TestJson3 (0.02s)
PASS

相比1.3,使用Golang匿名结构体的特性实现了Time对time.Time的 “伪继承” (go没有继承,只是看起来很像),这样 Time是可以调用time.Time的所有方法的,所以我们看到log.Printf("%v\n", *user2)输出的是{0 testUser 2020-07-16 15:58:51 +0800 CST},因为Time有String方法。
缺点是Time不再是time.Time类,使用ORM框架时无法映射数据库的日期类了,会报错unsupported Scan, storing driver.Value type time.Time into type *main_test.Time

2. 自定义每个结构体的MarshalJSON和UnmarshalJSON方法

一开始脑筋没转过弯来,想着把需要使用自定义json的参数所在的结构体重写一套通用的MarshalJSON和UnmarshalJSON方法,写的很艰难。代码如下:

package main_test

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "reflect"
    "strconv"
    "strings"
    "testing"
    "time"
)

type VssUser struct {
    Id         int64     `json:"id"`
    Name       string    `json:"name"`
    CreateTime time.Time `json:"createTime"`
    UpdateTime time.Time `json:"updateTime"`
}

// MarshalJSON 序列化方法
func (s *VssUser) MarshalJSON() ([]byte, error) {
    log.Println("自定义json序列化")
    buffer := bytes.NewBufferString("{")
    reType := reflect.TypeOf(*s)
    reValue := reflect.ValueOf(*s)
    count := reType.NumField() - 1
    for i := 0; i < reType.NumField(); i++ {
        jsonKey := getJsonKey(reType.Field(i))
        jsonValue, err := getJsonValue(reValue.Field(i))
        if err != nil {
            return nil, err
        }
        buffer.WriteString(fmt.Sprintf("\"%v\":%v", jsonKey, string(jsonValue)))
        if i < count {
            buffer.WriteString(",")
        }
    }
    buffer.WriteString("}")
    return buffer.Bytes(), nil
}

// getJsonKey 获取json的key,不考虑忽略默认值的事,不管omitempty标签
func getJsonKey(field reflect.StructField) string {
    jsonTag := field.Tag.Get("json")
    if len(jsonTag) == 0 {
        return field.Name
    } else {
        return strings.Split(jsonTag, ",")[0]
    }
}

func getJsonValue(value reflect.Value) ([]byte, error) {
    // 指针需要使用Elem取值
    if value.Kind() == reflect.Ptr {
        return jsonValue(value.Elem())
    } else {
        return jsonValue(value)
    }
}

func jsonValue(value reflect.Value) ([]byte, error) {
    // time.Time类型特殊处理,改为时间戳
    if value.Type().String() == "time.Time" {
        method := value.MethodByName("Unix")
        in := make([]reflect.Value, 0)
        rtn := method.Call(in)
        return ([]byte)(strconv.FormatInt(rtn[0].Int(), 10)), nil
    } else {
        return json.Marshal(value.Interface())
    }
}

func (s *VssUser) UnmarshalJSON(data []byte) error {
    log.Println("自定义json反序列化")
    // 先全部用接口接收
    commonArr := make(map[string]interface{})
    err := json.Unmarshal(data, &commonArr)
    if err != nil {
        return err
  }
    reValue := reflect.ValueOf(s)
    reType := reflect.TypeOf(*s)
    for i:=0; i<reType.NumField(); i++ {
        jsonKey := getJsonKey(reType.Field(i))
        // 每种数据类型都要针对性处理,暂时就只写int64、string、Time了
        switch reType.Field(i).Type.String() {
        case "time.Time":
            // 接口对象通过.(a)就转换成a类型,只有接口对象
            jsonValue := commonArr[jsonKey].(float64)
            time := time.Unix(int64(jsonValue), 0)
            reValue.Elem().Field(i).Set(reflect.ValueOf(time))
        case "int64":
            jsonValue := commonArr[jsonKey].(float64)
            reValue.Elem().Field(i).Set(reflect.ValueOf(int64(jsonValue)))
        case "string":
            jsonValue := commonArr[jsonKey].(string)
            reValue.Elem().Field(i).Set(reflect.ValueOf(jsonValue))
        default:
            return errors.New("value error")
        }
    }
    return nil
}

func TestJson2(t *testing.T) {
    vssUser := &VssUser{
        Id: 0,
        Name: "testUser",
        CreateTime: time.Now(),
        UpdateTime: time.Now(),
    }
    res, err := json.Marshal(vssUser)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(string(res))
    dateTime2 := &VssUser{}
    json.Unmarshal(res, &dateTime2)
    log.Printf("%v", *dateTime2)
}

执行输出:

=== RUN   TestJson2
2020/07/16 17:39:38 自定义json序列化
2020/07/16 17:39:38 {"id":0,"name":"testUser","createTime":1594892378,"updateTime":1594892378}
2020/07/16 17:39:38 自定义json反序列化
2020/07/16 17:39:38 {0 testUser 2020-07-16 17:39:38 +0800 CST 2020-07-16 17:39:38 +0800 CST}
--- PASS: TestJson2 (0.01s)
PASS

这里有个重点内容,UnmarshalJSON方法里面

reValue := reflect.ValueOf(s)

其他都写的值反射,即s是值,这里s是指针,然后后面value再调用Elem()方法,是为了解决反射修改值的可达性问题,参考这里写的反射第三定律

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350