分享
  1. 首页
  2. 文章

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

猫仙草 · · 2973 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

0. 问题

go的json对Time类型的序列化结果是2020年07月16日T14: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()方法,是为了解决反射修改值的可达性问题,参考这里写的反射第三定律


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2973 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