【问题标题】:struct type as map key结构类型作为映射键
【发布时间】:2019-01-18 21:40:57
【问题描述】:

我们有以下函数:

func (h *Handler) Handle(message interface{}) error {
    //here there is a switch for different messages
    switch m := message.(type) {
    }
}

此签名已给出且无法更改。处理程序处理大约 20 种不同的消息类型。

现在,其中一些消息(大约 4 条)需要特殊的后处理。在不同的包中。

因此,我想这样做:

 func (h *Handler) Handle(message interface{}) error {
        //here there is a switch for different messages

        switch m := message.(type) {
        }
        //only post-process if original message processing succeeds
        postProcessorPkg.Process(message)
    }

现在,在Process 函数中,我想快速查找消息类型是否确实属于我们需要后处理的类型。我不想在这里再次发送switch。有许多处理程序,在不同的包中,具有不同数量的消息类型,并且应该是通用的。

所以我想在后处理器中注册消息类型,然后进行查找:

func (p *Postprocessor) Register(msgtype interface{}) {
     registeredTypes[msgtype] = msgtype
}

然后

func (p *Postprocessor) Process(msgtype interface{}) error {
     if ok := registeredTypes[msgtype]; !ok {
        return errors.New("Unsupported message type")
     }
     prop := GetProp(registeredTypes[msgtype])
     doSmthWithProp(prop)
}

现在这一切都行不通了,因为据我所知,我只能“注册”消息的实例,而不是消息类型本身。因此,地图只会匹配消息的特定实例,而不是它的类型,这正是我所需要的。

