mysql error parsing_Go 解析日期格式-解决 parsing time xx as xx: cannot parse xx as xx 错误

最近在解析 Go 的日期数据格式时(mysql 的 datetime 类型)时遇到个问题,在网上搜了很多方案都试了以后发现不可行,于是自己尝试解决后将解决方案发布出来。【结尾有彩蛋】

Go 自身的 time.Time 类型默认解析的日期格式是 RFC3339 标准,也就是 2006-01-02T15:04:05Z07:00 的格式。如果我们想要在 Gin 的 shouldBindJSON 方法中,传入 YYYY-MM-DD hh:mm:ss 格式的日期格式作为 time.Time 类型的值,就会引发类似于 parsing time xx as xx: cannot parse xx as xx 的报错信息。这是因为 time.Time 类型默认支持的日期格式与我们传入的格式不同,导致解析出错。。

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

自定义 LocalTime 类型

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

自定义 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 格式数据 - UnmarshalJSON 与 MarshalJSON

在 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 的问题。

既然解决了 shouldBindJSON 的问题,我们还需要解决 c.JSON 时解析值的问题(实现如下)

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

}

数据库写入和写出问题 - Value 与 Scan

在实现了 JSON 格式数据的解析取值后,会发现我们的值依然无法通过 gorm 被存储到 mysql 数据库中,通过抓包我们可以看看正常的请求和错误的请求的区别

从 上图 1 (正常情况) 可以看出,payment_time 字段被传递,这样就可以正常存入更新。

从 上图 2(我们现在的情况) 可以看出,我们的 payment_time 字段根本没有被传递,从而导致更新失败。

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

// 写入 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{})

}

}

加上这条自定义规则后,我们的校验规则又可以生效了,问题完美解决!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值