Published on

Embedding NATS in Go

6 min read

Authors
banner

In this article, we will look at how we can embed NATS server within our Go application. After that, we will also do a little performance benchmark between embedded and external service.

Note: As always, all the code will be available here

Why do we need this?

architecture

While running nats server using cli or docker container is usually the preferred way but in some instances, it can be unnecessary, one such example is testing. While testing, it's often cumbersome to spin up new instances for external services, this can be completely avoided by using an in-memory server. Luckily, NATS server package provides this functionality out of the box!

Setup

Before writing any code, let's quickly set up our go project.

Go module

$ go mod init example
$ touch main.go

Dependencies

$ go get -d github.com/nats-io/nats-server/v2
$ go get github.com/nats-io/nats.go

Code

After the setup, we can now start by initializing a new server with options

opts := &server.Options{}
ns, err := server.NewServer(opts)

if err != nil {
	panic(err)
}

Note: You can configure things like Host, Port, Authorization, and much more using server.Options.

Next, we will start the server via goroutine and wait for the server to be ready for connections

go ns.Start()

if !ns.ReadyForConnections(4 * time.Second) {
	panic("not ready for connection")
}

Once our server is ready, we can connect to it with nats.go client using ClientURL function given by the server.

nc, err := nats.Connect(ns.ClientURL())

if err != nil {
	panic(err)
}

Let's subscribe to a subject, and print message data. Optionally, we can call Shutdown function to stop the nats server.

subject := "my-subject"

nc.Subscribe(subject, func(msg *nats.Msg) {
	data := string(msg.Data)
	fmt.Println(data)
	ns.Shutdown()
})

Finally, we will publish the data to our subject and call WaitForShutdown to keep our server running until shutdown.

nc.Publish(subject, []byte("Hello embedded NATS!"))
ns.WaitForShutdown()

Our complete example should look like this!

package main

import (
	"fmt"
	"time"

	"github.com/nats-io/nats-server/v2/server"
	"github.com/nats-io/nats.go"
)

func main() {
	opts := &server.Options{}

	// Initialize new server with options
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

	// Start the server via goroutine
	go ns.Start()

	// Wait for server to be ready for connections
	if !ns.ReadyForConnections(4 * time.Second) {
		panic("not ready for connection")
	}

	// Connect to server
	nc, err := nats.Connect(ns.ClientURL())

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Subscribe to the subject
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Print message data
		data := string(msg.Data)
		fmt.Println(data)

		// Shutdown the server (optional)
		ns.Shutdown()
	})

	// Publish data to the subject
	nc.Publish(subject, []byte("Hello embedded NATS!"))

	// Wait for server shutdown
	ns.WaitForShutdown()
}

Build and Run!

Let's build and run our example binary with an embedded NATS server.

$ go build
$ ./example
Hello embedded NATS!

As we can see, we get the output from our subscriber!

Performance

Performance is an important aspect of every application, so let's compare the performance for using NATS as an embedded or external service (cli, docker etc). We will run a benchmark for 1 million messages for 8 intervals.

Setup

First, we'll need to start the external nats server

$ nats-server
[5868] 2022/02/22 20:09:21.073386 [INF] Starting nats-server
[5868] 2022/02/22 20:09:21.073657 [INF]   Version:  2.7.0
[5868] 2022/02/22 20:09:21.073660 [INF]   Git:      [not set]
[5868] 2022/02/22 20:09:21.073662 [INF]   Name:     NCFHZUA6H6YRJHE65OXRW5X5NL2XDTR7Q4NBZG5Q2KEHZTFN6JMUK4HU
[5868] 2022/02/22 20:09:21.073665 [INF]   ID:       NCFHZUA6H6YRJHE65OXRW5X5NL2XDTR7Q4NBZG5Q2KEHZTFN6JMUK4HU
[5868] 2022/02/22 20:09:21.076236 [INF] Listening for client connections on 0.0.0.0:4222
[5868] 2022/02/22 20:09:21.076659 [INF] Server is ready

Results

Benchmark code can be found here, and can be run as below.

$ go run benchmark/benchmark.go
Results have been saved to results.html

Seems like there is not much difference in performance, that's really impressive considering we are testing for millions of messages.

benchmark

Conclusion

In this article, we learned how we can embed nats server in our go applications. And also did a small performance benchmark. Make sure to checkout the official docs.

© 2022 Karan Pratap Singh