修改go的時間類型time.Time序列化為時間戳——以及更通用的自定義json序列化方式

0. 問題

go的json對Time類型的序列化結(jié)果是2020-07-16T14:49:50.3269159+08:00這種類型伺帘。我們希望改成時間戳集畅。

1. 網(wǎng)上有各種現(xiàn)成的做法

1.1 輔助結(jié)構(gòu)體

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))
}

每個結(jié)構(gòu)體都要寫一個輔助結(jié)構(gòu)體债蓝,碼字量翻倍耗美,如果公司按照代碼行數(shù)算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
}

本質(zhì)上和1.1沒有什么區(qū)別腌乡,就是代碼行數(shù)少了盟劫。
注意一個問題,如果這里不用別名而直接用SelfUser類

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

會造成SelfUser反序列化調(diào)用無限嵌套与纽,最后棧溢出侣签。

1.3 受1.2啟發(fā),縮小修改范圍渣锦,直接創(chuàng)建一個Time的別名類

上面的方法需要在每個結(jié)構(gòu)體里面去做一個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())
}

執(zhí)行輸出:

=== 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)
}

執(zhí)行輸出:

=== 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)
}

這個方法還有一個很大的優(yōu)點就是不影響現(xiàn)有框架例如ORM框架在映射數(shù)據(jù)庫日期類時對日期類的解析皆看。

1.4 直接創(chuàng)建一個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)
}

執(zhí)行輸出:

=== 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)
}

執(zhí)行輸出:

=== 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匿名結(jié)構(gòu)體的特性實現(xiàn)了Time對time.Time的 “偽繼承” (go沒有繼承背零,只是看起來很像)腰吟,這樣 Time是可以調(diào)用time.Time的所有方法的,所以我們看到log.Printf("%v\n", *user2)輸出的是{0 testUser 2020-07-16 15:58:51 +0800 CST},因為Time有String方法毛雇。
缺點是Time不再是time.Time類嫉称,使用ORM框架時無法映射數(shù)據(jù)庫的日期類了,會報錯unsupported Scan, storing driver.Value type time.Time into type *main_test.Time灵疮。

2. 自定義每個結(jié)構(gòu)體的MarshalJSON和UnmarshalJSON方法

一開始腦筋沒轉(zhuǎn)過彎來织阅,想著把需要使用自定義json的參數(shù)所在的結(jié)構(gòu)體重寫一套通用的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))
        // 每種數(shù)據(jù)類型都要針對性處理润樱,暫時就只寫int64、string诉植、Time了
        switch reType.Field(i).Type.String() {
        case "time.Time":
            // 接口對象通過.(a)就轉(zhuǎn)換成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)
}

執(zhí)行輸出:

=== 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

這里有個重點內(nèi)容,UnmarshalJSON方法里面

reValue := reflect.ValueOf(s)

其他都寫的值反射晾腔,即s是值舌稀,這里s是指針,然后后面value再調(diào)用Elem()方法灼擂,是為了解決反射修改值的可達性問題壁查,參考這里寫的反射第三定律

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剔应,一起剝皮案震驚了整個濱河市睡腿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌峻贮,老刑警劉巖席怪,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異纤控,居然都是意外死亡挂捻,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門船万,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刻撒,“玉大人,你說我怎么就攤上這事耿导∩” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵舱呻,是天一觀的道長醋火。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么胎撇? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任介粘,我火速辦了婚禮,結(jié)果婚禮上晚树,老公的妹妹穿的比我還像新娘姻采。我一直安慰自己,他們只是感情好爵憎,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布慨亲。 她就那樣靜靜地躺著,像睡著了一般宝鼓。 火紅的嫁衣襯著肌膚如雪刑棵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天愚铡,我揣著相機與錄音蛉签,去河邊找鬼。 笑死沥寥,一個胖子當著我的面吹牛碍舍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播邑雅,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼片橡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了淮野?” 一聲冷哼從身側(cè)響起捧书,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎骤星,沒想到半個月后经瓷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡洞难,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年舆吮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廊营。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡歪泳,死狀恐怖萝勤,靈堂內(nèi)的尸體忽然破棺而出露筒,到底是詐尸還是另有隱情,我是刑警寧澤敌卓,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布慎式,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏瘪吏。R本人自食惡果不足惜癣防,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掌眠。 院中可真熱鬧蕾盯,春花似錦、人聲如沸蓝丙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渺尘。三九已至挫鸽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸥跟,已是汗流浹背丢郊。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留医咨,地道東北人枫匾。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像腋逆,于是被迫代替她去往敵國和親婿牍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350