Getting started
Install it
go get github.com/AceMQ-Company/acemq-go-amqpThe module needs Go 1.23 or newer.
Two imports
import (
acemq "github.com/AceMQ-Company/acemq-go-amqp/amqp"
_ "github.com/AceMQ-Company/acemq-go-amqp/rabbitmq"
)The named import is the library. The blank one registers the
amqp and amqps URL schemes with it — without
it, Connect will tell you exactly that:
acemq: no transport is registered for "amqp"; known schemes are [memory].
For a broker, add the blank import
_ "github.com/AceMQ-Company/acemq-go-amqp/rabbitmq"
Keeping the transport behind an import is what stops a program that
only uses memory:// from linking an AMQP client it never
calls.
Connect
mq, err := acemq.Connect(ctx, "amqp://guest:guest@localhost:5672/")
if err != nil {
return err
}
defer mq.Close()Close stops every consumer on the connection first and
lets handlers already running finish, so a message being worked on is
acknowledged rather than returned to the queue for somebody else to
redo.
Options go on the end:
mq, err := acemq.Connect(ctx, url,
acemq.WithOrigin("orders@"+hostname),
acemq.WithRetry(acemq.ExponentialRetry(5, time.Second, time.Minute)),
acemq.WithPrefetch(50))WithOrigin is worth setting. The default is
acemq@{hostname}, which names the machine but not the
service, and the first time you are reading a dead-letter queue at three
in the morning you will want to know which service published the
thing.
Declare what you use
if err := mq.DeclareQueue(ctx, "orders"); err != nil {
return err
}Durable by default: the queue survives a broker restart. Declaring is
idempotent — unless the queue already exists with different
settings, in which case RabbitMQ refuses with
PRECONDITION_FAILED and that refusal is passed straight on.
It means the code and the broker disagree about what the queue is, which
is worth stopping for rather than papering over.
See exchanges, queues and bindings for the rest.
Define a message
type OrderPlaced struct {
OrderID string `json:"orderId"`
TotalCents int64 `json:"totalCents"`
}Tag the fields. The tag is what makes a Go field and
a Java field the same field. Without one the name goes on the wire
capitalised — OrderID rather than orderId —
and the Java consumer reads nothing.
Publish
pub := acemq.NewPublisher[OrderPlaced](mq, "", "orders")
if err := pub.Send(ctx, OrderPlaced{OrderID: "o-1", TotalCents: 4250}); err != nil {
return err
}The empty exchange is the default exchange, which routes to the queue whose name matches the routing key. A publisher is cheap to keep and safe for concurrent use: build one per message type at start-up, not one per message.
More in publishing.
Consume
sub, err := acemq.Consume(ctx, mq, "orders",
func(ctx context.Context, m acemq.Message[OrderPlaced]) acemq.Ack {
if err := place(ctx, m.Payload); err != nil {
return acemq.Retry(err)
}
return acemq.Accept()
})
if err != nil {
return err
}
defer sub.Close()The handler returns its decision rather than calling a method, so a handler that forgets to decide does not compile. A message nobody acknowledges sits unacknowledged until the connection drops, and then comes back — usually to the same handler, with the same outcome.
More in consuming.
Run it without a broker
Every URL beginning memory:// is an in-process broker,
and each distinct URL is a separate one:
mq, err := acemq.Connect(ctx, "memory://"+t.Name())Nothing else in the code changes. See testing.
A whole program
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
acemq "github.com/AceMQ-Company/acemq-go-amqp/amqp"
_ "github.com/AceMQ-Company/acemq-go-amqp/rabbitmq"
)
type OrderPlaced struct {
OrderID string `json:"orderId"`
TotalCents int64 `json:"totalCents"`
}
func main() {
ctx := context.Background()
mq, err := acemq.Connect(ctx, "amqp://guest:guest@localhost:5672/",
acemq.WithOrigin("orders@example"),
acemq.WithRetry(acemq.ExponentialRetry(5, time.Second, time.Minute)))
if err != nil {
log.Fatal(err)
}
defer mq.Close()
if err := mq.DeclareQueue(ctx, "orders"); err != nil {
log.Fatal(err)
}
sub, err := acemq.Consume(ctx, mq, "orders",
func(ctx context.Context, m acemq.Message[OrderPlaced]) acemq.Ack {
log.Printf("order %s, attempt %d", m.Payload.OrderID, m.Envelope.Attempt)
return acemq.Accept()
})
if err != nil {
log.Fatal(err)
}
defer sub.Close()
pub := acemq.NewPublisher[OrderPlaced](mq, "", "orders")
if err := pub.Send(ctx, OrderPlaced{OrderID: "o-1", TotalCents: 4250}); err != nil {
log.Fatal(err)
}
// Wait for a signal, then let the deferred Close drain what is in flight.
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
}A broker to point it at:
docker run -d -p 5672:5672 rabbitmq:4-alpine