The Scanner / Valuer interface provides the ability to convert custom types and database types to each other.

Problems encountered in reality

When developing applications, some of the fields in the tables are customized, for example.

1
2
type Day time.Time  // Time in days
type LocaleTime  time.Time // Time of local formatting

Why do I need to redefine these types?

My reason here is to output normalized values when providing the JSON interface downstream. For example, for the Day type, the output needs to be “2021-01-11” and for LocaleTime the output needs to be “2021-01-11 12:41:01”.

For JSON formatting, the following interface is used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const dayFormat = "2006-01-02"

func (t *Day) UnmarshalJSON(data []byte) (err error) {
    now, err := time.ParseInLocation(`"`+dayFormat+`"`, string(data), time.Local)
    *t = Day(now)
    return
}

func (t Day) MarshalJSON() ([]byte, error) {
    b := make([]byte, 0, len(dayFormat)+2)
    b = append(b, '"')
    b = time.Time(t).AppendFormat(b, dayFormat)
    b = append(b, '"')
    return b, nil
}

const localTimeFormat = "2006-01-02 15:04:05"


func (t *LocalTime) UnmarshalJSON(data []byte) (err error) {
    now, err := time.ParseInLocation(`"`+localTimeFormat+`"`, string(data), time.Local)
    *t = LocalTime(now)
    return
}

func (t LocalTime) MarshalJSON() ([]byte, error) {
    b := make([]byte, 0, len(localTimeFormat)+2)
    b = append(b, '"')
    b = append(b, []byte(t.String())...)
    //b = time.Time(t).AppendFormat(b, localTimeFormat)
    b = append(b, '"')
    return b, nil
}

func (t LocalTime) String() string {
    if time.Time(t).IsZero() {
        return "0000-00-00 00:00:00"
    }

    return time.Time(t).Format(localTimeFormat)
}

Solved the problem of JSON coding and decoding, but there is always a problem for DB insert and query.

How to solve it

After looking through the interface documentation, the solution is actually similar to the json.UnmarshalJSON / json.MarshalJSON interface. Just implement the Valuer/Scanner interface of that type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func (t Day) Value() (driver.Value, error) {
    tTime := time.Time(t)
    return tTime.Format("2006/01/02 15:04:05"), nil
}

func (t *Day) Scan(v interface{}) error {
    switch vt := v.(type) {
    case time.Time:
        *t = Day(vt)
    case string:
        tTime, _ := time.Parse("2006/01/02 15:04:05", vt)
        *t = Day(tTime)
    }
    return nil
}

func (t LocalTime) Value() (driver.Value, error) {
    if time.Time(t).IsZero() {
        return "0000-00-00 00:00:00", nil
    }
    return time.Time(t), nil
}

func (t *LocalTime) Scan(v interface{}) error {
    switch vt := v.(type) {
    case time.Time:
        *t = LocalTime(vt)
    case string:
        tTime, _ := time.Parse("2006/01/02 15:04:05", vt)
        *t = LocalTime(tTime)
    default:
        return nil
    }
    return nil
}

Learn the two interfaces

Below, learn the next two interfaces in detail.

Scanner interface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Value interface{}
type Valuer interface {
    // Value returns a driver Value.
    Value() (Value, error)
}

func IsValue(v interface{}) bool {
    if v == nil {
        return true
    }
    switch v.(type) {
    case []byte, bool, float64, int64, string, time.Time:
        return true
    }
    return false
}

In sql’s Exec and Query, you need to convert the input parameters to the data types supported by each db driver package. And Valuer is the interface definition for converting values to driver.Value types.

Therefore, for custom time escaping, it can be escaped to a time.Time type, or a string type.

Valuer Interface

1
2
3
type Scanner interface {
    Scan(src interface{}) error
}

In the data query result, the query result needs to be mapped to the data types supported by go. In the implementation, all the data is first converted to int64, float64, bool, []byte, string, time.Time, nil and then the Scan method of the target type is called to assign the value. When implementing the Scanner interface, the input is one of these types, and we just need to convert the input to our variable.