json时间格式化

Gorm JSON 时间格式化

JSON 解析与扩展已有类型

Go 语言是没有完整的 OOP 对象模型的,在 Golang 的世界里没有继承,只有组合和接口,并且是松散的接口结构,不强制声明实现接口。通过对结构体的组合对现有对象进行扩展也是很便利的,参考 interface & struct 接口与结构体。

单一继承关系解决了 is-a 也就是定义问题,因此可以把子类当做父类来对待。但对于父类不同但又具有某些共同行为的数据,单一继承就不能解决了,C++ 采取了多继承这种复杂的方式。GO 采取的组合方式更贴近现实世界的网状结构,不同于继承,GO 语言的接口是松散的结构,它不和定义绑定。从这一点上来说,Duck Type 相比传统的 extends 是更加松耦合的方式,可以同时从多个维度对数据进行抽象,找出它们的共同点,使用同一套逻辑来处理。

注意 People.Name 成员首字母大写,否则不会导出,解析 JSON 时不会正确赋值。 如果想在一个包中访问另一个包中结构体的字段,则必须是大写字母开头的变量,即可导出的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import (
// "database/sql/driver"
"encoding/json"
"fmt"
"time"
)

type People struct {
Name string `json:"name"`
Time TimeNormal
}

func main() {
js := `{
"name":"Aob"
}`
var p People
err := json.Unmarshal([]byte(js), &p)
if err != nil {
fmt.Println("err: ", err)
return
}
fmt.Println("people: ", p)

p.Time = TimeNormal{time.Now()}
data, err := json.Marshal(p)
if err != nil {
fmt.Println("JSON marshaling failed: %s", err)
}
fmt.Printf("JSON: %s\n", data)

}

// type TimeNormal time.Time // 别名方式扩展
type TimeNormal struct { // 内嵌方式(推荐)
time.Time
}

func (t TimeNormal) MarshalJSON() ([]byte, error) {
// tune := fmt.Sprintf(`"%s"`, t.Format("2006-01-02 15:04:05"))
tune := t.Format(`"2006-01-02 15:04:05"`)
return []byte(tune), nil
}

GO 的 time 包中实现 json.Marshaler 接口的序列化方法 MarshalJSON 指定 RFC3339Nano 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MarshalJSON implements the json.Marshaler interface.
// The time is a quoted string in RFC 3339 format, with sub-second precision added if present.
func (t Time) MarshalJSON() ([]byte, error) {
if y := t.Year(); y < 0 || y >= 10000 {
// RFC 3339 is clear that years are 4 digits exactly.
// See golang.org/issue/4556#c15 for more discussion.
return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
}

b := make([]byte, 0, len(RFC3339Nano)+2)
b = append(b, '"')
b = t.AppendFormat(b, RFC3339Nano)
b = append(b, '"')
return b, nil
}

可以使用格式化函数进行转换,下面是12H、24H两种格式的转换,年份和小时格式代码分别是06、03,使用4位数年份就是 2006,使用24H制就是 15:

1
2
time.Now().Format("06-01-02 03:04:05")
time.Now().Format("2006-01-02 15:04:05")

也可以直接给 Format 函数传入格式类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
time.ANSIC:       Fri Aug  2 23:02:02 2019
time.UnixDate: Fri Aug 2 23:02:02 CST 2019
time.RFC1123: Fri, 02 Aug 2019 23:02:02 CST
time.RFC3339: 2019-08-02T23:02:02+08:00
time.RFC822: 02 Aug 19 23:02 CST
time.RFC850: Friday, 02-Aug-19 23:02:02 CST
time.RFC1123Z: Fri, 02 Aug 2019 23:02:02 +0800
time.RFC3339Nano: 2019-08-02T23:02:02.6227628+08:00
time.RFC822Z: 02 Aug 19 23:02 +0800
time.Kitchen: 11:02PM
time.Stamp: Aug 2 23:02:02
time.StampMicro: Aug 2 23:02:02.629703
time.StampMilli: Aug 2 23:02:02.631
time.StampNano: Aug 2 23:02:02.631646200

Go 不允许在包外新增或重写方法 cannot define new methods on non-local type,只能通过在外部定义别名或者内嵌结构体进行内置对象的扩展。需要注意别名方式只能使用原始类型的字段,不能使用其方法,只重写字段的时候可以考虑使用。

在 gorm 中只重写 MarshalJSON 是不够的,因为 ORM 在插入记录、读取记录时需要的相应执行 Value 和 Scan 方法,需要引入 database/sql/driver 包。为了方便使用,可以定义一个 BaseModel 来替代 gorm.Model。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import "database/sql/driver"

type TimeNormal struct { // 内嵌方式(推荐)
time.Time
}

func (t TimeNormal) MarshalJSON() ([]byte, error) {
// tune := fmt.Sprintf(`"%s"`, t.Format("2006-01-02 15:04:05"))
tune := t.Format(`"2006-01-02 15:04:05"`)
return []byte(tune), nil
}

