Categorygithub.com/altstory/go-data
modulepackage
1.1.3
Repository: https://github.com/altstory/go-data.git
Documentation: pkg.go.dev

# README

go-data:可方便稳定序列化的树状数据结构

go-data 旨在封装所有类似 JSON 结构数据的处理方法,可以用于处理数据或配置文件(JSON、TOML、YAML 等)。 其核心数据结构 Datadata.RawData 的包装,但不同于简单的 map,通过 MakeEncoder 获得的 Data 可以保证里面不包含任何无法序列化的结构,例如 funcchan 等,也不会包含任何指针、interface 等。通过这样的加工,可以保证 Data 使用任何序列化和反序列化工具,比如 json.Marshaljson.Unmarshal,得到稳定输出(除非遇到了工具的 bug,比如 JSON 无法表达超过 2^53 的整数)。

使用方法

构造 Data

MakeEncoder 可以将任意结构转换成 Data。需要注意,由于 Data 本质上是一个 map,所以只有 struct、结构的指针、map[string]T 可以成功转换成 Data

type T struct {
    Foo     int    `sample:"foo"`
    Bar     string `sample:"bar"`
    Empty   uint   `sample:"empty,omitempty"` // 设置 omitempty 后,如果 Empty 未设置值则不会被放入 Data
    Skipped bool   `sample:"-"`               // 名字为 -,则代表这个字段会被忽略
    *Embedded      `sample:",squash"`         // 设置 squash,这个结构会被展开(inline)到上层结构中去
}

type Embedded struct {
    Player bool `sample:"player"`
}

func main() {
    t := &T{
        Foo: 123,
        Bar: "player",
        Embedded: &Embedded{
            Player: true,
        },
    }
    enc := data.Encoder{
        TagName: "sample", // 自定义 field tag
    }
    d := enc.Encode(t)
    fmt.Println(d)

    // Output:
    // {
    //     "foo": 123,
    //     "bar": "player",
    //     "player": true,
    // }
}

读取数据

Data 提供一些方法来方便的读取里面的数据。

func main() {
    d := data.Make(data.RawData{
        "foo": data.RawData{
            "bar": 123,
        },
    })

    fmt.Println(d.Get("foo", "bar")) // 输出:123
    fmt.Println(d.Query("foo.bar"))  // 输出:123
}

解析数据

通过使用 Decoder 可以将 Data 解析到任意 Go 结构里面去。

当前支持以下类型的解析:

  • 布尔:bool
  • 所有的整型:int/int8/.../int64/uint/uint8/.../uint64
  • 所有的浮点:float32/float64
  • 所有的复数:complex64/complex128
  • 字符串:string
  • 各种 Go 内置类型:map/slice/array/struct
  • 时间类型:time.Time/time.Duration

其中,time.Duration 的源数据需要是符合 time.ParseDuration 规则的字符串,比如 "2m30s"

type T struct {
    Foo int           `sample:"foo"`
    Bar string        `sample:"bar"`
    Dur time.Duration `sample:"dur"`
}

func main() {
    d := data.Make(data.RawData{
        "foo": 123,
        "bar": "player",
        "dur": "2m30s",
    })
    dec := data.Decoder{
        TagName: "sample", // 自定义 field tag
    }

    var t T
    enc.Decode(d, &t)
    fmt.Println(t.Foo, t.Bar, t.Dur)
    fmt.Println(t.Dur == 2*time.Minute+30*time.Second)

    var foo int
    enc.DecodeQuery(d, "foo", &foo)
    fmt.Println("foo:", foo)

    // 输出:
    // 123  player  2m30s
    // true
    // foo:    123
}

序列化和反序列化

为了方便将 Data 进行持久化存储,特别提供了专用的格式来进行序列化和反序列化。

如果要将 Data 序列化,可以直接调用 Data#String 方法。如果要反序列化,则使用 Parse 函数。

func main() {
    d := data.Make(data.RawData{
        "foo": 123,
        "bar": "player",
    })
    str := d.String()
    fmt.Println(str)

    parsed, err := data.Parse(str)            // 输出:<json>{"bar":"player","foo":123}
    fmt.Println(err)                          // 输出:nil
    fmt.Println(reflect.DeepEqual(parsed), d) // 输出:true
}

通过 Patch 进行增量更新

由于 Data 底层数据结构相对复杂,手动更新数据会出现很多问题,比如难以跟踪变化,在持久化存储时会出现难以追查的并发冲突问题。 为了解决这个,推荐所有对 Data 的变更都采用 Patch 来实现。

