Recently, I encountered a problem when parsing the date data format of Go (datetime type of mysql). After searching many solutions on the Internet, I found it was not feasible, so I tried to solve them myself and published the solution.

The default date format for Go’s own time. time type is RFC3339, which is 2006-01-02t15:04:05z07:00. If we wanted to pass in the date format of YYYY-MM-DD hh: MM :ss as a value of type time. time in Gin’s shouldBindJSON method, this would be analogous to parsing time xx as xx: Cannot parse xx as xx error message. This is because the time. time type by default supports a different date format than the one we passed in, causing parsing errors.

After encountering this problem, I searched the Internet for many solutions, only to find that they all failed. Some parse properly, but fail to write correctly to the database. Some can be written and written normally, but gin’s built-in verification rules, such as binding:” Required “rule, will be invalid and the verification function will be lost.

The customLocalTimetype

The key to solving this problem is to solve the c.scheddbindjson and Gorm.Updates problem, we need to define a new Time type and custom date format parsing (as follows), And specify our struct struct datetime field as our custom type (as follows)

  • The customLocalTimetype
// model.LocalTime
package model

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

type LocalTime time.Time
Copy the code
  • Business code structure
// 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"`
}
Copy the code

Parse JSON data –UnmarshalJSONMarshalJSON

The field.UnmarshalJSON method is called when c.shouldbindjson is used, so we need to set this method first (as follows) :

func (t *LocalTime) UnmarshalJSON(data []byte) (err error) {
  // Empty values are not parsed
	if len(data) == 2 {
		*t = LocalTime(time.Time{})
		return
	}

  // Specify the format for parsing
	now, err := time.Parse(` "`+TimeFormat+` "`.string(data))
	*t = LocalTime(now)
	return
}
Copy the code

With UnmarshalJSON parsing, shouldBindJSON can parse the date format in YYYY-MM-DD hh: MM :ss, which completes parsing time xx as xx: Cannot parse xx as xx.

Now that we have shouldBindJSON, we also need to solve the problem of parsing values in c. json.

func (t LocalTime) MarshalJSON(a) ([]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
}
Copy the code

Database write and write problems –ValueScan

After the implementation of JSON format data parsing value, we will find that our value still cannot be stored in mysql database through GORM. Through packet capture, we can see the difference between normal request and wrong request (see the following figure).

As you can see from Figure 1 (normal), the payment_time field is passed so that updates can be saved normally.

As you can see from Figure 2 (where we are now), our payment_time field was not passed at all, causing the update to fail.

Therefore, this problem belongs to gorM’s field Value problem. Gorm internally writes and checks out values through Value and Scan methods. So from this point of view, we need to implement Value and Scan methods for our type, corresponding to fetch values on write and parse values on check out respectively. (The implementation is as follows)

// called when writing to mysql
func (t LocalTime) Value(a) (driver.Value, error) {
	// 0001-01-01 00:00:00 is null. If there is a null value, the value can be resolved to null
	if t.String() == "0001-01-01 00:00:00" {
		return nil.nil
	}
	return []byte(time.Time(t).Format(TimeFormat)), nil
}

// called when checking out mysql
func (t *LocalTime) Scan(v interface{}) error {
	// The date in mysql is in 2006-01-02 15:04:05 +0800 CST format
	tTime, _ := time.Parse("2006-01-02 15:04:05 +0800 CST", v.(time.Time).String())
	*t = LocalTime(tTime)
	return nil
}

// For fmt.println and subsequent validation scenarios
func (t LocalTime) String(a) string {
	return time.Time(t).Format(TimeFormat)
}
Copy the code

As a result, time data in YYYY-MM-DD hh: MM: SS format can be accessed and parsed.

The complete LocalTime code is as follows:

package model

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

const TimeFormat = "The 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(a) ([]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(a) (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(a) string {
	return time.Time(t).Format(TimeFormat)
}
Copy the code

Resolve validatorbinding:"required"Out of order

After completing the above steps, your Go application can normally access the custom date format format. Binding :”required” does not work. If you pass in an empty string “” date data, it will pass the check and write null to the database!

The problem is that gin’s built-in Validator does not yet have a complete null-value detection mechanism for our Model.localtime, we just need to add this detection mechanism. (The implementation is as follows)

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 is a null value of the time. time type in go
		// If the validator returns Nil, the binding:" Required "rule will not pass
		if timeStr == "0001-01-01 00:00:00" {
			return nil
		}
		return timeStr
  }
	return nil
}

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

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    // Register a custom validation rule of type model.LocalTime
		v.RegisterCustomTypeFunc(ValidateJSONDateType, model.LocalTime{})
  }
}
Copy the code

After adding this custom rule, our verification rule can take effect again, the problem is solved perfectly! (See below)

This problem puzzled me for several days. At first, I wanted to solve it quickly, but I did not solve the problem after looking for many solutions on the Internet and taking copies. Finally, I decided to calm down, think about the principle behind it, analyze it carefully, and finally overcome this problem by myself. It was not easy.

This also let me understand a truth, teach people to fish is better than teach people to fish, so I also here to solve the problem to share ideas, I hope you can also have a little understanding of the improvement.

One last thing

If this article helped you, please like and bookmark it!

Your likes are the greatest encouragement to the author, and can also let more people see this article!

The original address