AceMQ for Go

A Go client for AceMQ messaging over AMQP, speaking the same wire contract as the Java and .NET libraries: the same reserved headers, the same defaults, the same retry semantics. A Go consumer reads what a Java producer writes.

That claim is pinned rather than asserted. The fixtures under internal/testdata/ were generated by the Java implementation and are carried byte for byte by all five libraries. One holds the headers: a test reads each case and writes it back, and no header is gained, lost or renamed. The other holds the retry schedule, the queue names and the topology, down to the thirty rows that decide whether a wait is spent here or on the broker. A port that hand-copies a wire contract from documentation acquires a difference nobody notices until two languages disagree in production — which is exactly how Java came to multiply its backoff by five while everything else doubled, for ten releases, with every suite green. See how we know the five libraries agree.

go get github.com/AceMQ-Company/acemq-go-amqp

A message, end to end

import (
    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"`
}

mq, err := acemq.Connect(ctx, "amqp://guest:guest@localhost:5672/",
    acemq.WithRetry(acemq.ExponentialRetry(5, time.Second, time.Minute)))
if err != nil {
    return err
}
defer mq.Close()

if err := mq.DeclareQueue(ctx, "orders"); err != nil {
    return err
}

pub := acemq.NewPublisher[OrderPlaced](mq, "", "orders")
if err := pub.Send(ctx, OrderPlaced{OrderID: "o-1", TotalCents: 4250}); err != nil {
    return err
}

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()
    })
defer sub.Close()

Getting started takes this apart line by line, and the tutorials build a working service in four steps.

What it does for you

An envelope on every message. Identity, type, correlation, causation, attempt count, origin and the time it was first published — the same fields the Java and .NET libraries write, so a message crossing between them keeps its history. See the envelope.

Retries that stop. A handler returns Retry, and a policy decides whether there is another attempt and how long to wait. The attempt count comes from the broker's redelivery flag rather than the header on the wire, because a broker requeues the bytes it was given and that header never moves. See retries and redelivery.

A broker you do not need. memory:// is built in, so tests exercise real publishing, consuming, acknowledgement and retry without Docker. It is deliberately no kinder than RabbitMQ. See testing.

Security you have to opt out of. TLS with a named authority, credentials read fresh on every connection, and development certificates that are refused unless you say otherwise — on every path, including the insecure one. See security.

A core with no dependencies. Nothing outside the standard library, including the UUIDs. The AMQP client lives behind a transport registry in a subpackage, so a program that only uses memory:// never compiles it in, and every format beyond JSON is a module of its own.

Connections that come back. A dropped connection is redialled, the topology redeclared and every consumer reattached — because the alternative is a service that silently stops consuming and says nothing. See retries and redelivery.

The layout

The module is a package per concern, with nothing at its root:

.../acemq-go-amqp/amqp envelopes, codecs, publishing, consuming, retry, the in-memory transport
.../acemq-go-amqp/rabbitmq the RabbitMQ transport, on github.com/rabbitmq/amqp091-go
.../acemq-go-amqp/security TLS modes, trusted authorities, credentials
.../acemq-go-amqp/patterns request-reply, idempotency, outbox, ordering, pipelines, replay, routing slips, claim check, sagas, delayed delivery, streams, consumer groups, schema registry, SQL stores
.../acemq-go-amqp/actuator metrics, health and info over HTTP
.../acemq-go-amqp/crypto encrypted message bodies
.../acemq-go-amqp/codec/... XML, YAML, TOML, Protocol Buffers, Avro — a module each
.../acemq-go-amqp/devcerts development certificates, behind the acemq-certs command

The package in amqp/ is named acemq, so the import carries the name:

import acemq "github.com/AceMQ-Company/acemq-go-amqp/amqp"

The directory says what the package is; the name says whose it is. It also keeps the module root free for the packages that will sit beside these two, and acemq.Connect reads better at every call site than amqp.Connect would.

The blank import is what registers the amqp and amqps schemes:

import _ "github.com/AceMQ-Company/acemq-go-amqp/rabbitmq"

The same arrangement database/sql uses for its drivers, and for the same reason: the program says which broker it talks to, and nothing else is linked in.

One difference from Java and .NET

NewPublisher and Consume are functions taking the connection rather than methods on it, because a method cannot have its own type parameter before Go 1.27:

func (c *Conn) Publisher[T any](...)   // generic method: needs go1.27
func NewPublisher[T any](c *Conn, ...) // generic function: works everywhere

This module declares go 1.23. For a library, working with the toolchains people actually have matters more than mirroring the exact shape of another language's API. Publisher[T] is a generic type with ordinary methods, which has no such restriction, so once you hold one it reads much as it does elsewhere.

Where to go next

Status

Pre-1.0 and in development. The API may change before 1.0.