package pubsub type operation int const ( sub operation = iota subOnce pub tryPub unsub unsubAll closeTopic shutdown ) // PubSub is a collection of topics. type PubSub struct { cmdChan chan cmd capacity int } type cmd struct { op operation topics []string ch chan interface{} msg interface{} } // New creates a new PubSub and starts a goroutine for handling operations. // The capacity of the channels created by Sub and SubOnce will be as specified. func New(capacity int) *PubSub { ps := &PubSub{make(chan cmd), capacity} go ps.start() return ps } // Sub returns a channel on which messages published on any of // the specified topics can be received. func (ps *PubSub) Sub(topics ...string) chan interface{} { return ps.sub(sub, topics...) } // SubOnce is similar to Sub, but only the first message published, after subscription, // on any of the specified topics can be received. func (ps *PubSub) SubOnce(topics ...string) chan interface{} { return ps.sub(subOnce, topics...) } func (ps *PubSub) sub(op operation, topics ...string) chan interface{} { ch := make(chan interface{}, ps.capacity) ps.cmdChan <- cmd{op: op, topics: topics, ch: ch} return ch } // AddSub adds subscriptions to an existing channel. func (ps *PubSub) AddSub(ch chan interface{}, topics ...string) { ps.cmdChan <- cmd{op: sub, topics: topics, ch: ch} } // Pub publishes the given message to all subscribers of // the specified topics. func (ps *PubSub) Pub(msg interface{}, topics ...string) { ps.cmdChan <- cmd{op: pub, topics: topics, msg: msg} } // TryPub publishes the given message to all subscribers of // the specified topics if the topic has buffer space. func (ps *PubSub) TryPub(msg interface{}, topics ...string) { ps.cmdChan <- cmd{op: tryPub, topics: topics, msg: msg} } // Unsub unsubscribes the given channel from the specified // topics. If no topic is specified, it is unsubscribed // from all topics. func (ps *PubSub) Unsub(ch chan interface{}, topics ...string) { if len(topics) == 0 { ps.cmdChan <- cmd{op: unsubAll, ch: ch} return } ps.cmdChan <- cmd{op: unsub, topics: topics, ch: ch} } // Close closes all channels currently subscribed to the specified topics. // If a channel is subscribed to multiple topics, some of which is // not specified, it is not closed. func (ps *PubSub) Close(topics ...string) { ps.cmdChan <- cmd{op: closeTopic, topics: topics} } // Shutdown closes all subscribed channels and terminates the goroutine. func (ps *PubSub) Shutdown() { ps.cmdChan <- cmd{op: shutdown} } func (ps *PubSub) start() { reg := registry{ topics: make(map[string]map[chan interface{}]bool), revTopics: make(map[chan interface{}]map[string]bool), } loop: for cmd := range ps.cmdChan { if cmd.topics == nil { switch cmd.op { case unsubAll: reg.removeChannel(cmd.ch) case shutdown: break loop } continue loop } for _, topic := range cmd.topics { switch cmd.op { case sub: reg.add(topic, cmd.ch, false) case subOnce: reg.add(topic, cmd.ch, true) case tryPub: reg.sendNoWait(topic, cmd.msg) case pub: reg.send(topic, cmd.msg) case unsub: reg.remove(topic, cmd.ch) case closeTopic: reg.removeTopic(topic) } } } for topic, chans := range reg.topics { for ch := range chans { reg.remove(topic, ch) } } } // registry maintains the current subscription state. It's not // safe to access a registry from multiple goroutines simultaneously. type registry struct { topics map[string]map[chan interface{}]bool revTopics map[chan interface{}]map[string]bool } func (reg *registry) add(topic string, ch chan interface{}, once bool) { if reg.topics[topic] == nil { reg.topics[topic] = make(map[chan interface{}]bool) } reg.topics[topic][ch] = once if reg.revTopics[ch] == nil { reg.revTopics[ch] = make(map[string]bool) } reg.revTopics[ch][topic] = true } func (reg *registry) send(topic string, msg interface{}) { for ch, once := range reg.topics[topic] { ch <- msg if once { for topic := range reg.revTopics[ch] { reg.remove(topic, ch) } } } } func (reg *registry) sendNoWait(topic string, msg interface{}) { for ch, once := range reg.topics[topic] { select { case ch <- msg: if once { for topic := range reg.revTopics[ch] { reg.remove(topic, ch) } } default: } } } func (reg *registry) removeTopic(topic string) { for ch := range reg.topics[topic] { reg.remove(topic, ch) } } func (reg *registry) removeChannel(ch chan interface{}) { for topic := range reg.revTopics[ch] { reg.remove(topic, ch) } } func (reg *registry) remove(topic string, ch chan interface{}) { if _, ok := reg.topics[topic]; !ok { return } if _, ok := reg.topics[topic][ch]; !ok { return } delete(reg.topics[topic], ch) delete(reg.revTopics[ch], topic) if len(reg.topics[topic]) == 0 { delete(reg.topics, topic) } if len(reg.revTopics[ch]) == 0 { close(ch) delete(reg.revTopics, ch) } }