所以我想这需要重新设计。我可以完全放弃注册和地图查找,但是

  • 我无法将Handle 函数更改为特定类型(签名需要保留message interface{}
  • 我想避免不得不使用reflect,因为我很难与一些同事捍卫这样的解决方案。

【问题讨论】:

  • 如果您想要类型映射,我认为没有办法避免使用reflect。顺便说一句,当你制作这样一个只有键真正重要的映射时,我建议使用struct{} 作为值类型,而不是使用interface{}。在 go 中,表示空值和无用值的惯用方式是 struct{}{}
  • IMO,解决您的问题的最干净的方法是在支持该类型的情况下从Handle 调用Process。你已经知道Handle 中的类型,因为你已经在切换它了。
  • 我还是个 Go 菜鸟,但是 "some messages ... need post-processing" 让我觉得你应该让 Postprocessor 成为一个接口需要Postprocess 方法并且只满足需要后处理的类型的接口。然后,您可以使用 if p, ok := message.(Postprocessor); ok { p.Postprocess(...) } 之类的东西来避免需要其他类型的开关。此选择还允许您自定义特定类型的后处理行为,因此您无需处理具体的“注册”类型。满足接口=注册类型进行后处理。
  • 非常好的想法@ChronoKitsune!您可能应该将此作为答案发布,很确定这是解决他/她问题的好方法。
  • 确实我也已经想到了@ChronoKitsune 的提议。但是在需要的每种消息类型上实现Process 方法本身是完全过分的。当然,我可以只使用“标记”界面,例如type Postprocess interface { NeedsPostprocess() bool } 这在我看来有点傻,因为每个 msg 类型都会实现相同的 func (c *ConcreteMsg) NeedsPostprocess() bool { return true},但我认为这是我们可以达到的最佳目标

标签: go types interface go-map


【解决方案1】:

由于无法将类型设置为映射键,因此我最终决定实现以下解决方案,该解决方案基于@Chrono Kitsune 的解决方案:

type Postprocess interface {
    NeedsPostprocess() bool
}

type MsgWithPostProcess struct {}

func (p *MsgWithPostProcess) NeedsPostprocess() bool {
  return true
}

type Msg1 struct {
   MsgWithPostProcess
   //other stuff
}

type Msg2 struct {
    MsgWithPostProcess
    //other stuff
}

type Msg3 struct {
    //no postprocessing needed
}

func (p *Postprocessor) Process(msgtype interface{}) error {
     if _, ok := msgtype.(Postprocess); ok {
        //do postprocessing
     }         
}

在我做的简单测试中,只有Msg1Msg2 会被后处理,而不是Msg3,这正是我想要的。

【讨论】:

  • 有很多方法可以使用 Go 类型作为 map 中的键,但不能直接使用接口作为键。请参阅下面stackoverflow.com/a/55321744/58961 的回答,我认为您现在应该坚持使用您的解决方案,这在您的情况下似乎更合理,但这是我在谷歌搜索时偶然发现的第一件事,Go type as map关键,所以我修好了。
【解决方案2】:

这个问题是我在 Google 上找到的第一个热门问题,但标题有点误导。因此,我将把这个留在这里,以补充一些思考问题的标题。

首先,地图的问题在于它的键必须是一个可比较的值。这就是为什么例如不能使用切片是映射键的原因。切片是不可比较的,因此是不允许的。出于同样的原因,您可以使用数组(固定大小的切片)但不能使用切片。

其次,您可以在reflect.TypeOf(...).String()a 中获取类型的规范字符串表示。尽管除非您包含包路径,否则它并不是明确的,正如您在此处看到的那样。

package main

import (
    "fmt"
    s2 "go/scanner"
    "reflect"
    s1 "text/scanner"
)

type X struct{}

func main() {
    fmt.Println(reflect.TypeOf(1).String())
    fmt.Println(reflect.TypeOf(X{}).String())
    fmt.Println(reflect.TypeOf(&X{}).String())
    fmt.Println(reflect.TypeOf(s1.Scanner{}).String())
    fmt.Println(reflect.TypeOf(s2.Scanner{}).String())
    fmt.Println(reflect.TypeOf(s1.Scanner{}).PkgPath(), reflect.TypeOf(s1.Scanner{}).String())
    fmt.Println(reflect.TypeOf(s2.Scanner{}).PkgPath(), reflect.TypeOf(s2.Scanner{}).String())
}
int
main.X
*main.X
scanner.Scanner
scanner.Scanner
text/scanner scanner.Scanner
go/scanner scanner.Scanner

https://play.golang.org/p/NLODZNdik6r

有了这些信息,您可以(如果您愿意的话)创建一个地图,让我们从 reflect.Type 到一个键然后再返回,就像这样。

package main

import (
    "fmt"
    s2 "go/scanner"
    "reflect"
    s1 "text/scanner"
)

type TypeMap struct {
    m []reflect.Type
}

func (m *TypeMap) Get(t reflect.Type) int {
    for i, x := range m.m {
        if x == t {
            return i
        }
    }
    m.m = append(m.m, t)
    return len(m.m) - 1
}

func (m *TypeMap) Reverse(t int) reflect.Type {
    return m.m[t]
}

type X struct{}

func main() {
    var m TypeMap

    fmt.Println(m.Get(reflect.TypeOf(1)))
    fmt.Println(m.Reverse(0))

    fmt.Println(m.Get(reflect.TypeOf(1)))
    fmt.Println(m.Reverse(0))

    fmt.Println(m.Get(reflect.TypeOf(1)))
    fmt.Println(m.Reverse(0))

    fmt.Println(m.Get(reflect.TypeOf(X{})))
    fmt.Println(m.Reverse(1))

    fmt.Println(m.Get(reflect.TypeOf(&X{})))
    fmt.Println(m.Reverse(2))

    fmt.Println(m.Get(reflect.TypeOf(s1.Scanner{})))
    fmt.Println(m.Reverse(3).PkgPath(), m.Reverse(3))

    fmt.Println(m.Get(reflect.TypeOf(s2.Scanner{})))
    fmt.Println(m.Reverse(4).PkgPath(), m.Reverse(4))
}
0
int
0
int
0
int
1
main.X
2
*main.X
3
text/scanner scanner.Scanner
4
go/scanner scanner.Scanner

在上述情况下,我假设N 很小。还要注意reflect.TypeOf的标识的使用,它会在后续调用中返回相同类型的相同指针。

如果 N 不小,你可能想做一些更复杂的事情。

package main

import (
    "fmt"
    s2 "go/scanner"
    "reflect"
    s1 "text/scanner"
)

type PkgPathNum struct {
    PkgPath string
    Num     int
}

type TypeMap struct {
    m map[string][]PkgPathNum
    r []reflect.Type
}

func (m *TypeMap) Get(t reflect.Type) int {
    k := t.String()

    xs := m.m[k]

    pkgPath := t.PkgPath()
    for _, x := range xs {
        if x.PkgPath == pkgPath {
            return x.Num
        }
    }

    n := len(m.r)
    m.r = append(m.r, t)
    xs = append(xs, PkgPathNum{pkgPath, n})

    if m.m == nil {
        m.m = make(map[string][]PkgPathNum)
    }
    m.m[k] = xs

    return n
}

func (m *TypeMap) Reverse(t int) reflect.Type {
    return m.r[t]
}

type X struct{}

func main() {
    var m TypeMap

    fmt.Println(m.Get(reflect.TypeOf(1)))
    fmt.Println(m.Reverse(0))

    fmt.Println(m.Get(reflect.TypeOf(X{})))
    fmt.Println(m.Reverse(1))

    fmt.Println(m.Get(reflect.TypeOf(&X{})))
    fmt.Println(m.Reverse(2))

    fmt.Println(m.Get(reflect.TypeOf(s1.Scanner{})))
    fmt.Println(m.Reverse(3).PkgPath(), m.Reverse(3))

    fmt.Println(m.Get(reflect.TypeOf(s2.Scanner{})))
    fmt.Println(m.Reverse(4).PkgPath(), m.Reverse(4))
}
0
int
1
main.X
2
*main.X
3
text/scanner scanner.Scanner
4
go/scanner scanner.Scanner

https://play.golang.org/p/2fiMZ8qCQtY

注意类型指针的字幕,X*X 实际上是不同的类型。

【讨论】:

  • 我投了反对票,因为您应该简单地使用 reflect.Type 作为地图的键。 pkg.go.dev/reflect#Type 的文档特别提到了Type values are comparable, such as with the == operator, so they can be used as map keys. Two Type values are equal if they represent identical types.
猜你喜欢
  • 1970-01-01
  • 2011-11-12
  • 2023-03-30
  • 1970-01-01
  • 2012-02-24
  • 2020-06-20
  • 2011-11-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多