go-data
旨在封装所有类似 JSON 结构数据的处理方法,可以用于处理数据或配置文件(JSON、TOML、YAML 等)。
其核心数据结构 Data
是 data.RawData
的包装,但不同于简单的 map,通过 Make
或 Encoder
获得的 Data
可以保证里面不包含任何无法序列化的结构,例如 func
、chan
等,也不会包含任何指针、interface 等。通过这样的加工,可以保证 Data
使用任何序列化和反序列化工具,比如 json.Marshal
和 json.Unmarshal
,得到稳定输出(除非遇到了工具的 bug,比如 JSON 无法表达超过 2^53
的整数)。
Make
或 Encoder
可以将任意结构转换成 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
}
由于 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.Marshal
和 json.Unmarshal
类似,可以查阅相关文章了解实现原理,这里不赘述。
为了保证 Data
序列化/反序列化结果能够稳定,这个库做了几件重要的事情:
- 将所有结构、指针、map、interface 等变成标准的
Data
类型; - 将所有具有宽度的类型,比如
int
/int8
/int16
/int32
等,都转化成最大尺寸的类型,比如int64
,这样保证同样的数据通过Make
或Encoder
得到Data
时能够稳定; - 将所有 slice、array 也标准化成 slice,并且如果 slice 的元素类型是
int
/int8
/int16
/int32
等,也都转化成最大尺寸的类型,保证 slice 类型也能稳定; - 消除所有类型别名,比如如果有一个类型是
type MyInt int
,则会变成普通的int64
。