Consuming from multiple queue - go

I have a rabbit mq instance that I use to listen to changes. However, I have different messages that I need to consume. What I am not sure about now is how to go about it. Would I need to just keep duplicating my Consume method and what is the danger of doing that if any.
type Consumer struct {
conn *amqp.Connection
db *mongo.Database
queueName string
}
func (consumer *Consumer) setup() error {
_, err := consumer.conn.Channel()
if err != nil {
return err
}
return nil
}
// NewConsumer returns a new Consumer
func NewConsumer(conn *amqp.Connection, db *mongo.Database) (Consumer, error) {
consumer := Consumer{
conn: conn,
db: db,
}
return consumer, nil
}
// Listen will listen for all new Queue publications
// and print them to the console.
func (consumer *Consumer) Listen(topics []string) error {
ch, err := consumer.conn.Channel()
if err != nil {
return err
}
defer ch.Close()
msgs, err := ch.Consume("charge.get", "", true, false, false, false, nil)
if err != nil {
return err
}
forever := make(chan bool)
go func() {
for msg := range msgs {
switch msg.RoutingKey {
case "charge.get":
worker.Refund(paymentRepo.NewPaymentRepository(consumer.db), msg.Body)
}
// acknowledege received event
log.Printf("Received a message: %s ROUTIG KEY %s", msg.Body, msg.RoutingKey)
}
}()
log.Printf("[*] Waiting for message [Exchange, Queue][%s, %s]. To exit press CTRL+C", " getExchangeName()", "q.Name")
<-forever
return nil
}
Do I just add another msgs, err := ch.Consume("charge.update", "", true, false, false, false, nil) below the first one to consume from this queue? Or how can I go about this.

Related

Rabbit consumer connected, but after a while cannot receive message from queue

