Consider this simple code:
type Message struct { /* ... */ }
type MyProcess struct {
in chan Message
}
func (foo *MyProcess) Start() {
for msg := range foo.in {
// handle `msg`
}
// someone closed `in` - bye
}
I'd like to change MyProcess to support 2 different kinds of messages. I have 2 ideas:
a) Type switch
type Message struct { /* ... */ }
type OtherMessage struct { /* ... */ }
type MyProcess struct {
in chan interface{} // Changed signature to something more generic
}
func (foo *MyProcess) Start() {
for msg := range foo.in {
switch msg := msg.(type) {
case Message:
// handle `msg`
case OtherMessage:
// handle `msg`
default:
// programming error, type system didn't save us this time.
// panic?
}
}
// someone closed `in` - bye
}
b) Two channels
type Message struct { /* ... */ }
type OtherMessage struct { /* ... */ }
type MyProcess struct {
in chan Message
otherIn chan OtherMessage
}
func (foo *MyProcess) Start() {
for {
select {
case msg, ok := <-foo.in:
if !ok {
// Someone closed `in`
break
}
// handle `msg`
case msg, ok := <-foo.otherIn:
if !ok {
// Someone closed `otherIn`
break
}
// handle `msg`
}
}
// someone closed `in` or `otherIn` - bye
}
What's the functional difference between the two implementations? One thing is the ordering differences - only the first one guarantees that the messages (
Message
andOtherMessage
) will be processed in the proper sequence.Which one is more idiomatic? The approach 'a' is shorter but doesn't enforce message type correctness (one could put anything in the channel). The approach 'b' fixes this, but has more boilerplate and more space for human error: both channels need to be checked for closedness (easy to forget) and someone needs to actually close both of them (even easier to forget).
Long story short I'd rather use 'a' but it doesn't leverage the type system and thus feels ugly. Maybe there is an even better option?