第一种方案
方案研究:
- 采用 protobuf 做数据格式定义
- 语法采用 proto3
- 需要修改 protoc-gen-go 代码
- 新增生成 Setter 接口
- Field 改为下划线加小写字母开头的私有字段
- 参考 jspiro/protobuf@348d24f
- 在 Setter 接口添加数据写脏逻辑
假设数据格式定义如下:
syntax = "proto3";
package example;
option go_package = "github.com/hanxi/godata/example";
message PhoneNumber {
string number = 1;
}
生成代码如下:
package example
type Observer interface {
OnDirty(interface{})
}
type PhoneNumber struct {
_number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"`
observer Observer
}
func (x *PhoneNumber) GetNumber() string {
if x != nil {
return x._number
}
return ""
}
func (x *PhoneNumber) SetNumber(_number string) {
x._number = _number
x.NotifyDirty()
}
func (x *PhoneNumber) NotifyDirty() {
if observer != nil {
observer.OnDirty(x)
}
}
func (x *PhoneNumber) Attach(o Observer) {
x.observer = o
}
实现一个观察者
package godata
type Customer struct {}
func (c *Customer) OnDirty(i interface {}) {
fmt.Println("OnDirty", i)
}
测试代码
package main
func main() {
pn := &pb.PhoneNumber{}
observer := &Customer{}
pn.Attach(observer)
pn.SetNumber("123")
}
关于写脏后的处理逻辑,只对 root 节点处理,这样就需要每个子节点存放 root 节点的信息。对于子节点,预计导出接口如下:
type PhoneNumber struct {
_my *User `protobuf:"bytes,3,opt,name=my,proto3" json:"my,omitempty"`
_root interface{}
}
// 初始化自己的时候让 root 为自己
func NewPhoneNumber() *PhoneNumber {
this := &PhoneNumber {}
this._root = this
return this
}
func (x *PhoneNumber) GetMy() *User {
if x != nil {
return x._my
}
return nil
}
func (x *PhoneNumber) SetMy(user *User) {
if x != nil {
x._my = user
// user 的 root 为 x 的 root
user._root = x._root
x.NotifyDirty()
}
}
func (x *PhoneNumber) NotifyDirty() {
if observer != nil {
observer.OnDirty(x)
}
if x._root != nil {
x._root.NotifyDirty()
}
}
type User struct {
_name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
_age uint32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
_root interface{}
}
func NewUser() *User {
this := &User {}
this._root = this
return this
}
func (x *User) NotifyDirty() {
if observer != nil {
observer.OnDirty(x)
}
if x._root != nil {
x._root.NotifyDirty()
}
}
这样一直往 root 传递 dirty 。只需要在 root 节点 attach 即可。
接下来说说如何处理 map 类型,对 map 类型进行封装
type DataObject interface {
NotifyDirty()
Attach()
}
type PhoneNumber struct {
_number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"`
_users map[uint32]string `protobuf:"bytes,2,rep,name=users,proto3" json:"users,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
_wrap_users *WrapMapUsers
}
type WrapMapUsers struct {
_parent *PhoneNumber
_root DataObject
}
func (w *WrapMapUsers) Set(key uint32, value string) {
w._parent._users[key] = value
w.NotifyDirty()
}
func (w *WrapMapUsers) Delete(key uint32) {
delete(w._parent._users, key)
w.NotifyDirty()
}
func (w *WrapMapUsers) Get(key uint32) sting {
return w._parent._users[key]
}
func (x *PhoneNumber) GetUsers() *WrapMapUsers {
if x != nil {
return x._wrap_users
}
return nil
}
func (x *PhoneNumber) SetUsers(v *WrapMapUsers) {
if x != nil {
x._wrap_users = v
// v 的 root 为 x 的 root
v._root = x._root
x.NotifyDirty()
}
}
func NewWrapMapUsers(x *PhoneNumber) *WrapMapUsers {
this := &WrapMapUsers {}
this._root = this
this._parent = x
return this
}
处理 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
首次写脏操作:
- a.b.x = p1
- a.b.y = p2
- a.c.z = p3
再次写脏操作:
- a.b.x.t = p4 (不更新脏节点,因为a.b.x 已经脏了)
- a.c = p5(更新脏节点,把 z 节点标记为不脏了)
example 基本逻辑已完成 https://github.com/hanxi/godata/blob/main/example_test.go
接下来先写一个定时落地数据库的逻辑,选 MongoDB 。
数据结构:
// user
{
uid: 1,
account: "xxx",
baseinfo: {
name: "hanxi",
age: 18
},
prop:{
[proptype]: {
[propvalue]: size,
}
},
wanfa1:{
updatetime: 0
}
}
另一种方案
golang 的 ast 库可以很好的解析代码和生成代码,考虑使用 ast 来生成代码。
定义输入的数据结构:
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
}
wrap 部分需要慎重考虑,set 的参数只能是包裹好的数据,get 返回的也只是包裹好的数据,不提供接口获取到包裹里的 slice 和 map 。
func (p *Person) SetFriends(value *WrapPersonFriends) {
if p == nil {
return
}
p._wrap_friends = value
value.root = p.root
p.NotifyDirty()
}
func (p *Person) GetFriends() *WrapPersonFriends {
if p == nil {
return nil
}
return p._wrap_friends
}
可以给两个 New 方法,方便不同的情况初始化:
func NewWrapPersonFriends() *WrapPersonFriends {
p := &WrapPersonFriends{}
p.friends = make([]*Person, 0)
p.self = p
p.root = p
return p
}
func NewWrapPersonFriendsFromSlice(friends []*Person) *WrapPersonFriends {
p := &WrapPersonFriends{}
p.friends = make([]*Person, 0)
p.friends = append(p.friends, friends...)
p.self = p
p.root = p
return p
}
给 wrap slice 加个 append 方法:
func (p *WrapPersonFriends) Append(value *Person) {
if p == nil {
return
}
p.friends = append(p.friends, value)
value.root = p.root
p.NotifyDirty()
}
由于不允许直接获取到 slice ,所以新增一个 foreach 方法用于遍历:
func (p *WrapPersonFriends) Foreach(f func(*Person)) {
if p == nil {
return
}
for _, v := range p.friends {
f(v)
}
}
对于 map 类型,预期是这样的:
func (p *Person) SetPeoples(value *WrapPersonPeoples) {
if p == nil {
return
}
p._wrap_peoples = value
value.root = p.root
p.NotifyDirty()
}
func (p *Person) GetPeoples() *WrapPersonPeoples {
if p == nil {
return nil
}
return p._wrap_peoples
}
func NewWrapPersonPeoples() *WrapPersonPeoples {
p := &WrapPersonPeoples{}
p.peoples = make(map[string]*Person)
p.self = p
p.root = p
return p
}
func NewWrapPersonPeoplesFromMap(peoples map[string]*Person) *WrapPersonPeoples {
p := &WrapPersonPeoples{}
p.peoples = make(map[string]*Person)
for k,v := range peoples {
p.peoples[k] = v
}
p.self = p
p.root = p
return p
}
再提供 Set , Get , Delete, Foreach 接口:
func (p *WrapPersonPeoples) Get(key string) *Person {
if p == nil {
return
}
return p.peoples[key]
}
func (p *WrapPersonPeoples) Set(key string, value *Person) {
if p == nil {
return
}
p.peoples[key] = value
value.root = p.root
p.NotifyDirty()
}
func (p *WrapPersonPeoples) Delete(key string) {
if p == nil {
return
}
delete(p.peoples, key)
p.NotifyDirty()
}
func (p *WrapPersonPeoples) Foreach(f func(string, *Person)) {
if p == nil {
return
}
for k, v := range p.peoples {
f(k, v)
}
}
再设计一下文件目录结构
输入目录:
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
ls dirty_tmpl | while read line; do
name=${line%%.*}
echo dirty_gen -tmpl=dirty_tmpl/$line -out=dirty_out/$name.go
done
就是对每个文件生成对应代码:
dirty_gen -tmpl=dirty_tmpl/user.tmpl -out=dirty_out/user.go
dirty_gen -tmpl=dirty_tmpl/wanfa1.tmpl -out=dirty_out/wanfa1.go
dirty_gen -tmpl=dirty_tmpl/wanfa2.tmpl -out=dirty_out/wanfa2.go
其他地方使用可以这样:
import dirty_out
dirty_out.NewXXX()
进展:目前就差实现 dirty_gen 程序了,初版的 dirty_gen.go 如下:
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"io/ioutil"
"strings"
)
func main() {
var filename string
flag.StringVar(&filename, "filename", "example.go", "The input struct file.")
flag.Parse()
src, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
fmt.Printf("package %s\n\n", f.Name.Name)
for _, decl := range f.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
fmt.Printf("func New%s() *%s {\n\tp := &%s{}\n\tp.self = p\n\tp.root = p\n\treturn p\n}\n\n", typeSpec.Name.Name, typeSpec.Name.Name, typeSpec.Name.Name)
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
continue
}
fieldName := field.Names[0].Name
fieldType := getTypeString(fset, field.Type)
// Generate Get and Set methods for all fields
if fieldIsStarStruct(field) {
fmt.Printf("func (p *%s) Set%s(value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s = value\n\tvalue.root = p.root\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), fieldType, fieldName)
} else {
if fieldIsArrayStarStruct(field) || fieldIsMapStarStruct(field) {
setRoot := "\n\tfor _,v := range value {\n\t\tv.root = p.root\n\t}"
fmt.Printf("func (p *%s) Set%s(value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s = value%s\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), fieldType, fieldName, setRoot)
} else {
fmt.Printf("func (p *%s) Set%s(value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s = value\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), fieldType, fieldName)
}
}
fmt.Printf("func (p *%s) Get%s() %s {\n\tif p == nil {\n\t\treturn %s\n\t}\n\treturn p.%s\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), fieldType, getZeroValue(fieldType), fieldName)
// Generate Append method for slice fields
arrType, ok := field.Type.(*ast.ArrayType)
if ok {
if isStarStruct(arrType.Elt) {
fmt.Printf("func (p *%s) Append%s(value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s = append(p.%s, value)\n\tvalue.root = p.root\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), fieldType[2:], fieldName, fieldName)
} else {
fmt.Printf("func (p *%s) Append%s(value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s = append(p.%s, value)\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), fieldType[2:], fieldName, fieldName)
}
}
// Generate Get and Set methods for map fields
mapType, ok := field.Type.(*ast.MapType)
if ok {
keyType := getTypeString(fset, mapType.Key)
valueType := getTypeString(fset, mapType.Value)
if isStarStruct(mapType.Value) {
fmt.Printf("func (p *%s) Put%s(key %s, value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s[key] = value\n\tvalue.root = p.root\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), keyType, valueType, fieldName)
} else {
fmt.Printf("func (p *%s) Put%s(key %s, value %s) {\n\tif p == nil {\n\t\treturn\n\t}\n\tp.%s[key] = value\n\tp.NotifyDirty()\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), keyType, valueType, fieldName)
}
fmt.Printf("func (p *%s) Lookup%s(key %s) %s {\n\tif p == nil {\n\t\treturn %s\n\t}\n\treturn p.%s[key]\n}\n\n", typeSpec.Name.Name, strings.Title(fieldName), keyType, valueType, getZeroValue(valueType), fieldName)
}
}
}
}
}
func getTypeString(fset *token.FileSet, expr ast.Expr) string {
var buf bytes.Buffer
if err := format.Node(&buf, fset, expr); err != nil {
panic(err)
}
return buf.String()
}
func getZeroValue(fieldType string) string {
switch fieldType {
case "int", "int8", "int16", "int32", "int64":
return "0"
case "uint", "uint8", "uint16", "uint32", "uint64":
return "0"
case "float32", "float64":
return "0.0"
case "bool":
return "false"
case "string":
return "\"\""
default:
return "nil"
}
}
func fieldIsStarStruct(field *ast.Field) bool {
return isStarStruct(field.Type)
}
func isStarStruct(expr ast.Expr) bool {
starExpr, ok := expr.(*ast.StarExpr)
if ok {
_, ok = starExpr.X.(*ast.Ident)
if ok {
return true
}
}
return false
}
func fieldIsArrayStarStruct(field *ast.Field) bool {
arrType, ok := field.Type.(*ast.ArrayType)
if ok {
if isStarStruct(arrType.Elt) {
return true
}
}
return false
}
func fieldIsMapStarStruct(field *ast.Field) bool {
mapType, ok := field.Type.(*ast.MapType)
if ok {
if isStarStruct(mapType.Value) {
return true
}
}
return false
}
这个版本是没有把 slice 和 map 包裹起来的,后续再根据前面设计的格式重新写一版。
支持JSON序列化
为了支持数据落地,就需要对数据序列化,首先拿 JSON 做尝试,其他的应该都类似。
假设原始定义的数据如下:
package dirty_tmpl
type BaseInfo struct {
Lv uint32 `json:"lv"`
Exp uint32 `json:"exp"`
}
生成的数据如下(剔除了前面脏数据相关的接口):
package dirty_out
import (
"encoding/json"
"github.com/hanxi/dirty-go/dirty_tmpl"
)
type BaseInfo struct {
Base
lv uint32
exp uint32
_origin dirty_tmpl.BaseInfo
}
func (p *BaseInfo) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &p._origin); err != nil {
return err
}
p.lv = p._origin.Lv
p.exp = p._origin.Exp
return nil
}
func (p *BaseInfo) MarshalJSON() ([]byte, error) {
p._origin.Lv = p.lv
p._origin.Exp = p.exp
return json.Marshal(&p._origin)
}
为 BaseInfo
结构定义 UnmarshalJSON
和 MarshalJSON
方法,就可以使用 json 库来序列化和反序列化了。
测试代码如下:
func TestUserJsonMarshal(t *testing.T) {
baseInfo := dirty_out.NewBaseInfo()
baseInfo.SetLv(10)
baseInfo.SetExp(100)
b, err := json.Marshal(baseInfo)
if err != nil {
t.Error("error: ", err)
}
t.Log(string(b))
if string(b) != `{"lv":10,"exp":100}` {
t.Error("json marshal failed.")
}
}
func TestUserJsonUnmarshal(t *testing.T) {
baseInfo := dirty_out.NewBaseInfo()
jsonStr := `{"lv":20,"exp":300}`
err := json.Unmarshal([]byte(jsonStr), &baseInfo)
if err != nil {
t.Error("error:", err)
}
t.Logf("lv:%d, exp:%d\n", baseInfo.GetLv(), baseInfo.GetExp())
if baseInfo.GetLv() != 20 || baseInfo.GetExp() != 300 {
t.Error("json unmarshal failed.")
}
}
输出结果应该是这样的:
=== RUN TestExample
--- PASS: TestExample (0.00s)
=== RUN TestNotifyDirty
--- PASS: TestNotifyDirty (0.00s)
=== RUN TestUserJsonMarshal
example_test.go:155: {"lv":10,"exp":100}
--- PASS: TestUserJsonMarshal (0.00s)
=== RUN TestUserJsonUnmarshal
example_test.go:169: lv:20, exp:300
--- PASS: TestUserJsonUnmarshal (0.00s)
PASS
ok github.com/hanxi/dirty-go 0.005s
今天就设计了格式,生成代码有空再写。