如何在Go中将一个JSON对象数组转换为带有默认值的结构体数组?

3

我正在开发一个Go API,可以接收由JSON对象数组组成的POST请求。POST请求的结构大致如下:

[
  {
    "name":"Las Vegas",
    "size":14
  },
  {
    "valid": false,
    "name":"Buffalo",
    "size":63
  }
]  

假设我有以下结构体:

type Data {
    Valid    bool
    Name     string
    Size     float64
}

我希望创建一组 Data,如果 JSON 中未指定为 false,则将其设置为 true。如果我只需要创建单个 Data,我可以使用如何在 Go 中解析 JSON 时指定默认值,但是对于未知数量的数据,我能想到的唯一方法就是像下面这样:
var allMap []map[string]interface{}
var structs []Data
for _, item := range allMap {
  var data Data
  var v interface{}
  var ok bool
  if v, ok := item["value"]; ok {
    data.Valid = v
  } else {
    data.Valid = true
  }
  id v, ok := item["name"]; ok {
    data.Name = v
  }
  ...
  structs = append(structs, data)
}
return structs

目前我正在使用的结构体有14个字段,其中一些字段的值我想要分配默认值,而另一些则可以留空,但是所有字段都必须使用这种方法进行迭代。

是否有更好的方法?

2个回答

3
您可以使用json.RawMessage类型来推迟解组一些JSON文本值。如果使用此类型,则JSON文本将存储在其中而不进行解组(因此您可以稍后按照需要对此片段进行解组)。
因此,在您的情况下,如果尝试解组为这种RawMessage的切片,则可以使用您在问题中提到的技术,即可以遍历原始值的切片(每个Data的JSON文本),创建一个具有所需默认值的Data结构,并将一个切片元素解组为此准备好的结构。就是这样。
代码示例如下:
allJson := []json.RawMessage{}
if err := json.Unmarshal(src, &allJson); err != nil {
    panic(err)
}

allData := make([]Data, len(allJson))
for i, v := range allJson {
    // Here create your Data with default values
    allData[i] = Data{Valid: true}
    if err := json.Unmarshal(v, &allData[i]); err != nil {
        panic(err)
    }
}

请在Go Playground上试用。

备注/变体

为了提高效率(避免复制结构体),您也可以将上面的例子中的allData设置为指针切片,代码如下:

allData := make([]*Data, len(allJson))
for i, v := range allJson {
    // Here create your Data with default values
    allData[i] = &Data{Valid: true}
    if err := json.Unmarshal(v, allData[i]); err != nil {
        panic(err)
    }
}

如果您想继续使用非指针类型,为了提高效率,您可以在切片元素本身中“预先准备”所需的默认值,代码如下:
allData := make([]Data, len(allJson))
for i, v := range allJson {
    // Here set your default values in the slice elements
    // Only set those which defer from the zero values:
    allData[i].Valid = true
    if err := json.Unmarshal(v, &allData[i]); err != nil {
        panic(err)
    }
}

工作得非常出色,谢谢。也许这是一个天真的问题,但指针切片相对于结构体切片有什么更高效的地方呢?无论哪种方式,代码都在使用引用,不是吗? - j boschiero
在指针的情况下,当你给切片元素赋值时,它只会复制一个指针(4或8个字节)。如果切片包含非指针类型,给切片元素赋值(例如 allData[i] = &Data{Valid: true})会复制整个结构体,如果你的结构体很大(你说它有14个字段),那么它可能会很大,如果结构体的大小为200字节,则赋值将复制200字节。 - icza

1
你可以通过在类型上提供一个 UnmarshalJSON 方法来实现透明和自动工作,即使你的类型在结构体或切片中被发现。
func (d *Data) UnmarshalJSON(j []byte) error {
    type _Data Data // Dummy type to avoid infinite recursion in UnmarshalJSON
    tmp := _Data{ // Set defaults here
        Valid: true,
    }

    err := json.Unmarshal(j, &tmp)
    if err != nil {
        return err
    }

    *d = Data(tmp)
    return nil
}

类型_Data的存在仅仅是为了我们能够调用json.Unmarshal(j, &tmp)并获得原始的未被覆盖的行为,而不是调用我们已经在中间的UnmarshalJSON方法。我们可以使用您已经链接到的技巧在tmp上设置默认值。然后在解组完成后,我们只需将tmp强制转换为Data,因为毕竟Data_Data实际上是相同的类型。

有了这种方法,您可以简单地

var structs []Data
err := json.Unmarshal(input, &structs)

(或类似使用 json.Decoder)使其按照您所希望的方式正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接