我有一个struct,它嵌入了指向另一个struct的嵌入式指针。当我使用默认的json.Unmarshal行为时,它可以完美运行。但是,当我为嵌入式 UnmarshalJSON的类型而不是外部struct实现struct时,则使用空指针取消引用进行恐慌。

如果我也为外部UnmarshalJSON类型实现了struct,那么它将起作用。但是,外部结构有很多字段,我不希望手动解组。

  • 为什么在一个而不是另一个上实现UnmarshalJSON会引起恐慌?
  • 是否可以在没有为外部类型实现UnmarshalJSON的情况下使其工作?
  • 如果不是,是否有更简单/更少的手动方法为外部类型实现UnmarshalJSON

  • 注意:有一个标题相似的问题“json.Unmarshal fails when embedded type has UnmarshalJSON”,但该问题与我的不同。

    tl; dr:这个问题的其余部分只是上面冗长的示例。

    基本示例

    (例如play.golang.org version)

    这两个结构,一个具有指向另一个的嵌入式字段指针:

    (例如,经过简化-确实不需要它自己的UnmarshalJSON,但是它说明了问题。)
    type Obj struct {
        X int `json:"x"`
    }
    
    type Container struct {
        *Obj
        Y int `json:"y"`
    }
    

    调用元帅:
    func main() {
        b := []byte(`{"x": 5, "y": 3}`)
        c := &Container{}
        err := json.Unmarshal(b, c)
        if err != nil {
            fmt.Printf("error ummarshalling json: %+v\n", err)
            return
        }
        fmt.Printf("unmarshalled: %+v --> %+v\n", c, c.Obj)
    }
    

    如果不实现任何UnmarshalJSON函数,则可以正常工作:
    unmarshalled: &{Obj:0x416080 Y:3} --> &{X:5}
    

    恐慌

    但是,如果仅将UnmarshalJSON添加到嵌入式Obj类型,则程序恐慌,因为json.Unmarshal调用在尝试解组nil时会传递*Obj指针。
    func (o *Obj) UnmarshalJSON(b []byte) (err error) {
        m := make(map[string]int)
        err = json.Unmarshal(b, &m)
        if err != nil {
            return nil
        }
        o.X = m["x"] // the line indicated by panic
        return nil
    }
    

    输出:
    panic: runtime error: invalid memory address or nil pointer dereference
    [...]
    main.(*Obj).UnmarshalJSON(0x0, 0x416030, 0x10, 0x10, 0x0, 0x0)
        /tmp/sandbox185809294/main.go:18 +0x130
    [...]
    

    问题:为什么在这里会出现恐慌,但默认的解组行为却不会呢?我认为如果在这里传递nil *Obj,那么默认行为也会绕过nil指针...

    解决恐慌

    当我为外部UnmarshalJSON类型实现Container时,它不再感到恐慌:
    func (c *Container) UnmarshalJSON(b []byte) (err error) {
        m := make(map[string]int)
        err = json.Unmarshal(b, &m)
        if err != nil {
            return err
        }
        c.Obj = &Obj{X: m["x"]}
        c.Y = m["y"]
        return nil
    }
    

    但是,如果实际Container和实际Container都具有比此更多的字段,并且每种字段具有不同的类型,则以这种方式手动解组Obj将变得乏味。

    问题:有没有更简单的方法来防止这种恐慌?

    最佳答案

    因为默认行为会检查nil,而您的自定义解组器则不会。您需要在UnmarshalJSON中添加一些逻辑来检查o是否为nil并正常运行,而不是假设o不是nil(通过尝试访问其字段之一),从而引发恐慌。

    func (o *Obj) UnmarshalJSON(b []byte) (err error) {
        if o == nil {
            return nil // maybe? What do you want to happen in this case?
        }
        m := make(map[string]int)
        err = json.Unmarshal(b, &m)
        if err != nil {
            return nil
        }
        o.X = m["x"] // the line indicated by panic
        return nil
    }
    

    同样仅供将来参考,您的*Obj字段不是“匿名字段”,而是嵌入式字段:https://golang.org/ref/spec#Struct_types

    09-25 16:45