hanxi / blog

涵曦的博客
https://blog.hanxi.cc
56 stars 5 forks source link

golang脏数据模块 #90

Open hanxi opened 1 year ago

hanxi commented 1 year ago

第一种方案

方案研究:

假设数据格式定义如下:

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

首次写脏操作:

写脏结果: Pasted image 20230416003949

再次写脏操作:

写脏结果: Pasted image 20230416004042


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 结构定义 UnmarshalJSONMarshalJSON 方法,就可以使用 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

今天就设计了格式,生成代码有空再写。

代码地址: https://github.com/hanxi/dirty-go