英文:
Handle different types of messages - one or many channels?
问题
考虑以下简单的代码:
type Message struct { /* ... */ }
type MyProcess struct {
in chan Message
}
func (foo *MyProcess) Start() {
for msg := range foo.in {
// 处理 `msg`
}
// 有人关闭了 `in` - 再见
}
我想将MyProcess更改为支持两种不同类型的消息。我有两个想法:
a) 类型切换
type Message struct { /* ... */ }
type OtherMessage struct { /* ... */ }
type MyProcess struct {
in chan interface{} // 将签名更改为更通用的类型
}
func (foo *MyProcess) Start() {
for msg := range foo.in {
switch msg := msg.(type) {
case Message:
// 处理 `msg`
case OtherMessage:
// 处理 `msg`
default:
// 编程错误,类型系统这次没有帮助我们。
// 报错?
}
}
// 有人关闭了 `in` - 再见
}
b) 两个通道
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 {
// 有人关闭了 `in`
break
}
// 处理 `msg`
case msg, ok := <-foo.otherIn:
if !ok {
// 有人关闭了 `otherIn`
break
}
// 处理 `msg`
}
}
// 有人关闭了 `in` 或 `otherIn` - 再见
}
-
这两种实现之间的功能区别是什么?一个区别是顺序的不同 - 只有第一种实现能够保证消息(
Message
和OtherMessage
)按正确的顺序处理。 -
哪种实现更符合惯用法?方法
a
更短,但不强制执行消息类型的正确性(可以在通道中放入任何内容)。方法b
修复了这个问题,但有更多的样板代码和更多的人为错误的空间:需要检查两个通道是否关闭(容易忘记),并且需要确保两个通道都被关闭(更容易忘记)。
长话短说,我更愿意使用方法a
,但它没有充分利用类型系统,因此感觉不太美观。也许还有更好的选择?
英文:
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?
答案1
得分: 6
我也会选择选项'a':只有一个通道。如果你创建一个基本的消息类型(一个interface
),并且两种可能的消息类型都实现它(或者如果它们也是接口,它们可以嵌入它),你可以强制类型正确性。
一个通道解决方案的进一步优势是它是可扩展的。如果现在你想处理第三种类型的消息,添加和处理它非常容易。而在另一种情况下,你需要一个第三个通道,如果消息类型的数量增加,很快就变得难以管理,使你的代码变得丑陋。而且在多通道的情况下,select
会随机选择一个就绪的通道。如果某些通道频繁地接收消息,其他通道可能会饿死,即使只有一个消息在通道中,也没有更多的消息到来。
英文:
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.
答案2
得分: 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 {
// 在这里处理消息
}
if msg.OtherMessage != nil {
// 在这里处理OtherMessage
}
}
// 有人关闭了`in`通道 - 再见
}
Wrap结构体的一个有趣的副作用是你可以在同一个通道消息中发送Message
和OtherMessage
。你可以自行决定这是否有意义或是否会发生。
需要注意的是,如果Wrap
要增加到多个消息类型,那么发送Wrap实例的成本可能会在某个临界点(可以进行简单的基准测试)上超过发送接口类型并进行类型切换的成本。
另一件你可能需要考虑的事情是,根据类型之间的相似性,定义一个非空接口,其中Message和OtherMessage都具有该方法接收器;也许它包含的功能将解决完全不需要进行类型切换的问题。
也许你正在读取消息以将它们发送到一个排队库,而你真正需要的只是:
interface{
MessageID() string
SerializeJSON() []byte
}
(我只是为了说明而编写的)
英文:
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)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论