Go 自定义日期时间格式解析解决方案 - 解决 `parsing time xx as xx` 错误

最近在解析 Go 的日期数据格式时(mysqldatetime 类型)时遇到个问题,在网上搜了不少方案都试了之后发现不可行,因而本身尝试解决后将解决方案发布出来。mysql

Go 自身的 time.Time 类型默认解析的日期格式是 RFC3339 标准,也就是 2006-01-02T15:04:05Z07:00 的格式。若是咱们想要在 GinshouldBindJSON 方法中,传入 YYYY-MM-DD hh:mm:ss 格式的日期格式做为 time.Time 类型的值,就会引起相似于 parsing time xx as xx: cannot parse xx as xx 的报错信息。这是由于 time.Time 类型默认支持的日期格式与咱们传入的格式不一样,致使解析出错。。git

遇到这个问题后,我在网上找了不少方案,发现都失败了。有的能够完成正常解析,可是没法正确写入到数据库。有的能够正常写入和写出,可是会使得 gin 自带的验证规则如 binding:"required" 规则失效,失去校验的功能。github

自定义 LocalTime 类型

解决这个问题的关键就是解决 c.ShouldBindJSONgorm.Updates 的问题,咱们须要定义一个新的 Time 类型和自定义的日期格式解析(以下),并将咱们的 struct 结构体 datetime 字段指定为咱们自定义的类型(以下)sql

  • 自定义 LocalTime 类型
// model.LocalTime
package model

const TimeFormat = "2006-01-02 15:04:05"

type LocalTime time.Time
  • 业务代码结构
// You Application Struct
package order

type OrderTest struct {
    OrderId     int              `json:"order_id"`
    Test        string           `json:"test"`
    PaymentTime *model.LocalTime `json:"payment_time" binding:"required"`
    TestTime    *model.LocalTime `json:"test_time"`
}

解析 JSON 格式数据 - UnmarshalJSONMarshalJSON

c.ShouldBindJSON 时,会调用 field.UnmarshalJSON 方法,因此咱们须要先设置这个方法(以下):数据库

func (t *LocalTime) UnmarshalJSON(data []byte) (err error) {
  // 空值不进行解析
    if len(data) == 2 {
        *t = LocalTime(time.Time{})
        return
    }

  // 指定解析的格式
    now, err := time.Parse(`"`+TimeFormat+`"`, string(data))
    *t = LocalTime(now)
    return
}

UnmarshalJSON 解析后,shouldBindJSON 就能够正常解析 YYYY-MM-DD hh:mm:ss 格式的日期格式了,这样一来就解决了 parsing time xx as xx: cannot parse xx as xx 的问题。json

既然解决了 shouldBindJSON 的问题,咱们还须要解决 c.JSON 时解析值的问题(实现以下)app

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

数据库写入和写出问题 - ValueScan

在实现了 JSON 格式数据的解析取值后,会发现咱们的值依然没法经过 gorm 被存储到 mysql 数据库中,经过抓包咱们能够看看正常的请求和错误的请求的区别(见下图)ui

图

图

上图 1 (正常状况) 能够看出,payment_time 字段被传递,这样就能够正常存入更新。spa

上图 2(咱们如今的状况) 能够看出,咱们的 payment_time 字段根本没有被传递,从而致使更新失败。code

因此这个问题属于 gorm 对字段取值的问题,gorm 内部是经过 ValueScan 这两个方法完成值的写入和检出。那么从这个角度出发,咱们就须要给咱们的类型实现 ValueScan 方法,分别对应写入的时候获取值和检出的时候解析值。(实现以下)

// 写入 mysql 时调用
func (t LocalTime) Value() (driver.Value, error) {
    // 0001-01-01 00:00:00 属于空值,遇到空值解析成 null 便可
    if t.String() == "0001-01-01 00:00:00" {
        return nil, nil
    }
    return []byte(time.Time(t).Format(TimeFormat)), nil
}

// 检出 mysql 时调用
func (t *LocalTime) Scan(v interface{}) error {
    // mysql 内部日期的格式多是 2006-01-02 15:04:05 +0800 CST 格式,因此检出的时候还须要进行一次格式化
    tTime, _ := time.Parse("2006-01-02 15:04:05 +0800 CST", v.(time.Time).String())
    *t = LocalTime(tTime)
    return nil
}

// 用于 fmt.Println 和后续验证场景
func (t LocalTime) String() string {
    return time.Time(t).Format(TimeFormat)
}

如此一来,咱们就能够正常解析存取 YYYY-MM-DD hh:mm:ss 格式的时间数据了(见下图)

图

LocalTime 完整代码以下:

package model

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

const TimeFormat = "2006-01-02 15:04:05"

type LocalTime time.Time

func (t *LocalTime) UnmarshalJSON(data []byte) (err error) {
    if len(data) == 2 {
        *t = LocalTime(time.Time{})
        return
    }

    now, err := time.Parse(`"`+TimeFormat+`"`, string(data))
    *t = LocalTime(now)
    return
}

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

func (t LocalTime) Value() (driver.Value, error) {
    if t.String() == "0001-01-01 00:00:00" {
        return nil, nil
    }
    return []byte(time.Time(t).Format(TimeFormat)), nil
}

func (t *LocalTime) Scan(v interface{}) error {
    tTime, _ := time.Parse("2006-01-02 15:04:05 +0800 CST", v.(time.Time).String())
    *t = LocalTime(tTime)
    return nil
}

func (t LocalTime) String() string {
    return time.Time(t).Format(TimeFormat)
}

解决验证器 binding:"required" 没法正常工做

在完成上述步骤后,你的 go 应用已经能够正常存取自定义的日期格式格式了。可是还有一个问题,那就是 binding:"required" 并不能正常工做了,若是你传入一个空字符串 "" 日期数据,也会经过校验,并在数据库写入 null

这个问题是由于 gin 内置的 validator 对咱们的 model.LocalTime 尚未一个完善的空值检测机制,咱们只须要加上这个检测机制便可。(实现以下)

package app

func ValidateJSONDateType(field reflect.Value) interface{} {
    if field.Type() == reflect.TypeOf(model.LocalTime{}) {
    timeStr := field.Interface().(model.LocalTime).String()
        // 0001-01-01 00:00:00 是 go 中 time.Time 类型的空值
        // 这里返回 Nil 则会被 validator 断定为空值,而没法经过 `binding:"required"` 规则
        if timeStr == "0001-01-01 00:00:00" {
            return nil
        }
        return timeStr
  }
    return nil
}

func Run() {
    router := gin.Default()

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    // 注册 model.LocalTime 类型的自定义校验规则
        v.RegisterCustomTypeFunc(ValidateJSONDateType, model.LocalTime{})
  }
}

加上这条自定义规则后,咱们的校验规则又能够生效了,问题完美解决!(见下图)

图

这个问题困惑了我好几天,一开始想快点解决,在网上找了不少方案拿过来 copy 后,都没有解决问题。最后决定静下来心来,思考其背后的原理,仔细分析,最终靠本身攻克了这个问题,真是不容易。

这件事也让我明白了一个道理,授人予鱼不如授人予渔,因此我在这里也把解决问题的思路分享出来,但愿对你们也能有一点理解上的提高。

最后一件事

若是本文对您有帮助的话,请点个赞和收藏吧!

您的点赞是对做者的最大鼓励,也可让更多人看到本篇文章!

原文地址