// Value insert timestamp into mysql need this function.
func (t TimeNormal) Value() (driver.Value, error) {
var zeroTime time.Time
if t.Time.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return t.Time, nil
}

// Scan valueof time.Time
func (t *TimeNormal) Scan(v interface{}) error {
value, ok := v.(time.Time)
if ok {
*t = TimeNormal{Time: value}
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}

type BaseModel struct {
// gorm.Model
ID uint `gorm:"primary_key" json:"id"`
CreatedAt TimeNormal `json:"createdAt"`
UpdatedAt TimeNormal `json:"updatedAt"`
DeletedAt *TimeNormal `sql:"index" json:"-"`
}

下面是别名方式扩展的核心代码示例,注意类型的转,类型断言和返回类型。访问时间对象时,内嵌方式是 t.Time,使用别名方式后时类型转换 time.Time(t),而且 Scan 方法中不能直接通过类型断言 v.(TimeNormal) 将接口转换到 TimeNormal。另外,设置别名后,TimeNormal 并不能直接使用原始类型 time.Time 的各种方法和成员,需要先进行类型转换。显然,通过结构体匿名嵌入的方式并不存在这样的不便,这种方式可以很好的保持对象的原有性质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type TimeNormal time.Time // 别名方式扩展

func (t TimeNormal) MarshalJSON() ([]byte, error) {
ti := time.Time(t)
tune := ti.Format(`"2006-01-02 15:04:05"`)
return []byte(tune), nil
}

// Value insert timestamp into mysql need this function.
func (t TimeNormal) Value() (driver.Value, error) {
var zeroTime time.Time
ti := time.Time(t)
if ti.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return ti, nil
}

// Scan valueof time.Time
func (t *TimeNormal) Scan(v interface{}) error {
ti, ok := v.(time.Time) // NOT directly assertion v.(TimeNormal)
if ok {
*t = TimeNormal(ti)
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}

Xorm JSON 时间格式化

golang默认的time.Time类型在转为json格式时不是常用的2019-05-08 10:00:01这种格式,解决办法是自定义一个时间类型,例如type myTime time.Time ,然后针对myTime实现Marshaler接口的MarshalJSON方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package models

import (
"database/sql/driver"
"time"
)

const localDateTimeFormat string = "2006-01-02 15:04:05"

type LocalTime time.Time

func (l LocalTime) MarshalJSON() ([]byte, error) {
t := time.Time(l)
b := make([]byte, 0, len(localDateTimeFormat)+2)
b = append(b, '"')
b = time.Time(l).AppendFormat(b, localDateTimeFormat)
if !t.Equal(time.Time{}) {
b = t.AppendFormat(b, localDateTimeFormat)
}
b = append(b, '"')
return b, nil
}

func (l *LocalTime) UnmarshalJSON(b []byte) error {
now, err := time.ParseInLocation(`"`+localDateTimeFormat+`"`, string(b), time.Local)
*l = LocalTime(now)
return err
}

上面的代码在网上随手一搜就能找到,没有什么困难的,接下来的才是本篇文章的重点,这玩意结合xorm使用时,特别是字段类型为*LocalTime的时候才需要折腾一番。

下面是我的对应数据库表结构的struct 定义,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type ServerInfo struct {
ServerInfoId string `xorm:"varchar(32) pk server_info_id"`
CreatedAt LocalTime `xorm:"timestamp created"`
UpdatedAt LocalTime `xorm:"timestamp updated"`
DeletedAt *LocalTime `xorm:"timestamp deleted index"`
OrgId string `xorm:"varchar(100) org_id" json:"orgId"`
ServerIp string `xorm:"varchar(128) server_ip" json:"serverIp"`
ServerNameDesc string `xorm:"varchar(500) server_name_desc" json:"serverNameDesc"`
ServerTimeNow LocalTime `xorm:"timestamp server_time" json:"serverTime"`
DataReceiveTime LocalTime `xorm:"timestamp data_receive_time" sql:"DEFAULT:current_timestamp" json:"dataRecvTime"`
LastUploadDataTime *LocalTime `xorm:"timestamp last_upload_data_time" json:"lastUploadDataTime"`
LastCheckTime *LocalTime `xorm:"timestamp last_check_time" json:"lastCheckTime"`
LastErrorTime *LocalTime `xorm:"timestamp last_error_time" json:"lastErrorTime"`
}

注意上面的字段类型,既有LocalTime类型的,又有*LocalTime类型的,*LocalTime是考虑到有时候数据值可能为NULL,即字段值可能为空的情况。

xorm不知道如何为LocalTime这个自定义类型进行赋值或者取值,因此需要实现xorm的core包中的Conversion接口,这个接口的定义如下:

注意,坑已经隐藏在上面的接口定义中了,过一会说。

整个完整的自定义时间类型的代码变成了下面的这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package model

import (
"database/sql/driver"
"time"
)

const localDateTimeFormat string = "2006-01-02 15:04:05"

type LocalTime time.Time

func (l LocalTime) MarshalJSON() ([]byte, error) {
t := time.Time(l)
b := make([]byte, 0, len(localDateTimeFormat)+2)
b = append(b, '"')
if !t.Equal(time.Time{}) {
b = t.AppendFormat(b, localDateTimeFormat)
}
b = append(b, '"')
return b, nil
}

func (l *LocalTime) UnmarshalJSON(b []byte) error {
now, err := time.ParseInLocation(`"`+localDateTimeFormat+`"`, string(b), time.Local)
*l = LocalTime(now)
return err
}

func (l LocalTime) String() string {
return time.Time(l).Format(localDateTimeFormat)
}

func (l LocalTime)Now()(LocalTime){
return LocalTime(time.Now())
}

func (l LocalTime)ParseTime(t time.Time)(LocalTime){
return LocalTime(t)
}

func (j LocalTime) format() string {
return time.Time(j).Format(localDateTimeFormat)
}

func (j LocalTime) MarshalText() ([]byte, error) {
return []byte(j.format()), nil
}

func (l *LocalTime) FromDB(b []byte) error {
if nil == b || len(b) == 0 {
l = nil
return nil
}
var now time.Time
var err error
now, err = time.ParseInLocation(localDateTimeFormat, string(b), time.Local)
if nil == err {
*l = LocalTime(now)
return nil
}
now, err = time.ParseInLocation(time.RFC3339, string(b), time.Local)
if nil == err {
*l = LocalTime(now)
return nil
}
panic("自己定义个layout日期格式处理一下数据库里面的日期型数据解析!")
return err
}

func (t *LocalTime) Scan(v interface{}) error {
// Should be more strictly to check this type.
vt, err := time.Parse(localDateTimeFormat, string(v.([]byte)))
if err != nil {
return err
}
*t = LocalTime(vt)
return nil
}

func (l *LocalTime) ToDB() ([]byte, error) {
if nil == l {
return nil,nil
}
return []byte(time.Time(*l).Format(localDateTimeFormat)), nil
}

func (l *LocalTime) Value() (driver.Value, error) {
if nil==l {
return nil, nil
}
return time.Time(*l).Format(localDateTimeFormat), nil
}

此时,要是数据库的字段内容都有值的话插入和更新应该是没有什么问题,但是*LocalTime字段的值为nil的话问题就开始出现了,上面说了,ToDB()方法的返回值类型为[]byte,当字段值为nil时,返回nil看上去一切正常,但是xorm打印出来的sql语句数据值是下面这个样子的:

1
0x30,0x3a,0x31,0x38,0x3a,0x35},4,5,1,[]uint8(nil),[]uint8(nil)

这个[]uint8(nil)就是*LocalTime值为nil时的情况,数据库驱动是不认可[]uint8(nil)这种数据去写给timestamp类型字段的,问题的根源就是ToDB方法的返回值类型为[]byte,既然是这样,就需要我们人为的把[]uint8(nil)这种类型改为interface(nil)类型,数据库驱动会识别interface(nil)为NULL值,修改代码xorm\statement.go第322行,把原来的val=data改成下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
if structConvert,ok := fieldValue.Interface().(core.Conversion);ok {
data,err := structConvert.ToDB()
if err != nil {
engine.logger.Error(err)
} else {
// val = data
if nil == data {
val=nil // []uint8(nil)改变为interface(nil)
} else {
val=data
}
}
}

就是把val=data改为 if nil==data { val=nil } else {val=data} ,看上去逻辑没有什么变化,但是给val=nil赋值的时候,val的类型就从[]uint8(nil)变成了interface(nil)了,这样数据库驱动就可以正确处理空值了。

除了需要修改xorm\statement.go文件的内容,还需要修改xorm\session_convert.go的第558行,增加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if filedConvert,ok := fieldValue.Interface().(core.Conversion);ok {
data,err := fieldConvert.ToDB()
if err != nil {
return 0,err
}
if col.SQLType.IsBlob() {
return data,nil
}
//fix when pointer type value is null,added by peihexian,2019-05-07
if nil==data {
return nil,nil
}
return string(data),nil
}

主要是增加下面的代码

1
2
3
4
//fix when pointer type value is null,added by peihexian,2019-05-07
if nil==data {
return nil,nil
}

之所以加这个代码是因为xorm作者没有考虑指针类型字段值为nil的情况,xorm对有转换的字段要么当成数字,要么当成了字符串,这两种对于NULL类型的值都不适用,所以需要增加if nil==data return nil,nil这样的代码,还是把数据值组织成interface(nil)去给数据库驱动去处理。

另外还有一个地方,是session_convert.go 第556行,同样需要增加

1
2
3
if nil==data {  //edit by peihexian 2019.06.19
return nil,nil
}

下面是加完以后的样子

到这里,对xorm做了几处小的修改,自定义日期的问题及json格式化问题完美解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
if filedConvert,ok := fieldValue.Interface().(core.Conversion);ok {
data,err := fieldConvert.ToDB()
if err != nil {
return 0,err
}
if col.SQLType.IsBlob() {
return data,nil
}
if nil==data {
return nil,nil
}
return string(data),nil
}