搬砖程序员带你飞

Golang Database 中 Scanner/ Valuer 接口学习

字数统计: 817阅读时长: 3 min
2021/01/11 Share

自定义类型在做数据库查询和插入操作时,可以通过实现 Scanner / Valuer 接口,使我们在做db的增删查改时更加顺畅。

现实中遇到的问题

在做后台系统时,有些表中的字段是定制化的,例如:

1
2
type Day time.Time  // 以天为单位标记
type LocaleTime time.Time // 本地格式化的时间, 存在 0000-00-00 00:00:00 值

为什么需要给这些类型重定义?

我这里的原因是为了在给下游提供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"

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

// 格式化JSON 编码
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)
}

解决了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 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
}

在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 接口时,入参即为这些类型中的一种,仅需把入参转为我们的变量即可。

CATALOG
  1. 1. 现实中遇到的问题
  2. 2. 如何解决
  3. 3. 接口学习
    1. 3.1. Scanner 接口
    2. 3.2. Valuer 接口