type Person struct {
name string
age int
friends []*Person
peoples map[string]*Person
}
type User struct {
baseInfo *Person
score uint32
}
为了防止 slice 和 map 在外部直接使用,需要将其包裹起来,期望生成的代码是下面的样子:
type Person struct {
Base
name string
age int
_wrap_friends *WrapPersonFriends
_wrap_peoples *WrapPersonPeoples
}
type WrapPersonFriends struct {
Base
friends []*Person
}
type WrapPersonPeoples struct {
Base
peoples map[string]*Person
}
type User struct {
Base
baseInfo *Person
score uint32
}
Base 为固定的代码:
type Observer interface {
OnDirty(interface{})
}
type DataObject interface {
NotifyDirty()
Attach(o Observer)
}
type Base struct {
DataObject
observer Observer
root DataObject
self DataObject
}
func (x *Base) NotifyDirty() {
if x.observer != nil {
x.observer.OnDirty(x)
}
if x.root != nil && x.root != x.self {
// 非根节点往上传递消息
x.root.NotifyDirty()
}
}
func (x *Base) Attach(o Observer) {
x.observer = o
}
期望非 wrap 部分生成的代码如下:
func NewPerson() *Person {
p := &Person{}
p.self = p
p.root = p
return p
}
func (p *Person) SetName(value string) {
if p == nil {
return
}
p.name = value
p.NotifyDirty()
}
func (p *Person) GetName() string {
if p == nil {
return ""
}
return p.name
}
func (p *Person) SetAge(value int) {
if p == nil {
return
}
p.age = value
p.NotifyDirty()
}
func (p *Person) GetAge() int {
if p == nil {
return 0
}
return p.age
}
func NewUser() *User {
p := &User{}
p.self = p
p.root = p
return p
}
func (p *User) SetBaseInfo(value *Person) {
if p == nil {
return
}
p.baseInfo = value
value.root = p.root
p.NotifyDirty()
}
func (p *User) GetBaseInfo() *Person {
if p == nil {
return nil
}
return p.baseInfo
}
func (p *User) SetScore(value uint32) {
if p == nil {
return
}
p.score = value
p.NotifyDirty()
}
func (p *User) GetScore() uint32 {
if p == nil {
return 0
}
return p.score
}
第一种方案
方案研究:
假设数据格式定义如下:
生成代码如下:
实现一个观察者
测试代码
关于写脏后的处理逻辑,只对 root 节点处理,这样就需要每个子节点存放 root 节点的信息。对于子节点,预计导出接口如下:
这样一直往 root 传递 dirty 。只需要在 root 节点 attach 即可。
接下来说说如何处理 map 类型,对 map 类型进行封装
处理 map 的难点在于如何修改 protoc-gen-go ,使其可以生成 wrap map 结构,数据还是存储在当前节点, wrap 结构里没有数据,这样可以做到不修改 protobuf 的数据打包和解包。
map 的 key 只需要支持 int 和 sting 这些基础数据即可,value 可以是自定义的结构,跟前面的
_my *User
处理方式类似。slice 结构就先不考虑了,处理方式和 map 差不多。
今天先研究到这里,下次先把上面的代码手写让其可以正常运行,需要写完 dirty 后所作的事情,比如 dirty 后把数据写入 redis 或者 mysql 或者 mongodb(如果是要实现 mongodb 的部分 set,需要修改数据结构,每个节点得有 parent,一直追溯到 root 拼接成 set 的 key ),然后再想办法写代码生成。
脏树: dirty tree
首次写脏操作:
写脏结果:
再次写脏操作:
写脏结果:
example 基本逻辑已完成 https://github.com/hanxi/godata/blob/main/example_test.go
接下来先写一个定时落地数据库的逻辑,选 MongoDB 。
数据结构:
另一种方案
golang 的 ast 库可以很好的解析代码和生成代码,考虑使用 ast 来生成代码。
定义输入的数据结构:
为了防止 slice 和 map 在外部直接使用,需要将其包裹起来,期望生成的代码是下面的样子:
Base 为固定的代码:
期望非 wrap 部分生成的代码如下:
wrap 部分需要慎重考虑,set 的参数只能是包裹好的数据,get 返回的也只是包裹好的数据,不提供接口获取到包裹里的 slice 和 map 。
可以给两个 New 方法,方便不同的情况初始化:
给 wrap slice 加个 append 方法:
由于不允许直接获取到 slice ,所以新增一个 foreach 方法用于遍历:
对于 map 类型,预期是这样的:
再提供 Set , Get , Delete, Foreach 接口:
再设计一下文件目录结构
输入目录: dirty_tmpl/user.tmpl dirty_tmpl/wanfa1.tmpl dirty_tmpl/wanfa2.tmpl
输出目录: 包名:dirty_out dirty_out/base.go # 固定文件 dirty_out/user.go dirty_out/wanfa1.go
工具使用 bash 脚本调用
gen_dirty.sh
就是对每个文件生成对应代码:
其他地方使用可以这样:
进展:目前就差实现 dirty_gen 程序了,初版的 dirty_gen.go 如下:
这个版本是没有把 slice 和 map 包裹起来的,后续再根据前面设计的格式重新写一版。
支持JSON序列化
为了支持数据落地,就需要对数据序列化,首先拿 JSON 做尝试,其他的应该都类似。
假设原始定义的数据如下:
生成的数据如下(剔除了前面脏数据相关的接口):
为
BaseInfo
结构定义UnmarshalJSON
和MarshalJSON
方法,就可以使用 json 库来序列化和反序列化了。测试代码如下:
输出结果应该是这样的:
今天就设计了格式,生成代码有空再写。
代码地址: https://github.com/hanxi/dirty-go