【问题标题】:Using msgp with interfaces and maps in Go在 Go 中将 msgp 与接口和映射一起使用
【发布时间】:2026-01-21 20:25:01
【问题描述】:

我有一个使用接口作为键的地图。该地图的定义如下MyMap map[Signature]Packets。接口是Signature,会有两个结构AB实现这个接口。我也在使用msgp 来序列化这两个结构。

我的问题是msgp 自动生成使用指针作为函数接收器类型的方法,我认为这将使键Signature 接收指针。如果是这种情况,那么键每次都会不同,因为指针不同,即使基础值相同。因此,每次我都会创建一个新条目,而不是查找现有条目并对其进行修改。

我想知道:

  1. 有没有办法强制msgp 生成纯粹使用具体类型的函数接收器的方法?目前,我只能将MarshalMsgUnmarshalMsg等自动生成方法的函数接收器修改为具体类型(AB而不是*A*B)。通过这样做,映射的键是A 类型或B 类型,并且映射MyMap 工作正常。但是,我知道我不应该修改自动生成的代码。所以,我想知道是否有一种可接受的方式来做到这一点。
  2. 如果没有办法做 1.,有没有办法解决这个问题?我真的需要使用 msgp 的地图键的一些多态特性。

更新 1(4 月 12 日): 感谢您分享您的想法并提供解决方案。以下是有关我的问题的一些详细信息。

  1. 背景是地图用于收集不同的网络事件。实现接口Signature的两个结构是EventSignatureIPv4EventSignatureIPv6
type EventSignatureIPv4 struct {
    SourceIPv4 [4]byte
    Port     uint16
    Traffic  TrafficType
}

type EventSignatureIPv6 struct {
    SourceIPv6 [16]byte
    Port     uint16
    Traffic  TrafficType
}

并且Signature 持有在 IPv4 和 IPv6 数据之间共享的通用方法。因此,本质上,我想在运行时收集和分组相应的 IPv4/v6 事件。地图的关键是识别同一个源,地图的价值是收集不同目的地的事件。

  1. 我正在使用的msgp 库是这个https://pkg.go.dev/github.com/tinylib/msgp@v1.1.5/msgp

  2. 如果我错了,请纠正我。对于 Go 中的组合,如果方法集中的方法之一具有指针类型的函数接收器,那么实例只会是指针类型吗?所以在这里,就像我一样

func (z *EventSignatureIPv6) MarshalMsg(b []byte) (o []byte, err error) {
    /* Auto-generated code */
}

每当我使用Signature 接收结构体EventSignatureIPv6 时,结构体只能是*EventSignatureIPv6 类型?

【问题讨论】:

  • 您使用的是哪个msgp 库?
  • @Sebastian 在 Golang 中,因为 Struct 是一种值类型,使用它们作为 Map 的 Key 是可能的。您是正确的,如果使用指针,您每次都会为相同的数据获得不同的密钥。因此,如果可能,您可以使用*pointer 语法获取指针的值,而不是直接保存指针。此外,如果您可以包含一些代码,则可以提出更好的解决方法。
  • @Christian 我使用的msgp 库是pkg.go.dev/github.com/tinylib/msgp@v1.1.5/msgp
  • @SaiRaviTejaK 我认为这与您所说的情况不同。我已经更新了我的问题,请再看一下,看看您是否还有其他想法。
  • 地图的关键是识别相同的来源”——这在语义上并不合理,IMO。接口类型映射键的相等性甚至与数据包源的相等性意味着相同。由于实现接口的结构完全属于不同类型,因此您的 v4 和 v6 数据包将永远不会具有相同的哈希值。 See it for yourself,或对您的代码运行一些单元测试。如果您因为它们可能提供的某些分组/标识方法而考虑使用这些接口,请改用 that 来键入您的地图。

标签: go msgpack go-interface go-map


【解决方案1】:

你是对的,“如果两个指针值指向同一个变量,则它们是相等的。”,所以如果你想比较可能持有不同类型指针的接口,例如*A*B,你已经有麻烦了。

话虽如此,我不认为首先使用接口类型作为映射键是一个了不起的想法,因为您必须处理一些警告,首先是:

必须为键类型的操作数完全定义比较运算符 == 和 !=

现在您需要注意实现接口的类型。理论上,没有人会阻止客户端在具有底层不可散列类型的已定义类型上实现您的接口,例如type UncomparableSignature []int

因此,您可能必须在接口上添加一个未导出的方法,以便该包之外的客户端代码无法实现它。但是,没有什么能阻止代码同一个包中实现它,所以这充其量只是维护开销。

如果接口持有指向零值的指针,它甚至取决于规范的实现:

指向不同的零大小变量的指针可能相等,也可能不相等。

此外,您还会遇到令人讨厌的错误,例如拥有nilSignature 类型的变量会覆盖彼此的值:

var foo Signature
var bar Signature

myMap[foo] = &Packet{/*pretending to have value 1*/}
myMap[bar] = &Packet{/*pretending to have value 2*/}

fmt.Println(myMap[foo]) // 2

一个可能的解决方案是,您可以用唯一的 id 替换映射键,并通过在接口 Signature 上声明适当的方法来强制实现者提供它(这仍然假设实现者可以协调以提供唯一的ids):

type Signature interface {
    UniqueIdent() uint64 // or string, if you prefer
    // ...other methods
}

然后

packet := myMap[someSignature.UniqueIdent()]

【讨论】:

  • 感谢您的回答。我已经提供了有关我的问题的更多详细信息,并希望寻求更多建议。独特的 id 想法很棒,但我希望开销尽可能小,这也是我使用接口来处理 IPv4/v6 事件的原因。比较确实是一个问题,因为我现在正在比较指针,但是根据我的错误检查,我会说空值并不是什么大问题