Go 对象扩展与Gorm JSON 时间格式化

JSON 解析与扩展已有类型

Go 语言是没有完整的 OOP 对象模型的,在 Golang 的世界里没有继承,只有组合和接口,并且是松散的接口结构,不强制声明实现接口。通过对结构体的组合对现有对象进行扩展也是很便利的,参考 interface & struct 接口与结构体。
单一继承关系解决了 is-a 也就是定义问题,因此可以把子类当做父类来对待。但对于父类不同但又具有某些共同行为的数据,单一继承就不能解决了,C++ 采取了多继承这种复杂的方式。GO 采取的组合方式更贴近现实世界的网状结构,不同于继承,GO 语言的接口是松散的结构,它不和定义绑定。从这一点上来说,Duck Type 相比传统的 extends 是更加松耦合的方式,可以同时从多个维度对数据进行抽象,找出它们的共同点,使用同一套逻辑来处理。
注意 People.Name 成员首字母大写,否则不会导出,解析 JSON 时不会正确赋值。 如果想在一个包中访问另一个包中结构体的字段,则必须是大写字母开头的变量,即可导出的变量。

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 格式:

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

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

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

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。

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 的各种方法和成员,需要先进行类型转换。显然,通过结构体匿名嵌入的方式并不存在这样的不便,这种方式可以很好的保持对象的原有性质。

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