First, let’s look at an example of processing messages using broker.Broker .

Below we’ll explain the key parts of the code.

Function main

func main() {

// New Service

service := micro.NewService(

micro.Name("com.foo.broker.example"), // name the client service

) // Initialise service

service.Init(micro.AfterStart(func() error {

brk := service.Options().Broker

if err := brk.Connect(); err != nil {

log.Fatalf("Broker Connect error: %v", err)

}

go sub(brk)

go pub(brk)

return nil

})) service.Run()

}

First, create an instance of micro.Service with name com.foo.broker.example

with name Then initialize the service instance with the option micro.AfterStart . We can make ensure that the broker is ready only after the service is started.

instance with the option . We can make ensure that the broker is ready only after the service is started. In the callback function, we obtain the instance of broker.Broker , then pass it to function sub and pub , which are used to receive and send messages, respectively.

Function sub, subscribe messages

func sub(brk broker.Broker) {

// subscribe a topic with queue specified

_, err := brk.Subscribe(topic, func(p broker.Event) error {

fmt.Println("[sub] received message:", string(p.Message().Body), "header", p.Message().Header)

return nil

}, broker.Queue(topic))

if err != nil {

fmt.Println(err)

}

}

Pass in an instance of broker.Broker , then call its method Subscribe . The method signature is as follows:

type Event interface {

Topic() string

Message() *Message

Ack() error

} type Handler func(Event) error type Broker interface {

... Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error)



...

}

The first parameter is the subscription topic.

The second parameter is an event handler, which is a callback function that will be executed when a new event is received. The parameter of this handler is broker.Event from which the topic and message object *broker.Message can be fetched. In addition, this interface provides the method Ack for acknowledging manually (usually used with the DisableAutoAck option, which will be explained in the next paragraph).

The third parameter is broker.SubscribeOption , its meaning is consistent with server.SubscriberOption mentioned in the previous article. There three built-in subscription options in Micro, broker.DisableAutoAck, broker.Queue, broker.SubscribeContext , they are consistent with server.SubscriberOption , too. The difference is that any broker plugin can implement additional broker.SubscribeOption providing plugin-specific functionality.

For example, the RabbitMQ plugin provides an option named DurableQueue to control the persistence of a queue:

...

import "github.com/micro/go-plugins/broker/rabbitmq"

... _, err := broker.Subscribe(topic, func(p broker.Event) error {

...

p.Ack()

return nil

}, broker.Queue(topic), broker.DisableAutoAck(), rabbitmq.DurableQueue()，)

Note: when the option DisableAutoAck is used, we also call the Ack method to acknowledge manually.

As mentioned in the previous article, in addition to the RabbitMQ plugin, there are other broker plugins available such as Kafka plugin, NSQ plugin, etc. All these plugins and options can be used in the same way.

In this example we follow the best practices mentioned earlier: Always explicitly set the queue name. So we pass broker.Queue (topic) as the third parameter of the method.

So far the function sub has completed the message subscription. When a new message is received, a log of the message header and body will be output.

Function pub, publish the message

func pub(brk broker.Broker) {

i := 0

for range time.Tick(time.Second) {

// build a message

msg := &broker.Message{

Header: map[string]string{

"id": fmt.Sprintf("%d", i),

},

Body: []byte(fmt.Sprintf("%d: %s", i, time.Now().String())),

}

// publish it

if err := brk.Publish(topic, msg); err != nil {

log.Printf("[pub] failed: %v", err)

} else {

fmt.Println("[pub] pubbed message:", string(msg.Body))

}

i++

}

}

The core function in this code snippet is brk.Publish , and its signature is as follows:

type Message struct {

Header map[string]string

Body []byte

}

type Broker interface {

... Publish(topic string, msg *Message, opts ...PublishOption) error ...

}

The first parameter represents the message topic.

The second one is a message object, whose type is *broker.Message . Each message could contain multiple string headers and a byte slice as the body.

The third parameter is optional and its type is borker.PublishOption . It represents additional options that can be provided when sending a message. Micro has no built-in options, but a broker plugin could provide its own options, so these options are plugin-specific.

Take the RabbitMQ plug-in as an example, it provides the option DeliveryMode to control message persistence. We can use it in the following ways (the meaning of mode value comes from RabbitMQ document):

...

import "github.com/micro/go-plugins/broker/rabbitmq"

... const mode = 2 // Transient (0 or 1) or Persistent (2)

if err := broker.Publish(topic, msg, rabbitmq.DeliveryMode(mode)); err != nil {

log.Printf("[pub] failed: %v", err)

} else {

fmt.Println("[pub] pubbed message:", string(msg.Body))

}

...

All publishing options from broker plugins work the same as this.

After explaining the broker.Publish function, understanding the pub function becomes very easy.

Loop with time.Tick once a second. In each loop, create a *broker.Message and set a header named id for it, set the current time as the message body. then send it out.

Run it up

After preparing the code, we can launch the example program. Then we will see the following output with new logs appended every second:

$ go run main.go

2020-04-03 12:34:38 level=info Starting [service] com.foo.broker.example

2020-04-03 12:34:38 level=info Server [grpc] Listening on [::]:53616

2020-04-03 12:34:38 level=info Registry [mdns] Registering node: com.foo.broker.example-d70b788e-344b-45bd-ae6c-4d78d1bd7039

[pub] pubbed message: 0: 2020-04-03 12:34:39.170926 +0800 CST m=+1.152448710

[sub] received message: 0: 2020-04-03 12:34:39.170926 +0800 CST m=+1.152448710 header map[id:0]

[pub] pubbed message: 1: 2020-04-03 12:34:40.169054 +0800 CST m=+2.150609178

[sub] received message: 1: 2020-04-03 12:34:40.169054 +0800 CST m=+2.150609178 header map[id:1]

...

Although pub and sub run in the same process in this example, this is not mandatory, it is just for the convenience of the demonstration. Micro has encapsulated the underlying cross-process communication, and we can run them in separate programs without considering communication details.

Note: This example is derived from the official example. we didn’t use the original one because I don’t think the official example is appropriate and it can be misleading for developers. In contrast, you will find that our version is more consistent and simpler: micro.Service is the central at all times, there is no need to consider the command line parsing problem, and there is no need to handle the broker connection and initialization separately.

Conclusion

When Pub/Sub cannot meet your requirements, you can always turn to the broker.Broker to access the low-level API.

The so-called “low-level” means that we could gain more detailed control for the underly message broker system. broker.Broker is implemented by different broker plugins and adapts to different broker system, which gives us great flexibility.

broker.Broker and Pub/Sub together provide a complete development framework to Micro developers for asynchronous message processing. It’s a really smart and elegant design.

To be continued.