Last active
May 23, 2025 01:38
-
-
Save liweitianux/11234821d277d7aae668425528549c52 to your computer and use it in GitHub Desktop.
JSON-based marshaling with custom "oplog" tag
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"fmt" | |
"reflect" | |
"time" | |
"strings" | |
) | |
type oplogMarshaler interface { | |
MarshalOpLog() ([]byte, error) | |
} | |
type jsonMarshaler interface { | |
MarshalJSON() ([]byte, error) | |
} | |
func Marshal(v any) ([]byte, error) { | |
if w, ok := v.(oplogMarshaler); ok { | |
return w.MarshalOpLog() | |
} | |
if v == nil { | |
return []byte("null"), nil | |
} | |
buf := &bytes.Buffer{} | |
err := marshal(buf, reflect.ValueOf(v)) | |
if err != nil { | |
return nil, err | |
} | |
return buf.Bytes(), nil | |
} | |
func marshal(buf *bytes.Buffer, v reflect.Value) error { | |
switch v.Kind() { | |
case reflect.Pointer, reflect.Interface: | |
if v.IsNil() { | |
buf.WriteString("null") | |
return nil | |
} | |
return marshal(buf, v.Elem()) | |
case reflect.Array, reflect.Slice: | |
buf.WriteByte('[') | |
for i := 0; i < v.Len(); i++ { | |
if i > 0 { | |
buf.WriteByte(',') | |
} | |
if err := marshal(buf, v.Index(i)); err != nil { | |
return err | |
} | |
} | |
buf.WriteByte(']') | |
case reflect.Map: | |
buf.WriteByte('{') | |
for i, key := range v.MapKeys() { | |
if i > 0 { | |
buf.WriteByte(',') | |
} | |
if err := marshal(buf, key); err != nil { | |
return err | |
} | |
buf.WriteByte(':') | |
if err := marshal(buf, v.MapIndex(key)); err != nil { | |
return err | |
} | |
} | |
buf.WriteByte('}') | |
case reflect.Struct: | |
_, err := marshalStruct(buf, v, false) | |
if err != nil { | |
return err | |
} | |
default: | |
var b []byte | |
var err error | |
if w, ok := v.Interface().(oplogMarshaler); ok { | |
b, err = w.MarshalOpLog() | |
} else { | |
b, err = json.Marshal(v.Interface()) | |
} | |
if err != nil { | |
return err | |
} | |
buf.Write(b) | |
} | |
return nil | |
} | |
func marshalStruct(buf *bytes.Buffer, v reflect.Value, embedded bool) (int, error) { | |
if v.Kind() != reflect.Struct { | |
return 0, fmt.Errorf("want Struct but got %s", v.Type()) | |
} | |
{ | |
var b []byte | |
var err error | |
done := true | |
if w, ok := v.Interface().(oplogMarshaler); ok { | |
b, err = w.MarshalOpLog() | |
} else if w, ok := v.Interface().(jsonMarshaler); ok { | |
b, err = w.MarshalJSON() | |
} else { | |
done = false | |
} | |
if done { | |
if err != nil { | |
return 0, err | |
} | |
if len(b) == 0 { | |
return 0, nil | |
} | |
buf.Write(b) | |
return 1, nil | |
} | |
} | |
if !embedded { | |
buf.WriteByte('{') | |
} | |
n := 0 | |
for i := 0; i < v.NumField(); i++ { | |
f := v.Type().Field(i) | |
if !f.IsExported() { | |
continue | |
} | |
// Deal with embedded struct. | |
if f.Anonymous { | |
w := v.Field(i) | |
if w.Kind() == reflect.Pointer { | |
w = w.Elem() | |
} | |
if n > 0 { | |
buf.WriteByte(',') | |
} | |
m, err := marshalStruct(buf, w, true) | |
if err != nil { | |
return 0, err | |
} | |
n += m | |
continue | |
} | |
key, ok := f.Tag.Lookup("oplog") | |
if !ok { | |
key = f.Tag.Get("json") | |
key = strings.Split(key, ",")[0] // ignore ",omitempty" etc. | |
} | |
if key == "-" { | |
continue | |
} | |
if key == "" { | |
key = f.Name | |
} | |
if n > 0 { | |
buf.WriteByte(',') | |
} | |
fmt.Fprintf(buf, `"%s":`, key) | |
if err := marshal(buf, v.Field(i)); err != nil { | |
return 0, err | |
} | |
n++ | |
} | |
if !embedded { | |
buf.WriteByte('}') | |
} | |
return n, nil | |
} | |
// ---------------------------------------------------------------------- | |
type BaseModel struct { | |
ID uint `json:"id"` | |
At time.Time `json:"at" oplog:"op_at"` | |
} | |
type Model1 struct { | |
BaseModel | |
AField string | |
BField string `json:"-" oplog:"bfield"` | |
CField string `json:"cfield" oplog:"-"` | |
DField string `json:"dfield" oplog:""` | |
Int1 int `json:"int1" oplog:"op_int1"` | |
Float1 float32 `oplog:"float1"` | |
} | |
type SubModel1 struct { | |
AAA string `json:"aaa"` | |
BBB string `json:"bbb" oplog:"-"` | |
} | |
type Model2 struct { | |
BaseModel | |
Sub1 *SubModel1 `json:"sub1"` | |
Sub2 []SubModel1 `json:"sub2"` | |
CCC *string `json:"ccc"` | |
Names []string `json:"names"` | |
Numbers []int `oplog:"numbers"` | |
Items map[string]int `json:"items"` | |
} | |
type Model3 struct { | |
BaseModel | |
} | |
type Model4 struct { | |
F1 string | |
F2 string `oplog:"f2"` | |
Time time.Time | |
BaseModel | |
} | |
type Model5 struct { | |
*BaseModel | |
} | |
type Model6 struct { | |
time.Time | |
} | |
type Model7 struct { | |
ABC string | |
XYZ int | |
} | |
func (m Model7) MarshalOpLog() ([]byte, error) { | |
s := fmt.Sprintf(`{"[abc]":"%s", "[xyz]":%d}`, m.ABC, m.XYZ) | |
return []byte(s), nil | |
} | |
type Model8 struct { | |
Base Model7 `oplog:"base"` | |
Hello string | |
} | |
func show(name string, v any) { | |
if b, err := json.Marshal(v); err != nil { | |
fmt.Printf("*** ERROR: json(%s) failed: %v\n", err) | |
} else { | |
fmt.Printf("json(%s): %s\n", name, b) | |
} | |
fmt.Println() | |
if b, err := Marshal(v); err != nil { | |
fmt.Printf("*** ERROR: oplog(%s) failed: %v\n", err) | |
} else { | |
fmt.Printf("oplog(%s): %s\n", name, b) | |
} | |
fmt.Println() | |
} | |
func main() { | |
bm := BaseModel{ID: 123, At: time.Now()} | |
m1 := Model1{ | |
BaseModel: bm, | |
AField: "aaa", | |
BField: "bbb", | |
CField: "ccc", | |
DField: "ddd", | |
Int1: 123456, | |
Float1: 123.456, | |
} | |
show("m1", m1) | |
sm1 := SubModel1{AAA: "a", BBB: "b"} | |
sm2 := SubModel1{AAA: "hhh", BBB: "yyy"} | |
str1 := "str1" | |
m2 := Model2{ | |
BaseModel: bm, | |
Sub1: &sm1, | |
Sub2: []SubModel1{sm1, sm2}, | |
CCC: &str1, | |
Names: []string{"name1", "name2", "name3"}, | |
Numbers: []int{11, 22, 33}, | |
Items: map[string]int{"name1": 11, "name2": 22, "name3": 33}, | |
} | |
show("m2", m2) | |
m3 := &Model3{BaseModel: bm} | |
show("m3", m3) | |
m4 := Model4{ | |
F1: "field1", | |
F2: "field2", | |
Time: time.Now(), | |
BaseModel: bm, | |
} | |
show("m4", m4) | |
m5 := &Model5{BaseModel: &bm} | |
show("m5", m5) | |
m6 := Model6{Time: time.Now()} | |
show("m6", m6) | |
m7 := Model7{ABC: "abc", XYZ: 123} | |
show("m7", m7) | |
m8 := Model8{ | |
Base: m7, | |
Hello: "hello", | |
} | |
show("m8", m8) | |
show("nil:untyped", nil) | |
var n1 *int | |
show("nil:ptr", n1) | |
type myint int | |
var n2 *myint | |
show("nil:myint", n2) | |
var n3 any | |
show("nil:any", n3) | |
n3 = n2 | |
show("nil:any=", n3) | |
type iface1 interface{} | |
var n4 iface1 | |
show("nil:iface1", n4) | |
type stringer interface { | |
String() string | |
} | |
var n5 stringer | |
show("nil:stringer", n5) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Program output: