Golang中使用Alias方法对嵌套结构体进行JSON反序列化的异常行为

Golang中使用Alias方法对嵌套结构体进行JSON反序列化的异常行为 我偶然发现了这篇博客 Go 中的自定义 JSON 序列化 · Choly’s Blog,它使用别名(Alias)来反序列化一个结构体(并避免递归)……但当我尝试对嵌套结构体(它们有自己的 Unmarshal() 方法)使用类似的方法时,它就不再起作用了。请参见下面的代码。

看起来,“派生”结构体(BS)的别名被识别为 AS,因此调用了 AS.Unmarshal(),但后续的字段却没有被反序列化。我真的很想理解这是为什么。

我注意到,如果我在 type BS { AS; CS; Name string} 中包含另一个结构体 CS,并且 CS 有自己的 Unmarshal() 方法,代码就能正常工作。然而,如果 CS 没有自己的 Unmarshal() 方法,它就不会更新 CS 的值或 Name 字段。

package main

import (
   "encoding/json"
   "fmt"
)

type AS struct{ Id int }

func (as *AS) UnmarshalJSON(data []byte) error {
   type Alias AS
   aux := struct{ *Alias }{Alias: (*Alias)(as)}

   if err := json.Unmarshal(data, &aux); err != nil {
      return err
   }
   return nil
}

type BS struct {
   AS
   Name string 
}

func (bs *BS) UnmarshalJSON(data []byte) error {
   type Alias BS
   aux := struct{ *Alias }{Alias: (*Alias)(bs)}

   if err := json.Unmarshal(data, &aux); err != nil {
      return err
   }
   return nil
}

func main() {
   bs := BS{}
   data := []byte(`{"id": 10, "name": "TEST ME"}`)
   if err := json.Unmarshal(data, &bs); err != nil {
      panic(err)
   }

   fmt.Printf("bs: %#v\n", bs)
   // 没有 AS.UnmarshalJSON() 时的正确结果 - 例如通过更改方法名称
   // 没有 AS.UnmarshalJSON() 时的输出:bs: main.BS{AS:main.AS{Id:10}, Name:"TEST ME"}

   // 有 AS.UnmarshalJSON() 时的错误结果
   // 输出:bs: main.BS{AS:main.AS{Id:10}, Name:""}
   // 看起来它只调用了 AS.UnmarshalJSON(),后续字段没有被反序列化
}

我真的很想理解这是为什么?


更多关于Golang中使用Alias方法对嵌套结构体进行JSON反序列化的异常行为的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中使用Alias方法对嵌套结构体进行JSON反序列化的异常行为的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这个问题涉及到Go语言JSON反序列化中方法集解析和结构体嵌入的交互。当BS嵌入了AS,而两者都有UnmarshalJSON方法时,会出现方法集冲突。

关键原因在于:BSUnmarshalJSON方法中,aux结构体使用了*Alias类型,而AliasBS的别名。由于BS嵌入了AS*Alias类型会继承ASUnmarshalJSON方法,导致JSON包在反序列化aux时直接调用了AS.UnmarshalJSON,跳过了BS字段的常规反序列化。

以下是修复后的代码示例:

package main

import (
    "encoding/json"
    "fmt"
)

type AS struct{ Id int }

func (as *AS) UnmarshalJSON(data []byte) error {
    type Alias AS
    aux := &struct {
        *Alias
    }{
        Alias: (*Alias)(as),
    }
    
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    return nil
}

type BS struct {
    AS
    Name string
}

func (bs *BS) UnmarshalJSON(data []byte) error {
    // 使用临时结构体避免继承AS的UnmarshalJSON方法
    aux := &struct {
        AS   `json:",inline"`
        Name string `json:"name"`
    }{}
    
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    
    bs.AS = aux.AS
    bs.Name = aux.Name
    return nil
}

func main() {
    bs := BS{}
    data := []byte(`{"id": 10, "name": "TEST ME"}`)
    if err := json.Unmarshal(data, &bs); err != nil {
        panic(err)
    }

    fmt.Printf("bs: %#v\n", bs)
    // 输出: bs: main.BS{AS:main.AS{Id:10}, Name:"TEST ME"}
}

或者,如果希望保持原有的别名模式,可以显式地处理嵌入字段:

func (bs *BS) UnmarshalJSON(data []byte) error {
    type Alias BS
    aux := &struct {
        *Alias
        // 显式声明AS字段,避免继承方法
        AS json.RawMessage `json:"as"`
    }{
        Alias: (*Alias)(bs),
    }
    
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    
    // 手动处理AS字段
    if err := json.Unmarshal(aux.AS, &bs.AS); err != nil {
        return err
    }
    
    return nil
}

问题的本质是:当结构体嵌入另一个有UnmarshalJSON方法的结构体时,JSON包会优先调用嵌入结构体的方法。在别名方法中,由于类型转换保留了方法集,导致AS.UnmarshalJSON被意外调用。

回到顶部