自定义类型在做数据库查询和插入操作时,可以通过实现 Scanner / Valuer 接口,使我们在做db的增删查改时更加顺畅。
现实中遇到的问题
在做后台系统时,有些表中的字段是定制化的,例如:
1 2
| type Day time.Time type LocaleTime time.Time
|
为什么需要给这些类型重定义?
我这里的原因是为了在给下游提供JSON接口时输出标准化的值。例如,对于Day 类型,需要输出 “2021-01-11”,对于 LocaleTime 需要输出的是 “2021-01-11 12:41:01”。
对于JSON 的格式化,使用的是如下接口:
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 42 43
| 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 = 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) }
|
解决了JSON 编解码的问题,但是对于DB插入和查询却总是有问题。
如何解决
经过翻看接口文档,其实解决方式和 json.UnmarshalJSON
/ json.MarshalJSON
接口类似。只要实现该类型的Valuer/Scanner接口即可。
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 }
|
接口学习
下面,具体学习下两个接口。
Scanner 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| type Value interface{} type Valuer interface { 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 }
|
在sql的Exec 和 Query 时,需要将入参转换为各db驱动包支持的数据类型。而Valuer 是将值转换为 driver.Value 类型的接口定义。
因此,对于自定义的时间转义,可以转义为一个 time.Time 类型,或者一个字符串类型。
Valuer 接口
1 2 3
| type Scanner interface { Scan(src interface{}) error }
|
在数据查询结果中,需要将查询结果映射为go支持的数据类型。在实现时,首先会把所有的数据都转换为 int64, float64, bool, []byte, string, time.Time, nil 几种类型,然后调用目标类型的Scan方法赋值。实现Scanner 接口时,入参即为这些类型中的一种,仅需把入参转为我们的变量即可。