func main() {
    patch := data.NewPatch()

    // 删除 d["v2"]、d["v3"][1]、d["v4"]["v4-4"]。
    patch.Add([]string{"v2", "v3.1", "v4.v4-1"}, nil)

    // 添加数据。
    patch.Add(nil, map[string]Data{
        // 在根添加数据。
        "": data.Make(data.RawData{
            "v1": []int{2, 3},
            "v2": 456,
        }),

        // 在 d["v4"] 里添加数据。
        "v4": data.Make(data.RawData{
            "v4-1": "new",
        }),
    })

    // 同时删除并添加数据。
    patch.Add([]string{"v4.v4-2"}, map[string]Data{
        "v4": data.Make(data.RawData{
            "v4-2": data.RawData{
                "new": true,
            },
        }),
    })

    d := data.Make(data.RawData{
        "v1": []int{1},
        "v2": 123,
        "v3": []string{"first", "second", "third"},
        "v4": data.RawData{
            "v4-1": "old",
            "v4-2": data.RawData{
                "old": true,
            },
        },
    })
    patch.ApplyTo(&d)

    fmt.Println(d)

    // Output:
    // <json>{"v1":[1,2,3],"v2":456,"v3":["first","third"],"v4":{"v4-1":"new","v4-2":{"new":true}}}
}

工作原理

将数据编码成 Data 或者将 Data 数据提取到任意 Go 结构,这个的工作原理与 json.Marshaljson.Unmarshal 类似,可以查阅相关文章了解实现原理,这里不赘述。

为了保证 Data 序列化/反序列化结果能够稳定,这个库做了几件重要的事情:

  • 将所有结构、指针、map、interface 等变成标准的 Data 类型;
  • 将所有具有宽度的类型,比如 int/int8/int16/int32 等,都转化成最大尺寸的类型,比如 int64,这样保证同样的数据通过 MakeEncoder 得到 Data 时能够稳定;
  • 将所有 slice、array 也标准化成 slice,并且如果 slice 的元素类型是 int/int8/int16/int32 等,也都转化成最大尺寸的类型,保证 slice 类型也能稳定;
  • 消除所有类型别名,比如如果有一个类型是 type MyInt int,则会变成普通的 int64

# Functions

Make 将一个普通 m 转化成 Data。 虽然 m 和 Data 类型一样,但是要成为 Data 必须得过滤掉不合法的数据, 也需要将 struct 转化成 Data 格式。 Make 适合用于将 JSON、YAML 等反序列化工具获得的 map[string]interface{} 数据转化成 Data, 假设需要将任意数据转化成 Data,应该使用 Encoder。.
Merge 将多个 data 从左至右合并到 d 里面,如果有同名的 key 会进行深度遍历进行合并。 返回的 d 是一个全新的 Data,修改 d 的内容不会影响参数中任何的 data。 具体的合并规则是: - 对于 Data 类型的数值,将相同的 key 进行深度合并; - 如果出现同名 key 且 value 类型相同,都是 Data 或者 slice,深度合并 value 值; - 如果出现同名 key 且 value 类型不同,后面出现的 value 覆盖前面的 value。 - 对于 slice 类型的数值,如果两个 slice 类型相同,后面出现的 slice 的值会被 append 进去。.
MergeTo 将多个 data 从左至右合并到 target 里面,如果有同名的 key 会进行深度遍历进行合并。 如果 target 为 nil,则直接返回,不做任何操作。 具体的合并规则是参考 `Merge` 的文档。.
NewPatch 创建一个新 Patch 对象。.
Parse 从 str 中解析 Data,这个 str 应该是符合 Data 序列化格式的字符串。 如果 str 格式不合法,返回错误。 Data 序列化格式定义如下: '<' type '>' raw 当前 type 仅支持 JSON,值为 `json`,对应的 raw 是 JSON 字符串。 例如: <json>{"hello":"world!"}.
ParseFieldTag 解析 field tag 的 alias 和选项。 详细的格式见 FieldTag 文档。.
ParseJSON 解析 JSON 字符串并且生成 Data,如果解析过程出现任何错误则返回错误。 由于 Data 是一个 map,所以 JSON 必须是一个 object,如果不是则返回错误。.

# Structs

Data 是一种通用的数据存储结构,可以用于表达各种以 JSON、TOML、YAML 等为介质的数据, 也可以用于序列化/反序列化 Go struct。 Data 里面的数据不允许随意修改,只能通过 `MergeTo`、`Patch#ApplyTo` 等方法修改。.
Decoder 用来将 Data 设置到指定值里面去。.
Encoder 用来将数据转化成 Data。.
FieldTag 是一个解析完成的字段 tag。 格式是: data:"alias,opt1,opt2,..." 当前支持以下选项: - omitempty:忽略空值 - squash:将一个字段的内容展开到当前 struct 当 alias 为“-”时,当前字段会被跳过。.
Patch 代表一系列的对 Data 的修改操作。.
PatchAction 代表一个 patch 操作。.

# Type aliases

RawData 是一种未经加工的 map[string]interface{}。.