5

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?

4

2 回答 2

6

我也会选择“a”选项:只有一个频道。如果您创建一个基本消息类型 (an interface) 并且两种可能的消息类型都实现了它(或者如果它们也是接口,它们可以嵌入它),那么您可以强制执行类型正确性。

单通道解决方案的另一个优点是它是可扩展的。如果现在您要处理第三种类型的消息,添加和处理它非常容易。如果是另一种情况:您将需要第三个通道,如果消息类型的数量很快增加,它将变得难以管理并使您的代码变得丑陋。同样在多通道的情况下,select随机选择一个就绪通道。如果消息在某些频道中频繁出现,即使频道中只有一条消息并且没有更多消息,其他人也可能会饿死。

于 2015-03-25T21:15:04.423 回答
3

先回答你的问题:

1)您已经获得了主要的功能差异,排序差异取决于通道的写入方式。在实现结构类型和接口类型的通道的实现方式上也存在一些差异。大多数情况下,这些是实现细节,并不会改变使用代码的大多数结果的性质,但是在您发送数百万条消息的情况下,也许这个实现细节会让您付出代价。

2)我想说,你给出的任何一个例子都比另一个简单地阅读你的伪代码更惯用,因为你是从一个通道还是两个通道读取更多的是与你的程序的语义和要求有关(排序,数据来自哪里来自,通道深度要求等)比其他任何东西。例如,如果其中一种消息类型是“停止”消息,告诉您的处理器停止阅读,或者做一些可以改变未来处理消息状态的事情怎么办?也许这会在它自己的通道上进行,以确保它不会因挂起写入另一个通道而延迟。

然后您要求可能有更好的选择?

继续使用单个通道并避免进行类型检查的一种方法是发送封闭类型作为通道类型:

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
}

struct Wrap 的一个有趣的副作用是您可以同时发送 aMessageOtherMessagein 同一频道消息。由您决定这是否意味着任何事情或是否会发生。

应该注意的是,如果Wrap要超出少数消息类型,则发送包装实例的成本实际上可能在某个中断点(足够容易进行基准测试)比简单地发送接口类型和进行类型切换更高。

根据类型之间的相似性,您可能想要查看的另一件事是定义一个非空接口,其中 Message 和 OtherMessage 都设置了该方法接收器;也许它将包含完全解决必须进行类型切换的功能。

也许您正在阅读消息以将它们发送到排队库,而您真正需要的是:

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

(我只是为了说明目的而编造的)

于 2015-03-25T21:27:45.103 回答