I have a code which is successfully connected and consume messages from RabbitMQ.
But after a while, consumers cannot receive messages however its connected while this issue happen.
package rabbitmq
import (
"context"
"fmt"
"os"
"runtime"
"time"
"github.com/getsentry/sentry-go"
log "github.com/sirupsen/logrus"
"github.com/streadway/amqp"
)
type RabbitMQ struct {
conn *amqp.Connection
queues map[string]amqp.Queue
connString string
rabbitCloseError chan *amqp.Error
recoveryConsumer []RecoveryConsumer
// ch *amqp.Channel
// exchange_name string
}
type RecoveryConsumer struct {
queueName string
routingKey string
handler func(d amqp.Delivery)
concurrency int8
}
type (
Delivery = amqp.Delivery
)
func (r *RabbitMQ) IfExist(queueName string) bool {
for _, item := range r.recoveryConsumer {
if item.queueName == queueName {
return false
}
}
return true
}
func (r *RabbitMQ) RecoverConsumers() {
for _, i := range r.recoveryConsumer {
go r.StartConsumer(i.queueName, i.routingKey, i.handler, int(i.concurrency))
log.Infof("Consumer for %v successfully recovered", i.queueName)
}
}
func (r *RabbitMQ) Reconnector() {
for { //nolint
select {
case err := <-r.rabbitCloseError:
log.Errorf("[RabbitMQ] Connection Closed : {'Reason': '%v', 'Code': '%v', 'Recoverable': '%v', 'Server_Side': '%v'", err.Reason, err.Code, err.Recover, err.Server)
log.Debug("Reconnecting after connection closed")
sentry.CaptureException(fmt.Errorf("[RabbitMQ] Connection Closed : {'Reason': '%v', 'Code': '%v', 'Recoverable': '%v', 'Server_Side': '%v'", err.Reason, err.Code, err.Recover, err.Server))
r.connection()
r.RecoverConsumers()
}
}
}
func (r *RabbitMQ) Connect(host string, user string, pass string, virthost string) {
r.connString = "amqp://" + user + ":" + pass + "#" + host + "/"
if virthost != "/" || len(virthost) > 0 {
r.connString += virthost
}
r.connection()
go r.Reconnector()
}
func (r *RabbitMQ) connection() {
if r.conn != nil {
if !r.conn.IsClosed() {
return
} else {
log.Info("Reconnecting to RabbitMQ...")
}
}
var err error
r.conn, err = amqp.Dial(r.connString)
if err != nil {
sentry.CaptureException(err)
log.Fatalf("%s: %s", "Failed to connect to RabbitMQ", err)
}
r.conn.Config.Heartbeat = 5 * time.Second
r.queues = make(map[string]amqp.Queue)
r.rabbitCloseError = make(chan *amqp.Error)
r.conn.NotifyClose(r.rabbitCloseError)
log.Debug("[RabbitMQ] Successfully connected to RabbitMQ")
log.Infof("Number of Active Thread/Goroutine %v", runtime.NumGoroutine())
}
func (r *RabbitMQ) CreateChannel() *amqp.Channel {
ch, err := r.conn.Channel()
if err != nil {
log.Error(err)
return nil
}
return ch
}
func (r *RabbitMQ) QueueAttach(ch *amqp.Channel, name string) {
q, err := ch.QueueDeclare(
name, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
log.Fatalf("%s: %s", "Failed to declare a queue", err)
}
r.queues[name] = q
// r.ch.ExchangeDeclare()
}
func (r *RabbitMQ) TempQueueAttach(ch *amqp.Channel, name string) {
_, err := ch.QueueDeclare(
name, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
ch.Close()
log.Fatalf("%s: %s", "Failed to declare a temporary queue", err)
sentry.CaptureException(fmt.Errorf("%s: %s", "Failed consume message", err))
}
}
func (r *RabbitMQ) Publish(ch *amqp.Channel, queue string, body []byte) {
span := sentry.StartSpan(context.TODO(), "publish message")
defer span.Finish()
err := ch.Publish(
"", // exchange
r.queues[queue].Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
Headers: map[string]interface{}{},
ContentType: "application/json",
ContentEncoding: "",
DeliveryMode: amqp.Persistent,
Priority: 0,
CorrelationId: "",
ReplyTo: "",
Expiration: "",
MessageId: "",
Timestamp: time.Now().UTC(),
Type: "",
UserId: "",
AppId: "",
Body: body,
})
if err != nil {
sentry.CaptureException(err)
log.Fatalf("%s: %s", "Failed to publish a message", err)
}
log.Debugf("Send message: %s", string(body))
}
func (r *RabbitMQ) StartConsumer(queueName string, routingKey string, handler func(d amqp.Delivery), concurrency int) {
// prefetch 4x as many messages as we can handle at once
ok := r.IfExist(queueName)
if ok {
r.recoveryConsumer = append(r.recoveryConsumer, RecoveryConsumer{
queueName: queueName,
routingKey: routingKey,
handler: handler,
concurrency: int8(concurrency),
})
}
ch, err := r.conn.Channel()
if err != nil {
log.Error(err)
}
prefetchCount := concurrency * 1
err = ch.Qos(prefetchCount, 0, false)
if err != nil {
sentry.CaptureException(err)
log.Errorf("%s: %s", "Failed QOS", err)
}
r.QueueAttach(ch, queueName)
msgs, err := ch.Consume(
queueName, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
sentry.CaptureException(err)
log.Fatalf("%s: %s", "Failed consume message", err)
sentry.CaptureException(fmt.Errorf("%s: %s", "Failed consume message", err))
os.Exit(1)
}
go func() {
for msg := range msgs {
handler(msg)
}
}()
}
func (r *RabbitMQ) WaitMessage(ch *amqp.Channel, queueName string, timeout time.Duration) []byte {
st := time.Now()
for time.Since(st).Seconds() < 1 {
msg, ok, err := ch.Get(queueName, true)
if err != nil {
log.Errorf("Can't consume queue. Error: %s", err.Error())
sentry.CaptureException(err)
return nil
}
if ok {
return msg.Body
}
time.Sleep(50 * time.Millisecond)
}
return nil
}
What can be reason for this ?
I know that it should be in Rabbit side, but client library cannot shows any error.....
Because after start to work, consume continue to listen and work successfully.
The only thing I can advice you to try is to use heartbeats. This will detect whether a connection is dead due to for example a network fail.
You can take a look on it here: https://www.rabbitmq.com/heartbeats.html.
I'm not sure about this one, long time since I used it, but if you put a try catch around the receiving messages bit, it might show up in the catch when the connection dies.
I hope this helps you a little for solving your problem.

