6

We have a following function:

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

This signature is given and can't be changed. There are around 20 different message types the handler processes.

Now, there are some of these messages (around 4) which need special post-processing. In a different package.

Thus, I am thinking to do this like this:

 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)
    }

Now, in the Process function, I want to quickly lookup if the message type is indeed of the ones we need postprocessing for. I don't want to do a switch again here. There are many handlers, in different packages, with varying amount of message types, and it should be generic.

So I was thinking of registering the message type in the postprocessor and then just do a lookup:

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

and then

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

This will all not work now because I can only "register" instances of the message, not the message type itself, as far as I know. Thus the map would only match a specific instance of a message, not its type, which is what I need.

So I guess this needs redesign. I can completely ditch the registering and the map lookup, but

  • I can't change the Handle function to a specific type (signature will need to remain message interface{}
  • I would like to avoid to have to use reflect, just because I will have a hard time defending such a solution with some colleagues.
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
transient_loop
  • 5,984
  • 15
  • 58
  • 117
  • 1
    I don't think there is a way to avoid using `reflect` if you want a map of types. By the way, when you make a map like this where only keys actually matter, I'd recommend using `struct{}` as the value type instead of using `interface{}`. In go, the idiomatic way to represent an empty and useless value is `struct{}{}`. – Ullaakut Aug 11 '18 at 14:46
  • IMO, the cleanest solution to your problem here is to simply call `Process` from `Handle` if the type is supported. You already know the type in `Handle` since you already are switching over it. – Ullaakut Aug 11 '18 at 14:49
  • 3
    I'm still a bit of a noob at Go, but _"some messages ... need post-processing"_ makes me think you should make `Postprocessor` an interface requiring a `Postprocess` method and only satisfy the interface on the types that need postprocessing. You'd then use something like `if p, ok := message.(Postprocessor); ok { p.Postprocess(...) }` to avoid the need for another type switch. This choice also allows you to customize post-processing behavior for specific types, so you need not deal with a concrete "registration" type. Satisfying the interface = registration of the type for postprocessing. –  Aug 11 '18 at 14:53
  • Very good thinking @ChronoKitsune! You should probably post this as an answer, pretty sure this is a good solution to his/her problem. – Ullaakut Aug 11 '18 at 15:13
  • Indeed I have already thought of @ChronoKitsune 's proposal as well. But it would be totally overkill to implement the `Process` method itself on every message type needed. Of course, I could just use a "marker" interface, e.g. `type Postprocess interface { NeedsPostprocess() bool }` This looks a bit silly to me, as every msg type would implement just the same `func (c *ConcreteMsg) NeedsPostprocess() bool { return true}`, but I think that's the best we can aim for – transient_loop Aug 11 '18 at 15:55
  • Would that work if I just embedded an implementation of `NeedsPostproces()` to every Msg type which needs postprocessing? I'll try a simple test to check. – transient_loop Aug 11 '18 at 16:00
  • That seems to be working! @ChronoKitsune please provide your comment as an answer, I will accept it, but also add the solution I came up with based on yours. Thanks! – transient_loop Aug 11 '18 at 16:17

2 Answers2

3

As there is no possibility to set a type as the map key, I finally decided to implement the following solution, which is based on @Chrono Kitsune 's solution:

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
     }         
}

As of my simple test I did, only Msg1 and Msg2 will be postprocessed, but not Msg3, which is what I wanted.

transient_loop
  • 5,984
  • 15
  • 58
  • 117
  • 1
    There is very much a way to use a Go type as a key in a map but you can't directly use an interface as a key. See my answer below https://stackoverflow.com/a/55321744/58961 with that said, I think you should stick with your solution as-is right now, it seems more reasonable in your case, but this was the first thing I stumbled upon when I googled, Go type as map key, so I fixed it. – John Leidegren Mar 24 '19 at 07:57
2

This question was the first hit I found on Google but the title is somewhat misleading. So I'll leave this here to add some food for thought with the title of the question in mind.

First, the issue with maps is that its key must be a comparable value. This is why for example a slice cannot be used is a map key. A slice is not comparable and is therefore not allowed. You can use an array (fixed sized slice) but not a slice for the same reason.

Second, you have in the reflect.TypeOf(...).String()a way to get a canonical string representation for types. Though it is not unambiguous unless you include the package path, as you can see here.

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

With this information, you can (if you feel so inclined) create a map which let's go from a reflect.Type to a key and back again, like this.

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

In the above case I'm assuming that N is small. Also note the use of the identity of reflect.TypeOf, it will return the same pointer for the same type on subsequent calls.

If N is not small, you may want to do something a bit more complex.

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

Note the subtitles of pointer to type, that, X and *X actually are different types.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
John Leidegren
  • 59,920
  • 20
  • 131
  • 152
  • 1
    I downvoted because you should simply use `reflect.Type` as the key of your map. The doc at https://pkg.go.dev/reflect#Type specifically mentions `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.` –  Sep 16 '21 at 16:51