Skip to content

Getting started

By the end of this you will have sent a CloudEvent between two halves of one program and read every attribute back at the other end. It needs go/nats for the transport, and nothing else.

1. Install

$ go get gitlab.com/phpboyscout/go/cloudevents

2. Build an event

package main

import (
    "time"

    "gitlab.com/phpboyscout/go/cloudevents"
)

func order() cloudevents.Event {
    return cloudevents.Event{
        ID:              "01JQ8Z3F7K2M4N6P8R0T2V4W6X",
        Source:          "/phpbotscout/ingest",
        Type:            "uk.phpboyscout.orders.created",
        Subject:         "order-4172",
        Time:            time.Now().UTC(),
        DataContentType: "application/json",
        Data:            []byte(`{"order":"4172"}`),
    }
}

SpecVersion is deliberately absent. There is exactly one value this codec can write, so Marshal fills it in rather than making every caller repeat it.

ID and Source together identify the occurrence, and a producer must not reuse the pair. A ULID or a UUID is the usual choice.

3. Marshal it

headers, body, err := cloudevents.Marshal(order())
if err != nil {
    return err
}

headers is a map[string][]string holding seven keys — ce-id, ce-source, ce-specversion, ce-type, ce-subject, ce-time and content-type, with the body carrying the payload untouched. Values are percent-encoded, so read them back with Unmarshal rather than by hand.

http.Header.Get will not find them

Keys are written in lower case, which is the NATS binding's form. http.Header.Get canonicalises the key it is given, so hdr.Get("Ce-Id") returns "" against this map. Read it by iteration or by exact key, and let net/http canonicalise on the wire. Marshal returns an error rather than producing something no consumer would accept, so

a missing Type is caught in your process rather than in somebody else's subscriber.

4. Send it

nats.Header is map[string][]string, so the conversion is a conversion and not a copy:

import "github.com/nats-io/nats.go"

msg := &nats.Msg{
    Subject: "orders.created",
    Header:  nats.Header(headers),
    Data:    body,
}

if err := conn.PublishMsg(msg); err != nil {
    return err
}

5. Read it at the other end

sub, err := client.Subscribe(ctx, "orders.created", func(msg *nats.Msg) {
    e, err := cloudevents.Unmarshal(msg.Header, msg.Data)
    if err != nil {
        // e still carries whatever could be read — log e.ID, not "a message failed".
        logger.Error("refused a CloudEvent", "id", e.ID, "source", e.Source, "err", err)

        return
    }

    logger.Info("received", "type", e.Type, "subject", e.Subject)
})

That error branch is the part worth pausing on. Unmarshal refuses a malformed event, and hands back the attributes it did manage to read — because core NATS has no dead letter, so a dropped message is gone unless something recorded which one it was. See Handle a refused event.

Where next