golang rabbitmq channel.consume SIGSEGV

Folks,
Am trying to write a wrapper library for the rabbitmq implementation. For some strange reason, I am not able to consume from an existing queue.
msgs, err := w.AMQP.Channel.Consume( is causing:
2019-10-14T13:58:56.462-0400 info worker/worker.go:27 [Initializing NewWorker]
2019-10-14T13:58:56.462-0400 debug worker/worker.go:43 Starting Worker
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x14d024e]
The docs example I followed is simple, and the interface to the rabbitmq driver is simple. I dont see why im getting a invalid memory address error.
My implementation is identical to how the article lays it out, for reference, here is all the code i have:
// StartConsumingTlcFHVDrivers subscribes to queue and starts to do it's job
func (w *Worker) StartConsuming() {
queueName := w.options.Rabbit.AMQP.QueueName
w.logger.Debugf("Starting %s Worker", queueName)
msgs, err := w.AMQP.Channel.Consume(
queueName,
"",
false,
false,
false,
false,
nil,
)
if err != nil {
logger.Fatalf("Could not register consumer, err:%s", err)
}
forever := make(chan bool)
go func() {
for d := range msgs {
w.logger.Infof("Received a message: %s", d.Body)
}
}()
w.logger.Infof("Waiting for messages on %s", queueName)
<-forever
}
Before I finish the wrapper with my own interface, im setting up the client this way:
import ""github.com/streadway/amqp""
type Client struct {
options Options
logger ilogger.Logger
metrics imetrics.Client
Channel amqp.Channel
}
func NewRabbitClient(logger ilogger.Logger, metrics imetrics.Client, options Options) *Client {
var err error
conn, err := amqp.Dial(options.AMQPConnectionURL)
if err != nil {
logger.Fatalf("%s: %s", "Can't connect to AMQP", err)
}
defer conn.Close()
// create a dedicated channel per queue
channel, err := conn.Channel()
if err != nil {
logger.Fatalf("%s: %s", "Can't create a amqpChannel", err)
}
defer channel.Close()
// declare an Exchange
err = channel.ExchangeDeclare(options.ExchangeName, options.ExchangeType, true, false, false, false, nil)
// declare a Queue
_, err = channel.QueueDeclare(options.QueueName, true, false, false, false, nil)
if err != nil {
logger.Fatalf("Could not declare %s queue, err:%s", options.QueueName, err)
}
// bind a Queue to the Exchange
err = channel.QueueBind(options.QueueName, "", options.ExchangeName, false, nil)
// Qos on the Channel
err = channel.Qos(1, 0, false)
if err != nil {
logger.Fatalf("Could not configure QoS, err:%s", err)
}
return &Client{
metrics: metrics,
logger: logger,
options: options,
Channel: *channel,
}
}
The error is now obvious to me. In the constructor function, do not close the connection/channel. i.e.
conn, err := amqp.Dial(options.AMQPConnectionURL)
if err != nil {
logger.Fatalf("%s: %s", "Can't connect to AMQP", err)
}
instead of
conn, err := amqp.Dial(options.AMQPConnectionURL)
if err != nil {
logger.Fatalf("%s: %s", "Can't connect to AMQP", err)
}
defer conn.Close()
```

How to return a channel

I'm creating a worker to consume messages from a RabitMQ queue. To achieve that, I created the following file named queue.go
package ExternalServices
import (
"../domain"
"encoding/json"
"github.com/streadway/amqp"
"os"
)
const (
catalogQueue = "catalog-queue"
)
func EnqueueMessageCatalog(catalog *domain.Catalog) error {
marshal, err := json.Marshal(*catalog)
if err != nil {
return err
}
jsonVal := string(marshal)
err = enqueue(catalogQueue, jsonVal)
return err
}
func DequeueMessageCatalog() ([]domain.Catalog, error) {
msgs, err := dequeue(catalogQueue)
if err != nil {
return nil, err
}
allCatalogs := make([]domain.Catalog, len(msgs))
for _, currMsg := range msgs {
var currCatalog domain.Catalog
err = json.Unmarshal([]byte(currMsg), &currCatalog)
if err != nil {
return nil, err
}
}
return allCatalogs, nil
}
func openConnection() (*amqp.Connection, *amqp.Channel, error) {
conn, err := amqp.Dial(os.Getenv("RabbitMQConStr"))
if err != nil {
return nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
conn.Close()
return nil, nil, err
}
return conn, ch, nil
}
func ensureQueueExists(queueName string, ch *amqp.Channel) (amqp.Queue, error) {
q, err := ch.QueueDeclare(
queueName, // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
return q, err
}
func enqueue(queueName string, message string) error {
con, ch, err := openConnection()
if err != nil {
return err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return err
}
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: []byte(message),
})
return err
}
func dequeue(queueName string) ([]string, error) {
con, ch, err := openConnection()
if err != nil {
return nil, err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return nil, err
}
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
return nil, err
}
jsons := make([]string, len(msgs))
i := 0
for currMsg:= range msgs {
jsons[i] = string(currMsg.Body)
i += 1
}
return jsons, nil
}
However, I got a bit confused at the dequeue function. I want my worker to be notified every time a messages arrives at my queue, so I guess the proper way to do so is to create a string chan to my worker, since I don't want to expose the message channel returned by Consume to it.
This is my worker so far.
package worker
import (
"../external-services"
"log"
)
func StartWorker() {
go func() {
messages, err := ExternalServices.DequeueMessageCatalog();
if err != nil {
// todo log
}
for d := range messages {
log.Printf("Received a message: %s", d)
}
}()
}
How can I modify my dequeue function so it returns a string chan?
After modifying this method to return the string chan, do the lines defer con.Close() and defer ch.Close() need to be deleted from this method?
It's my first project in GoLang so anything you think can increase the quality of the code will be much appreciated :-D
maybe something like this:
msgs, err := ch.Consume(...)
/* handle error */
stringCh := make(chan string)
done := make(chan struct{})
go func() {
defer con.Close()
defer ch.Close()
defer close(stringCh)
for {
select {
case currMsg := <-msgs:
stringCh <- string(currMsg.Body)
case <-done:
return
}
}
}()
return stringCh, done
This is only a sketchy example. Basic idea is spawn another goroutine listen to the message chan returned by Consume. Other details like how to graceful shutdown, dequeue interface,... depend on your needs.
After reading #YSTai response, I realised I miss the go routine creation. This is how my code end up.
worker.go
package main
import (
"../domain"
"../externalservices"
"log"
"strings"
"sync"
)
/*
StartWorker initializes a program that will wait for messages enqueued and process them
*/
func StartWorker() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
catalogReceived := make(chan domain.Catalog)
defer close(catalogReceived)
for true {
go func() {
externalservices.DequeueMessageCatalog(catalogReceived)
catalog := <-catalogReceived
website := domain.Website{
Name: strings.ToUpper(catalog.Name),
Zip: catalog.Zip}
externalservices.InsertWebSite(&website)
}()
}
}()
log.Printf(" [*] Waiting for messages")
wg.Wait()
}
func main() {
StartWorker()
}
queue.go
package externalservices
import (
"../domain"
"encoding/json"
"github.com/streadway/amqp"
"os"
)
const (
catalogQueue = "catalog-queue"
)
func EnqueueMessageCatalog(catalog *domain.Catalog) error {
marshal, err := json.Marshal(*catalog)
if err != nil {
return err
}
jsonVal := string(marshal)
err = enqueue(catalogQueue, jsonVal)
return err
}
// DequeueMessageCatalog is nice
func DequeueMessageCatalog(messageChannel chan domain.Catalog) {
message := make(chan []byte)
defer close(message)
for true {
go func() {
dequeue(catalogQueue, message)
}()
currCatalog := domain.Catalog{}
json.Unmarshal([]byte(<-message), &currCatalog)
messageChannel <- currCatalog
}
}
func openConnection() (*amqp.Connection, *amqp.Channel, error) {
connString := os.Getenv("RabbitMQConStr")
conn, err := amqp.Dial(connString)
if err != nil {
return nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
conn.Close()
return nil, nil, err
}
return conn, ch, nil
}
func ensureQueueExists(queueName string, ch *amqp.Channel) (amqp.Queue, error) {
q, err := ch.QueueDeclare(
queueName, // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
return q, err
}
func enqueue(queueName string, message string) error {
con, ch, err := openConnection()
if err != nil {
return err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return err
}
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: []byte(message),
})
return err
}
func dequeue(queueName string, message chan []byte) error {
con, ch, err := openConnection()
if err != nil {
return err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return err
}
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
true, // no-wait
nil, // args
)
if err != nil {
return err
}
for currMsg := range msgs {
message <- currMsg.Body
}
return nil
}

Unable to consume messages from locally running Kafka server, using Golang Sarama Package

I am making a simple Telegram bot that would read messages from a local Kafka server and print it out to a chat.
Both zookeeper and kafka server config files are at their defaults. Console consumer works. The problem rises when I try to consume messages from code using Golang Sarama package. Before I added these lines:
case err := <-pc.Errors():
log.Panic(err)
the program only printed the messages once, after which it would stall.
Now it panics prinitng this to the log:
kafka: error while consuming test1/0: kafka: broker not connected
Here's the code:
type kafkaResponse struct {
telega *tgbotapi.Message
message []byte
}
type kafkaRequest struct {
telega *tgbotapi.Message
topic string
}
var kafkaBrokers = []string{"localhost:9092"}
func main() {
//channels for request response
var reqChan = make(chan kafkaRequest)
var respChan = make(chan kafkaResponse)
//starting kafka client routine to listen to topic channnel
go consumer(reqChan, respChan, kafkaBrokers)
//bot thingy here
bot, err := tgbotapi.NewBotAPI(token)
if err != nil {
log.Panic(err)
}
bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := bot.GetUpdatesChan(u)
for {
select {
case update := <-updates:
if update.Message == nil {
continue
}
switch update.Message.Text {
case "Topic: test1":
topic := "test1"
reqChan <- kafkaRequest{update.Message, topic}
}
case response := <-respChan:
bot.Send(tgbotapi.NewMessage(response.telega.Chat.ID, string(response.message)))
}
}
here's the consumer.go:
func consumer(reqChan chan kafkaRequest, respChan chan kafkaResponse, brokers []string) {
config := sarama.NewConfig()
config.Consumer.Return.Errors = true
// Create new consumer
consumer, err := sarama.NewConsumer(brokers, config)
if err != nil {
panic(err)
}
defer func() {
if err := consumer.Close(); err != nil {
panic(err)
}
}()
select {
case request := <-reqChan:
//get all partitions on the given topic
partitionList, err := consumer.Partitions(request.topic)
if err != nil {
fmt.Println("Error retrieving partitionList ", err)
}
initialOffset := sarama.OffsetOldest
for _, partition := range partitionList {
pc, _ := consumer.ConsumePartition(request.topic, partition, initialOffset)
go func(pc sarama.PartitionConsumer) {
for {
select {
case message := <-pc.Messages():
respChan <- kafkaResponse{request.telega, message.Value}
case err := <-pc.Errors():
log.Panic(err)
}
}
}(pc)
}
}
}
You are closing your consumer after setting up all the PartitionConsumers in the code
defer func() {
if err := consumer.Close(); err != nil {
panic(err)
}
}()
However, the documentation specifies that you should only close the consumer after all the PartitionConsumers have been closed.
// Close shuts down the consumer. It must be called after all child
// PartitionConsumers have already been closed.
Close() error
I would recommend you add a sync.WaitGroup to the function go func(pc sarama.PartitionConsumer) {

Could one connection support multiple channels in go api for rabbitmq?

package main
import (
"fmt"
"github.com/streadway/amqp"
"time"
)
// Every connection should declare the topology they expect
func setup(url, queue string) (*amqp.Connection, *amqp.Channel, error) {
//setup connection
conn, err := amqp.Dial(url)
if err != nil {
return nil, nil, err
}
//build channel in the connection
ch, err := conn.Channel()
if err != nil {
return nil, nil, err
}
//queue declare
if _, err := ch.QueueDeclare(queue, false, true, false, false, nil); err != nil {
return nil, nil, err
}
return conn, ch, nil
}
func main() {
//amqp url
url := "amqp://guest:guest#127.0.0.1:5672";
for i := 1; i <= 2; i++ {
fmt.Println("connect ", i)
//two goroutine
go func() {
//queue name
queue := fmt.Sprintf("example.reconnect.%d", i)
//setup channel in the tcp connection
_, pub, err := setup(url, queue)
if err != nil {
fmt.Println("err publisher setup:", err)
return
}
// Purge the queue from the publisher side to establish initial state
if _, err := pub.QueuePurge(queue, false); err != nil {
fmt.Println("err purge:", err)
return
}
//publish msg
if err := pub.Publish("", queue, false, false, amqp.Publishing{
Body: []byte(fmt.Sprintf("%d", i)),
}); err != nil {
fmt.Println("err publish:", err)
return
}
//keep running
for{
time.Sleep(time.Second * 20)
}
}()
}
//keep running
for {
time.Sleep(time.Second * 20)
}
}
I thought there is only one connection between the program and mq-server,
but there are two connection,one connection can only support one channel,why?
can't the two goroutine share the same tcp connection?
Socket descriptor can share in all threads of a process in the theory.
Why the two goroutine don't share one socket but have their own channel?
The model by hand:
The real model in rabbitmq:
Looking at the source for the library it appears as though you can call conn.Channel() as many times as you like and it creates a new stream of communication over the same connection.
Ok, I tried it, here's a working example... One goroutine, one connection, two channels
I setup the receiver, then send a message, then read from the receiver channel
if you wanted multiple queue's bound in one goroutine, you would call rec.Consume twice and then select across the queues.
package main
import (
"fmt"
"github.com/streadway/amqp"
"os"
)
func main() {
conn, err := amqp.Dial("amqp://localhost")
e(err)
defer conn.Close()
fmt.Println("Connected")
rec, err := conn.Channel()
e(err)
fmt.Println("Setup receiver")
rq, err := rec.QueueDeclare("go-test", false, false, false, false, nil)
e(err)
msgs, err := rec.Consume(rq.Name, "", true, false, false, false, nil)
e(err)
fmt.Println("Setup sender")
send, err := conn.Channel()
e(err)
sq, err := send.QueueDeclare("go-test", false, false, false, false, nil)
e(err)
fmt.Println("Send message")
err = send.Publish("", sq.Name, false, false, amqp.Publishing{
ContentType: "text/plain",
Body: []byte("This is a test"),
})
e(err)
msg := <-msgs
fmt.Println("Received from:", rq, "msg:", string(msg.Body))
}
func e(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Output on my box:
$ go run rmq.go
Connected
Setup receiver
Setup sender
Send message
Received from: {go-test 0 0} msg: This is a test

Resources