Skip to content

Getting started

By the end of this you will have a NATS server running inside your program, a client connected to it without touching the network, and a message going from one to the other.

Nothing needs installing. No broker, no container, no port.

Install

go get gitlab.com/phpboyscout/go/nats

The whole thing

This uses the constructors directly. A real service uses Register and lets go/controls run the lifecycle — see embed a server.

package main

import (
    "context"
    "fmt"
    "log/slog"
    "time"

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

    gonats "gitlab.com/phpboyscout/go/nats"
    "gitlab.com/phpboyscout/go/nats/client"
    "gitlab.com/phpboyscout/go/nats/server"
)

func main() {
    ctx := context.Background()
    log := slog.Default()

    // A server with no listener at all — not even loopback.
    srv, err := server.New(ctx, gonats.ServerSettings{InProcessOnly: true})
    if err != nil {
        panic(err)
    }

    if err := server.Start(log, srv)(ctx); err != nil {
        panic(err)
    }
    defer server.Stop(log, srv)(ctx)

    // A client that reaches it through memory rather than a socket.
    cli, err := client.Connect(ctx, gonats.ClientSettings{Name: "tutorial"}, nil,
        client.InProcess(srv))
    if err != nil {
        panic(err)
    }
    defer cli.Close(ctx)

    got := make(chan string, 1)

    if _, err := cli.Subscribe("greetings", func(m *nats.Msg) {
        got <- string(m.Data)
    }); err != nil {
        panic(err)
    }

    if err := cli.Publish(ctx, "greetings", []byte("hello")); err != nil {
        panic(err)
    }

    select {
    case msg := <-got:
        fmt.Println("received:", msg)
    case <-time.After(time.Second):
        fmt.Println("nothing arrived")
    }
}

Run it and you get received: hello.

What just happened

server.New built a real NATS server — the same one you would deploy standalone — and InProcessOnly told it not to open a listener. At v0.1.0 that is the only kind this module builds; anything with a host, port or route is refused, because there is no TLS or authentication configuration to protect it with yet.

server.Start ran it and waited for it to accept connections, so a server that never becomes ready fails there rather than at your first publish.

client.InProcess(srv) took an in-memory connection from the server rather than dialling an address. No TCP is involved, not even a loopback hop.

cli.Subscribe bounded the subscription's queue. Left alone, NATS allows 500,000 pending messages per subscription, which is a limit in the sense that the heat death of the universe is a deadline.

cli.Publish is the module's own publish path rather than cli.Raw().Publish. It is the one place header propagation, metrics and deadlines can live, and using it from the start means you never have to go back and find every call site. ctx here carries no deadline, so it behaves as NATS does and returns once the client has buffered; give it one and it waits for the server to acknowledge.

cli.Close(ctx) waited for the drain to finish. nats.Conn.Drain() on its own returns immediately, which would let the process exit while handlers were still running.

Next