最近在解析 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
後,都沒有解決問題。最後決定靜下來心來,思考其背後的原理,仔細分析,最終靠自己攻克了這個問題,真是不容易。
這件事也讓我明白了一個道理,授人予魚不如授人予漁,所以我在這裡也把解決問題的思路分享出來,希望對大家也能有一點理解上的提升。
最後一件事
如果本文對您有幫助的話,請點個贊和收藏吧!
您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!