golang脏数据模块

第一种方案

方案研究:

假设数据格式定义如下:

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

点击进入评论 ...