最近在解析 Go
的日期數據格式時(mysql
的 datetime
類型)時遇到個問題,在網上搜了不少方案都試了之後發現不可行,因而本身嘗試解決後將解決方案發布出來。mysql
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
類型默認支持的日期格式與咱們傳入的格式不一樣,致使解析出錯。。git
遇到這個問題後,我在網上找了不少方案,發現都失敗了。有的能夠完成正常解析,可是沒法正確寫入到數據庫。有的能夠正常寫入和寫出,可是會使得 gin
自帶的驗證規則如 binding:"required"
規則失效,失去校驗的功能。github
LocalTime
類型解決這個問題的關鍵就是解決 c.ShouldBindJSON
和 gorm.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"` }
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
的問題。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 }
Value
與 Scan
在實現了 JSON
格式數據的解析取值後,會發現咱們的值依然沒法經過 gorm
被存儲到 mysql
數據庫中,經過抓包咱們能夠看看正常的請求和錯誤的請求的區別(見下圖)ui
從 上圖 1 (正常狀況)
能夠看出,payment_time
字段被傳遞,這樣就能夠正常存入更新。spa
從 上圖 2(咱們如今的狀況)
能夠看出,咱們的 payment_time
字段根本沒有被傳遞,從而致使更新失敗。code
因此這個問題屬於 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
後,都沒有解決問題。最後決定靜下來心來,思考其背後的原理,仔細分析,最終靠本身攻克了這個問題,真是不容易。
這件事也讓我明白了一個道理,授人予魚不如授人予漁,因此我在這裏也把解決問題的思路分享出來,但願對你們也能有一點理解上的提高。
若是本文對您有幫助的話,請點個贊和收藏吧!
您的點贊是對做者的最大鼓勵,也可讓更多人看到本篇文章!