美文网首页Go
Go开发的各种坑 - struct序列化与反序列化

Go开发的各种坑 - struct序列化与反序列化

作者: 红薯爱帅 | 来源:发表于2023-05-09 19:25 被阅读0次

    1. 概述

    Golang中,巨大的坑就是struct的序列化和反序列化。

    struct的字段初始值,是Go零值,例如0、""、false。在CRUD操作中,需要两次序列化和反序列化,json<-->struct<-->db,存在的问题:

    • 1)增加实体时,某些字段选填,对应的内容应该是nil,不应该是""或0
    • 2)更新实体时,如果要支持部分字段更新,拿到的是一个完整的struct,字段值为0或"",无法确认是否是用户填写
    • 3)读取实体时,数据库的某些Number类型字段为null,读取到struct后则为0,显然不符合实际情况(0也是有意义的数值)

    基于此,简单方法,只支持全量更新,且不区分零值和nil,会带来诸多不便。

    如果要区分,解决办法有几个:

    • 通过指针的方式解决,即将field类型定义为*int*string等,可参考package null
    • 自定义类型,完成struct与json以及数据库的序列化和反序列化,可以严格按照自己的想法实现

    本文将对上述两种方法举例说明。

    当然,也有其他怪招,比如

    • 每次update请求,客户端指明要更新的字段名,基于此,服务端可以只读取特定字段,挺麻烦
    • json数据反序列化到map[any]any,然后逐个字段判断,这样基本上废弃了struct,代码复杂度骤增

    2. 通过指针的方式

    • 代码
    package main
    
    import (
        "encoding/json"
        "log"
    )
    
    type Foo struct{ Val *int }
    
    func do(bytes []byte) (Foo, error) {
        var a Foo
        err := json.Unmarshal(bytes, &a)
        return a, err
    }
    
    func testDeserialize() {
        notSet := []byte(`{}`)
        setNull := []byte(`{"val": null}`)
        setValid := []byte(`{"val": 123}`)
        setWrongType := []byte(`{"val": "123"}`)
    
        a, err := do(notSet)
        log.Printf("NotSet      |value:%v |err: %v\n", a.Val, err)
        a, err = do(setNull)
        log.Printf("SetNull     |value:%v |err: %v\n", a.Val, err)
        a, err = do(setValid)
        log.Printf("SetValid    |value:%d   |err: %v\n", *a.Val, err) // 注意:实际应用时,需要先判断a.Val是否为nil
        a, err = do(setWrongType)
        log.Printf("SetWrongType|value:%d     |err: %v\n", *a.Val, err) // 注意,这时候a.Val居然是0,而不是nil
    }
    
    func testSerialize() {
        notSet := Foo{}
        setNull := Foo{nil}
        setValid := Foo{&[]int{22}[0]}
    
        res, err := json.Marshal(notSet)
        log.Printf("NotSet |result:%s |err: %v\n", res, err)
        res, err = json.Marshal(setNull)
        log.Printf("setNull |result:%s |err: %v\n", res, err)
        res, err = json.Marshal(setValid)
        log.Printf("setValid |result:%s |err: %v\n", res, err)
    }
    
    func main() {
        log.Println("deserialize test ...")
        testDeserialize()
        log.Println("serialize test ...")
        testSerialize()
    }
    
    • 运行结果
    % go run test_null.go
    2023/05/10 15:10:12 deserialize test ...
    2023/05/10 15:10:12 NotSet      |value:<nil> |err: <nil>
    2023/05/10 15:10:12 SetNull     |value:<nil> |err: <nil>
    2023/05/10 15:10:12 SetValid    |value:123   |err: <nil>
    2023/05/10 15:10:12 SetWrongType|value:0     |err: json: cannot unmarshal string into Go struct field Foo.Val of type int
    2023/05/10 15:10:12 serialize test ...
    2023/05/10 15:10:12 NotSet |result:{"Val":null} |err: <nil>
    2023/05/10 15:10:12 setNull |result:{"Val":null} |err: <nil>
    2023/05/10 15:10:12 setValid |result:{"Val":22} |err: <nil>
    

    3. 自定义类型

    • 代码
    package main
    
    import (
        "encoding/json"
        "log"
        "strconv"
    )
    
    type Int struct {
        Exists bool // 表示是否存在
        IsNull bool // 表示是否为null
        Value  int
    }
    
    // UnmarshalJSON 自定义反序列化方法
    func (i *Int) UnmarshalJSON(data []byte) error {
        // 如果调用了该方法,说明设置了该值
        i.Exists = true
        if string(data) == "null" {
            // 表明该字段的值为 null
            i.IsNull = true
            return nil
        } else {
            i.IsNull = false
        }
    
        var temp int
        if err := json.Unmarshal(data, &temp); err != nil {
            return err
        }
        i.Value = temp
        return nil
    }
    
    // MarshalJSON 自定义序列化方法
    func (i Int) MarshalJSON() ([]byte, error) {
        if !i.Exists || i.IsNull {
            return []byte("null"), nil // 注意:必须是小写null,不能是NULL、Null,原因Json不允许
        }
        return []byte(strconv.Itoa(i.Value)), nil
    }
    
    type Some struct{ Val Int }
    
    func do(bytes []byte) (Some, error) {
        var a struct{ Val Int }
        err := json.Unmarshal(bytes, &a)
        return a, err
    }
    
    func testDeserialize() {
        notSet := []byte(`{}`)
        setNull := []byte(`{"val": null}`)
        setValid := []byte(`{"val": 123}`)
        setWrongType := []byte(`{"val": "123"}`)
    
        a, err := do(notSet)
        log.Printf("NotSet      |Exists:%t |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
        a, err = do(setNull)
        log.Printf("SetNull     |Exists:%t  |IsNull:%t  |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
        a, err = do(setValid)
        log.Printf("SetValid    |Exists:%t  |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
        a, err = do(setWrongType)
        log.Printf("SetWrongType|Exists:%t  |IsNull:%t |Value: %d |err: %v\n", a.Val.Exists, a.Val.IsNull, a.Val.Value, err)
    }
    
    func testSerialize() {
        notSet1 := Some{Int{Exists: false}}
        notSet2 := Some{Int{Exists: false}}
        setNull := Some{Int{Exists: true, IsNull: true}}
        setValid := Some{Val: Int{Exists: true, IsNull: false, Value: 33}}
    
        res, err := json.Marshal(notSet1)
        log.Printf("NotSet1 |result:%s |err: %v\n", res, err)
        res, err = json.Marshal(notSet2)
        log.Printf("notSet2 |result:%s |err: %v\n", res, err)
        res, err = json.Marshal(setNull)
        log.Printf("setNull |result:%s |err: %v\n", res, err)
        res, err = json.Marshal(setValid)
        log.Printf("setValid |result:%s |err: %v\n", res, err)
    }
    
    func main() {
        log.Println("deserialize test ...")
        testDeserialize()
        log.Println("serialize test ...")
        testSerialize()
    }
    
    • 运行结果
    % go run test_json.go
    2023/05/10 15:15:50 deserialize test ...
    2023/05/10 15:15:50 NotSet      |Exists:false |IsNull:false |Value: 0 |err: <nil>
    2023/05/10 15:15:50 SetNull     |Exists:true  |IsNull:true  |Value: 0 |err: <nil>
    2023/05/10 15:15:50 SetValid    |Exists:true  |IsNull:false |Value: 123 |err: <nil>
    2023/05/10 15:15:50 SetWrongType|Exists:true  |IsNull:false |Value: 0 |err: json: cannot unmarshal string into Go struct field .Val of type int
    2023/05/10 15:15:50 serialize test ...
    2023/05/10 15:15:50 NotSet1 |result:{"Val":null} |err: <nil>
    2023/05/10 15:15:50 notSet2 |result:{"Val":null} |err: <nil>
    2023/05/10 15:15:50 setNull |result:{"Val":null} |err: <nil>
    2023/05/10 15:15:50 setValid |result:{"Val":33} |err: <nil>
    

    4. 总结

    在实际应用中,基本不用区分json的null未设置,所以,都可以一并对应到struct的nil,认为用户未设置

    上述两种办法都是对Golang基本数据类型的补充,在使用过程中,会存在诸多不变,例如

    • 使用之前,需要先判断是否为nil,或者是否exists。如果直接使用,可能直接panic
    • 解决了json与struct的序列化问题,还需要关注如何兼容struct与数据库之间的数据读写

    所以,在Golang官方没有改变的情况下,还是尽量使用基本数据类型,否则,操作起来比较麻烦。

    另外,在对空数组、空字典的序列化,是符合预期的,能区分开nil和[]、{}。

    • nil,对应json的null
    • []int{}、make([]int, 0, 10),都对应json的[]
    • map[string]any{}、make(map[string]any),都对应json的{}

    json.Marshal() will return null for var myslice []int and [] for initialized slice myslice := []int{}

    5. Refer

    相关文章

      网友评论

        本文标题:Go开发的各种坑 - struct序列化与反序列化

        本文链接:https://www.haomeiwen.com/subject/uzayjdtx.html