Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle different types of messages - one or many channels?

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
}
  1. What's the functional difference between the two implementations? One thing is the ordering differences - only the first one guarantees that the messages (Message and OtherMessage) will be processed in the proper sequence.

  2. 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?

like image 926
Kos Avatar asked Mar 25 '15 20:03

Kos


People also ask

How many types of channel messages are there?

Communication channels can be categorized into three principal channels: (1) verbal, (2) written, and (3) non-verbal. Each of these communications channels have different strengths and weaknesses, and oftentimes we can use more than one channel at the same time.

What is the difference between channel and message?

The message brings together words to convey meaning, but is also about how it's conveyed — through nonverbal cues, organization, grammar, style, and other elements. “ The channel is the way in which a message or messages travel between source and receiver.” (McLean, 2005).

What is the channel of a message?

Message channels are broad message categories (for example, you might have a channel called "Promotional Emails"). Each message channel has a medium (such as email) and a type (either marketing or transactional). Every message channel has one or more message types.

What are the 4 different channels of communication?

4 Types of Communication: Verbal, Non-verbal, Written, Visual.


2 Answers

I would also go with option 'a': one channel only. You can enforce type correctness if you create a base message type (an interface) and both of the possible message types implement it (or if they are interfaces too, they can embed it).

Further advantage of the one-channel solution is that it is extensible. If now you want to handle a 3rd type of message, it's very easy to add it and to handle it. In case of the other: you would need a 3rd channel which if the number of message types increases soon becomes unmanageable and makes your code ugly. Also in case of multi channels, the select randomly chooses a ready channel. If messages come in frequently in some channels, others might starve even if only one message is in the channel and no more is coming.

like image 123
icza Avatar answered Oct 31 '22 15:10

icza


Answer to your questions first:

1) You got the major functional difference already, the ordering difference depending on how the channel is written to. There is also some differences in the implementation of how a channel of struct type versus interface type is implemented. Mostly, these are implementation details and don't change the nature of the majority of outcomes of using your code that much, but in the case where you're sending millions of messages, maybe this implementation detail will cost you.

2) I would say neither example you gave is more idiomatic than the other simply by reading your pseudocode, because whether you read from one channel or two has more to do with the semantics and requirements of your program (ordering, where the data is coming from, channel depth requirements, etc) than anything else. For example, What if one of the message types was a "stop" message to tell your processor to stop reading, or do something that could change the state of future messages processed? Maybe that would go on its own channel to make sure it doesn't get delayed by pending writes to the other channel.

And then you asked for possibly a better option?

One way to keep using a single channel and also keep from doing type checks is to instead send an enclosing type as the channel type:

type Message struct { /* ... */}
type OtherMessage struct { /* ... */}
type Wrap struct {
    *Message
    *OtherMessage
}

type MyProcess struct {
    in chan Wrap
}

func (foo *MyProcess) Start() {
    for msg := range foo.in {
        if msg.Message != nil {
            // do processing of message here
        }
        if msg.OtherMessage != nil {
            // process OtherMessage here
        }
    }
    // someone closed `in` - bye
}

An interesting side effect of struct Wrap is you can send both a Message and OtherMessage in the same channel message. It's up to you to decide whether this means anything or will happen at all.

One should note that if Wrap was going to grow beyond a handful of message types the cost of sending a wrap instance may actually be higher at some breakoff point (easy enough to benchmark) than simply sending an interface type and doing a type switch.

The other thing which you may want to look at, depending on the similarity between the types, is defining a non-empty interface where both Message and OtherMessage have that method receiver set; maybe it will contain functionality that will solve having to do a type switch at all.

Maybe you're reading messages to send them to a queuing library and all you really needed to get was:

interface{
    MessageID() string
    SerializeJSON() []byte
}

(I just made that up for illustration purposes)

like image 42
Crast Avatar answered Oct 31 '22 15:10

Crast