200 lines
5 KiB
Markdown
200 lines
5 KiB
Markdown
<h2 align="center">Codec for a Typed Map</h2>
|
|
<p align="center">Provides round-trip serialization of typed Go maps.<p>
|
|
<p align="center"><a href="https://godoc.org/github.com/bep/tmc"><img src="https://godoc.org/github.com/bep/tmc?status.svg" /></a>
|
|
<a href="https://goreportcard.com/report/github.com/bep/tmc"><img src="https://goreportcard.com/badge/github.com/bep/tmc" /></a>
|
|
<a href="https://codecov.io/gh/bep/tmc"><img src="https://codecov.io/gh/bep/tmc/branch/master/graph/badge.svg" /></a>
|
|
<a href="https://github.com/bep/tmc/actions"><img src="https://action-badges.now.sh/bep/tmc?workflow=test" /></a></p>
|
|
|
|
|
|
### How to Use
|
|
|
|
See the [GoDoc](https://godoc.org/github.com/bep/tmc) for some basic examples and how to configure custom codec, adapters etc.
|
|
|
|
### Why?
|
|
|
|
Text based serialization formats like JSON and YAML are convenient, but when used with Go maps, most type information gets lost in translation.
|
|
|
|
Listed below is a round-trip example in JSON (see https://play.golang.org/p/zxt-wi4Ljz3 for a runnable version):
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/kr/pretty"
|
|
)
|
|
|
|
func main() {
|
|
mi := map[string]interface{}{
|
|
"vstring": "Hello",
|
|
"vint": 32,
|
|
"vrat": big.NewRat(1, 2),
|
|
"vtime": time.Now(),
|
|
"vduration": 3 * time.Second,
|
|
"vsliceint": []int{1, 3, 4},
|
|
"nested": map[string]interface{}{
|
|
"vint": 55,
|
|
"vduration": 5 * time.Second,
|
|
},
|
|
"nested-typed-int": map[string]int{
|
|
"vint": 42,
|
|
},
|
|
"nested-typed-duration": map[string]time.Duration{
|
|
"v1": 5 * time.Second,
|
|
"v2": 10 * time.Second,
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(mi)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
m := make(map[string]interface{})
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
pretty.Print(m)
|
|
|
|
}
|
|
```
|
|
|
|
This prints:
|
|
|
|
```go
|
|
map[string]interface {}{
|
|
"vint": float64(32),
|
|
"vrat": "1/2",
|
|
"vtime": "2009-11-10T23:00:00Z",
|
|
"vduration": float64(3e+09),
|
|
"vsliceint": []interface {}{
|
|
float64(1),
|
|
float64(3),
|
|
float64(4),
|
|
},
|
|
"vstring": "Hello",
|
|
"nested": map[string]interface {}{
|
|
"vduration": float64(5e+09),
|
|
"vint": float64(55),
|
|
},
|
|
"nested-typed-duration": map[string]interface {}{
|
|
"v2": float64(1e+10),
|
|
"v1": float64(5e+09),
|
|
},
|
|
"nested-typed-int": map[string]interface {}{
|
|
"vint": float64(42),
|
|
},
|
|
}
|
|
```
|
|
|
|
And that is very different from the origin:
|
|
|
|
* All numbers are now `float64`
|
|
* `time.Duration` is also `float64`
|
|
* `time.Now` and `*big.Rat` are strings
|
|
* Slices are `[]interface {}`, maps `map[string]interface {}`
|
|
|
|
So, for structs, you can work around some of the limitations above with custom `MarshalJSON`, `UnmarshalJSON`, `MarshalText` and `UnmarshalText`.
|
|
|
|
For the commonly used flexible and schema-less`map[string]interface {}` this is, as I'm aware of, not an option.
|
|
|
|
Using this library, the above can be written to (see https://play.golang.org/p/PlDetQP5aWd for a runnable example):
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"log"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/bep/tmc"
|
|
|
|
"github.com/kr/pretty"
|
|
)
|
|
|
|
func main() {
|
|
mi := map[string]interface{}{
|
|
"vstring": "Hello",
|
|
"vint": 32,
|
|
"vrat": big.NewRat(1, 2),
|
|
"vtime": time.Now(),
|
|
"vduration": 3 * time.Second,
|
|
"vsliceint": []int{1, 3, 4},
|
|
"nested": map[string]interface{}{
|
|
"vint": 55,
|
|
"vduration": 5 * time.Second,
|
|
},
|
|
"nested-typed-int": map[string]int{
|
|
"vint": 42,
|
|
},
|
|
"nested-typed-duration": map[string]time.Duration{
|
|
"v1": 5 * time.Second,
|
|
"v2": 10 * time.Second,
|
|
},
|
|
}
|
|
|
|
c, err := tmc.New()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
data, err := c.Marshal(mi)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
m := make(map[string]interface{})
|
|
if err := c.Unmarshal(data, &m); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
pretty.Print(m)
|
|
|
|
}
|
|
```
|
|
|
|
This prints:
|
|
|
|
```go
|
|
map[string]interface {}{
|
|
"vduration": time.Duration(3000000000),
|
|
"vint": int(32),
|
|
"nested-typed-int": map[string]int{"vint":42},
|
|
"vsliceint": []int{1, 3, 4},
|
|
"vstring": "Hello",
|
|
"vtime": time.Time{
|
|
wall: 0x0,
|
|
ext: 63393490800,
|
|
loc: (*time.Location)(nil),
|
|
},
|
|
"nested": map[string]interface {}{
|
|
"vduration": time.Duration(5000000000),
|
|
"vint": int(55),
|
|
},
|
|
"nested-typed-duration": map[string]time.Duration{"v1":5000000000, "v2":10000000000},
|
|
"vrat": &big.Rat{
|
|
a: big.Int{
|
|
neg: false,
|
|
abs: {0x1},
|
|
},
|
|
b: big.Int{
|
|
neg: false,
|
|
abs: {0x2},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
|
|
### Performance
|
|
|
|
The implementation is easy to reason aobut (it uses reflection), but It's not particulary fast and probably not suited for _big data_. A simple benchmark with a roundtrip marshal/unmarshal is included. On my MacBook it shows:
|
|
|
|
```bash
|
|
BenchmarkCodec/JSON_regular-4 50000 27523 ns/op 6742 B/op 171 allocs/op
|
|
BenchmarkCodec/JSON_typed-4 20000 66644 ns/op 16234 B/op 411 allocs/op
|
|
```
|