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

  

加上這條自定義規則后,我們的校驗規則又可以生效了,問題完美解決!(見下圖)

圖

 

這個問題困惑了我好幾天,一開始想快點解決,在網上找了很多方案拿過來 copy 后,都沒有解決問題。最后決定靜下來心來,思考其背后的原理,仔細分析,最終靠自己攻克了這個問題,真是不容易。

這件事也讓我明白了一個道理,授人予魚不如授人予漁,所以我在這里也把解決問題的思路分享出來,希望對大家也能有一點理解上的提升。

最后一件事

轉載:https://blog.csdn.net/JineD/article/details/115673373


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM