【问题标题】:Go deserialization when type is not known当类型未知时进行反序列化
【发布时间】:2019-11-27 02:54:22
【问题描述】:

我正在使用特定类型的传输在 go 中编写一个包以在服务之间发送消息。

我希望包裹不了解正在发送的消息类型。 我的第一个想法是将消息对象序列化为 json,发送,在接收端反序列化,并将 go 对象(作为interface{})传递给订阅代码。

序列化不是问题,但我看不出通用包代码如何反序列化消息,因为它不知道类型。我想过使用反射TypeOf(),并将该值作为消息的一部分传递。但我不知道如何实现这一点,因为 Type 是一个接口并且没有导出实现的 rtype。

如果接收应用程序得到一个interface{},它无论如何都必须检查类型,所以也许它应该只进行反序列化。或者接收者可以提供一个反射类型,以便包可以反序列化?

或者它可以为接收者提供字段名称到值的映射,但我更喜欢实际类型。

有什么建议吗?

让我添加一个例子:

我有一个用于发送不同类型对象的更改通知的 go 频道。由于 go 不支持标记联合,我将通道类型定义为:

type UpdateInfo struct {
    UpdateType UpdateType
    OldObject interface{}
    NewObject interface{}
}

通道的接收端获得一个 UpdateInfo,其中 OldObject 和 NewObject 作为实际发送的具体对象类型。

我想扩展它以在应用程序之间工作,其中传输将通过消息队列来支持发布/订阅、多个消费者等。

【问题讨论】:

  • 这个问题没有意义。你想让对方不知道类型但你想让他们也知道吗?唯一近似于此的是发送一个类似 json 的对象(例如map[string]interface{}),但有什么意义呢?
  • 如果实际类型很重要(例如结构体),那么接收端必须提前知道它,因为 Go 无法在运行时动态创建自定义类型。
  • @user10753492 我的消息传递包的发送方知道具体类型。我希望它将此信息传递给包的接收方,以便它可以将其解组为正确的具体类型。
  • 我发布了一个答案,假设发送者和接收者都知道有限的具体类型集

标签: go serialization


【解决方案1】:

TL;DR

只需使用json.Unmarshal。您可以使用您的传输方式轻轻包装它,然后在您的预构建 JSON 字节和来自调用者的 v interface{} 参数上调用 json.Unmarshal(或使用 json.Decoder 实例,使用 d.Decode)。

有点长,举个例子

想想json.Unmarshal 如何发挥自己的魔力。它的第一个参数是 JSON (data []byte),但它的第二个参数是 interface{} 类型:

func Unmarshal(data []byte, v interface{}) error

正如文档继续说的,如果v 真的只是一个interface{}

为了将 JSON 解组为接口值,Unmarshal 将其中一项存储在接口值中:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

但如果v 有一个底层的具体类型,比如type myData struct { ... },那就更棒了。如果v 的底层类型 interface{},它只会执行上述操作。

它的actual implementation 特别复杂,因为它经过优化可以同时进行去JSON 化和分配到目标对象。但原则上,它主要是对接口值的底层(具体)类型进行大的类型切换。

同时,您在问题中描述的是,您将首先反序列化为通用 JSON(这实际上意味着 interface{} 类型的变量),然后对这个 pre 进行自己的赋值 out - 将 JSON 解码为另一个 interface{} 类型的变量,您自己的解码器的类型签名将是:

func xxxDecoder(/* maybe some args here, */ v interface{}) error {
    var predecoded interface{}

    // get some json bytes from somewhere into variable `data`
    err := json.Unmarshal(data, &predecoded)

    // now emulate json.Unmarshal by getting field names and assigning
    ... this is the hard part ...
}

然后您将通过编写以下代码调用此代码:

type myData struct {
    Field1 int    `xxx:"Field1"`
    Field2 string `xxx:"Field2"`
}

这样您就知道 JSON 对象键“Field1”应该用整数填充您的 Field1 字段,而 JSON 对象键“Field2”应该用字符串填充您的 Field2 字段:

func whatever() {
    var x myData
    err := xxxDecode(..., &x)
    if err != nil { ... handle error ... }
    ... use x.Field1 and x.Field2 ...
}

但这很愚蠢。你可以写:

type myData struct {
    Field1 int    `json:"Field1"`
    Field2 string `json:"Field2"`
}

(或者甚至省略标签,因为字段的名称是默认的 json 标签),然后这样做:

func xxxDecode(..., v interface{}) error {
    ... get data bytes as before ...
    return json.Unmarshal(data, v)
}

换句话说,只需让json.Unmarshal 完成所有工作,方法是在相关数据结构中提供 json 标签。您仍然可以通过您的特殊传输获得并传输 JSON 数据字节来自 json.Marshaljson.Unmarshal。你负责发送和接收。 json.Marshaljson.Unmarshal 尽一切努力:你不必碰它!

看看Json.Unmarshal 的工作原理还是很有趣的

跳转到around line 660 of encoding/json/decode.go,您将在其中找到处理JSON“对象”({ 后跟} 或表示键的字符串)的东西,例如:

func (d *decodeState) object(v reflect.Value) error {

有一些机制可以处理极端情况(包括v 可能不可设置和/或可能是应遵循的指针这一事实),然后它确保vmap[T1]T2struct,如果它是一个映射,那么它是合适的——T1T2 在解码对象中的 "key":value 项时都可以工作。

如果一切顺利,它会从第 720 行开始进入 JSON 键值扫描循环(for {,它将酌情中断或返回)。在此循环的每次行程中,代码首先读取 JSON 键,将 : 和值部分留待以后使用。

如果我们要解码为struct,解码器现在使用结构的字段(名称和json:"..." 标签)来查找我们将用于存储到字段中的reflect.Value 1 这是subv,通过调用v.Field(i) 找到右边的i,带有一些稍微复杂的goo 来处理嵌入式匿名structs 和指针跟随。不过,其核心只是subv = v.Field(i),其中i 是结构中此键名称的任何字段。所以subv 现在是一个reflect.Value,它代表实际结构实例的值,一旦我们解码了 JSON 键值对的值部分,就应该设置它。

如果我们要解码成map,我们会先将值解码成一个临时值,然后在解码后将它存储到map中。将其与 struct-field 存储共享会很好,但我们需要一个不同的 reflect 函数来存储到地图中:v.SetMapIndex,其中v 是地图的reflect.Value。这就是为什么对于地图,subv 指向一个临时的Elem

我们现在已经准备好将实际值转换为目标类型,因此我们返回 JSON 字节并使用冒号 : 字符并读取 JSON 值。我们获取值并将其存储到我们的存储位置 (subv)。这是从第 809 行 (if destring {) 开始的代码。实际分配是通过解码器函数(第 908 行的d.literalStore 或第 412 行的d.value)完成的,这些函数实际上在进行存储时解码 JSON 值。请注意,只有 d.literalStore 真正存储了值——d.value 会调用 d.arrayd.objectd.literalStore,以便在需要时递归地完成工作。

d.literalStore 因此包含许多switch v.Kind()s:它解析nulltruefalse 或整数或字符串或数组,然后确保它可以将结果值存储到@ 987654399@,并根据刚刚解码的内容和实际的v.Kind() 的组合选择如何将该结果值存储到v.Kind()。所以这里有点组合爆炸,但它完成了工作。

如果一切正常,并且我们正在解码为映射,我们现在可能需要按摩临时的类型,找到真正的键,并将转换后的值存储到映射中。这就是第 830 行 (if v.Kind() == reflect.Map {) 到 867 处最后一个右括号的内容。


1要查找字段,我们首先查看encoding/json/encode.go 以查找cachedTypeFields。它是typeFields 的缓存版本。这是找到 json 标记并将其放入切片的位置。结果通过cachedTypeFields 缓存在由struct 类型的反射类型值索引的映射中。所以我们得到的是第一次使用struct 类型时的慢速查找,然后是快速查找,以获取有关如何进行解码的信息片段。此信息片从 json-tag-or-field 名称映射到: field;类型;是否是匿名结构的子字段;等等:在编码方面,我们需要知道正确解码或编码它所需的一切。 (我没有仔细看这段代码。)

【讨论】:

  • 您基本上是在说我的消息传递包不应该尝试解组实际的消息对象,并将其留给接收应用程序。这不是世界末日,但它为接收应用程序留下了更多工作,并暴露了消​​息传递包在其实现中使用 json 的事实。
  • 这根本不是它的意思。包可以处理解组。接收应用程序只是传递它希望将消息解组到的对象,就像您对 json.Unmarshal 所做的那样。
  • 我原来的描述不够清楚,所以我更新了它。在我的主要用例(更改通知)中,对象包含 interface{} 字段,因此仅指定对象是不够的。
  • 如果对象中包含interface{},则接收对象的具体类型为interface{}。解决这个问题的方法是“不要那样做”——改为接收到实际对象。我确实认为拥有一个 json.Unmarshal 的变体可能会很方便,它将 JSON 化的数据存储在 interface{}map[string]interface{} 或类似中,以避免在某些情况下额外的往返。
  • 我的结论是,让它像 go 频道一样整洁是不可能的,我应该使用另一种方法。
【解决方案2】:

您可以在同一个缓冲区中编码/解码多个消息,无论是“gob”、“json”还是其他编码。

假设您想要支持的具体类型有限,您始终可以首先将类型标记编码为第一件事,然后再编码实际对象。这样decode可以先解码type标签,然后根据它的值,决定如何解码下一个item。

// encoder side

enc := json.NewEncoder(buffer) // or gob.NewEncoder(buffer)
enc.Encode("player")
enc.Encode(playerInstance)


// decoder side

dec := json.NewDecoder(buffer) // or gob.NewDecoder(buffer)
var tag string
dec.Decode(&tag)
switch tag {
    case "player":
        var playerInstance Player
        dec.Decode(&player)
        // do something with it
    case "somethingelse":
        // decode something else
}

【讨论】:

  • 是的,这就是我目前正在做的事情。但是,要么通用包需要了解类型集,以便它可以进行解码(这对于一般的更改通知服务来说是不需要的),要么包的用户需要进行解码(我更喜欢包为它做了那个)
【解决方案3】:

尝试dynobuffers 而不是结构。它为字节数组提供了按名称获取和设置的能力。它也比 json 快得多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-15
    • 2013-07-11
    • 2015-03-23
    相关资源
    最近更新 更多