用Go语言模板引擎提取数据

最近在开发一个通用消息通知工具,可以从 API 、数据库等数据源获取数据,然后根据数据渲染消息模板,然后通过企微、邮件、短信、电话语音等渠道推送消息。整个工作的设计思想,就是想提供一种低代码、配置化的消息通知解决方案。

简言之,工具对日常消息通知开发场景进行抽象建模,让所有要素都支持配置:

  • 数据源
  • 消息模板
  • 通知渠道
  • 通知对象
  • 发送策略(手动触发或定时任务)

那在开发的过程中,就不可避免要面对数据提取问题。比如,有个系统提需求说他们的变更单需要每天催一次,列表通过 API 给我,结构如下:

1
2
3
4
5
6
7
{
    "success": true,
    "data": [
      {"name": "变更①"},
      {"name": "变更②"}
    ]
}

消息通知工具需要支持灵活的数据提取,将待催办变更列表从 API 返回数据中提取出来。因为我肯定无法为每个对接接口的数据格式都对接一遍。如果另一个系统的字段名是不一样的呢?

1
2
3
4
5
6
7
{
    "Success": true,
    "Data": [
      {"name": "数据维护①"},
      {"name": "数据维护②"}
    ]
}

如果数据源存在很深的嵌套呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "Success": true,
    "Result": {
        "Count": 10,
        "Data": [
            {"name": "数据维护①"},
            {"name": "数据维护②"}
        ]
    }
}

想要提取数据,最好有一套比较灵活的数据访问语法,类似 Go 标准库中的 template 模板那样。考虑到我们没有精力为此开发一套提取引擎,我们决定借助 template 标准库来干这个事。

工具我只负责设计,代码实施由一个徒弟负责,他先想到 JSON 序列:

1
{{ jsonify .Result.Data }}
  • 渲染模板时提供工具函数 jsonify ,将数据转化成 JSON 格式;
  • 对渲染结果做 JSON 反序列化,还原成原来的数据;

这个方案比较绕,但开会时我一时半会也没想到更好的方式,就先这样了。虽然这阵子还有很多其他事情在忙,但这个事一直在我心头,因为它有两个重大缺陷:

  • JSON 序列化和反序列化肯定有不少性能开销,虽然目前基本没什么影响;
  • 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
44
45
46
47
48
49
50
51
52
53
54
package main

import (
    "fmt"
	"io/ioutil"
	"text/template"
)

type DataContainer struct {
	d interface{}
}

func NewDataContainer() *DataContainer {
	return &DataContainer{}
}

func (c *DataContainer) Get() interface{} {
	return c.d
}

func (c *DataContainer) Set(d interface{}) *DataContainer {
	c.d = d
	return c
}

type JsonMap map[string]interface{}

func main() {
	data := JsonMap{
		"Success": true,
		"Data": JsonMap{
			"User": JsonMap{
				"Name":     "fasionchan",
				"Language": "Go",
			},
		},
	}

	container := NewDataContainer()
	funcs := template.FuncMap{
		"setData": container.Set,
	}

	t, err := template.New("demo").Funcs(funcs).Parse("{{ setData .Data.User }}")
	if err != nil {
		panic(err)
	}

	if err := t.Execute(ioutil.Discard, data); err != nil {
		panic(err)
	}

	fmt.Println(container.Get())
}

这个程序设计了一个数据容器 DataContainer ,用来存放提取的数据,它只提供了 GetSet 方法;将 Set 方法传给模板引擎,通过标准模板语法将要提取出来的数据 Set 到数据容器;最后调用 Get 方法就可以拿到提取出来的数据!

我们的目的是提取数据,模板引擎的渲染结果是无关紧要的。因此这个程序直接用 ioutil.Discard ,忽略渲染结果。

借助模板引擎来提取数据,这个思路是不是有种剑走偏锋的意思?哈哈!

虽然目的达到了,但用起来还是比较繁琐。是不是有更完美的方案呢?我没搜到,就先这样罢。

订阅更新,获取更多学习资料,请关注我们的公众号:

【随笔】系列文章首发于公众号【小菜学编程】,敬请关注: