---
id: 8f79af5c6af89b748112aa8b77b348ab
author: prravda
title: Configuração de servidor NATS embarcado em aplicações Go e estrutura de comunicação em processo
description: Aborda a configuração para executar um broker NATS (nats-server/v2) incorporado dentro de um processo Go, além do projeto de comunicação em memória baseada em InProcessConn e de uma interface de consumidor definida pelo usuário.
language: pt
date: 2025-08-04T01:05:22.150350794Z
path: /blog/posts/how-embedded-nats-communicate-with-go-application-z36089af0
category: network
---

# Primeiros passos

## Sobre o NATS

> Aplicações e serviços de software precisam trocar dados. O NATS é uma infraestrutura que permite essa troca de dados, segmentada na forma de mensagens. Chamamos isso de "**message oriented middleware**" (middleware orientado a mensagens).
>
> Com o NATS, desenvolvedores de aplicações podem:
>
> - Construir aplicações cliente-servidor distribuídas e escaláveis de forma simples.
> - Armazenar e distribuir dados em tempo real de maneira geral. Isso pode ser alcançado de forma flexível em diversos ambientes, linguagens, provedores de nuvem e sistemas on-premises.
>
> [What is NATS, NATS docs](https://docs.nats.io/nats-concepts/what-is-nats)

- O NATS é um message broker construído em Go.

## NATS incorporado

> Se sua aplicação está em Go, e se isso se adequa ao seu caso de uso e aos cenários de implantação, você pode até incorporar um servidor NATS dentro da sua aplicação.

[Embedding NATS, NATS docs](https://docs.nats.io/running-a-nats-service/clients#embedding-nats)

>

- Há também uma característica peculiar do NATS: no caso de aplicações construídas em Go, ele oferece suporte ao embedded mode.
- Ou seja, em vez da forma comum dos message brokers, que consiste em iniciar um servidor de broker separado e comunicar-se com ele por meio dos clientes da aplicação, é possível embutir o próprio broker (embed) na aplicação desenvolvida em Go.

## Benefícios e casos de uso do NATS incorporado

- Como existe um [vídeo do Youtube](https://youtu.be/cdTrl8UfcBo?si=KYcXOQpiyLUft6AN) bem elaborado sobre o tema, o link do vídeo substitui uma explicação adicional.
- Mesmo sem implantar um servidor de message broker separado, é possível criar uma modular monolith application e alcançar separate of concern, aproveitando a vantagem de embutir o NATS como embedded. Além disso, torna-se possível a single binary deployment.
- Ele pode ser usado de forma útil não apenas em platform with no network (wasm), mas também em offline-first application.

# Exemplo da documentação oficial

```go
package main

import (
    "fmt"
    "time"

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

func main() {
    // Opções do servidor
    opts := &server.Options{}

    // Inicializa um novo servidor com as opções
    ns, err := server.NewServer(opts)

    if err != nil {
        panic(err)
    }

    // Inicia o servidor via goroutine
    go ns.Start()

    // Aguarda até que o servidor esteja pronto para conexões
    if !ns.ReadyForConnections(4 * time.Second) {
        panic("not ready for connection")
    }

    // Conecta ao servidor
    nc, err := nats.Connect(ns.ClientURL())

    if err != nil {
        panic(err)
    }

    subject := "my-subject"

    // Assina o subject
    nc.Subscribe(subject, func(msg *nats.Msg) {
        // Imprime os dados da mensagem
        data := string(msg.Data)
        fmt.Println(data)

        // Desliga o servidor (opcional)
        ns.Shutdown()
    })

    // Publica dados no subject
    nc.Publish(subject, []byte("Hello embedded NATS!"))

    // Aguarda o desligamento do servidor
    ns.WaitForShutdown()
}
```

- Este é o [exemplo de Embedded NATS](https://dev.to/karanpratapsingh/embedding-nats-in-go-19o) apresentado na documentação oficial do NATS, mas, seguindo o código de exemplo como está, a comunicação não ocorre no embedding mode.

```bash
Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:34:20
                                                                     in 0.017s (0)
...
tcp4       0      0  127.0.0.1.4222         127.0.0.1.63769        TIME_WAIT
```

- Ao executar o arquivo go com `go run .` enquanto se monitoram, pelo comando `watch 'netstat -an | grep 127.0.0.1'`, as redes que trafegam por [localhost](http://localhost)(127.0.0.1), observa-se que surgem novas requisições de rede originadas na porta padrão do NATS, `4222`.

# Configurações corretas para o embedding mode

- Para se comunicar no embedded mode conforme pretendido, são necessárias duas opções como segue.
  - Client: é preciso incluir a opção `InProcessServer`.
  - Server: é preciso declarar explicitamente a flag `DontListen` como `true` em `Server.Options`.
- Essas partes não estão oficialmente documentadas, e o início dessa funcionalidade pode ser identificado por meio deste [PR](https://github.com/nats-io/nats-server/pull/2360).

  > Este PR adiciona três coisas:
  >
  > 1. A função `InProcessConn()` em `Server`, que constrói um `net.Pipe` para obter uma conexão com o servidor NATS sem usar sockets TCP
  > 2. A opção `DontListen`, que instrui o servidor NATS a não escutar no listener TCP habitual
  > 3. Um canal `startupComplete`, que é fechado logo antes de iniciarmos o `AcceptLoop`, e `readyForConnections` aguardará por ele
  >
  > A principal motivação é que temos uma aplicação que pode executar tanto em modo monolith (processo único) quanto em modo polylith (múltiplos processos). Gostaríamos de usar o NATS nos dois modos por simplicidade, mas o modo monolith precisa atender a diversas plataformas onde abrir conexões por socket não faz sentido (mobile) ou simplesmente não é possível (WASM). Essas mudanças permitirão usar o NATS inteiramente em processo.
  >
  > Um PR complementar [nats-io/nats.go#774](https://github.com/nats-io/nats.go/pull/774) adiciona suporte no lado do cliente.
  >
  > Este é meu primeiro PR para este projeto, então peço desculpas antecipadamente caso eu tenha deixado passar algo óbvio.
  >
  > /cc @nats-io/core

# Exemplo funcional para o embedded mode

```go
package main

import (
	"fmt"
	"time"

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

func main() {
	opts := &server.Options{
		// Para configurar o servidor NATS embutido
		// defina DonListen como true
		DontListen: true,
	}

	// Inicializa um novo servidor com as opções
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

	// Inicia o servidor via goroutine
	go ns.Start()

	// Aguarda até que o servidor esteja pronto para conexões
	if !ns.ReadyForConnections(10 * time.Second) {
		panic("not ready for connection")
	}

	// Conecta ao servidor via conexão in-process
	nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Assina o subject
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Imprime os dados da mensagem
		data := string(msg.Data)
		fmt.Println(data)

		// Desliga o servidor (opcional)
		ns.Shutdown()
	})

	// Publica dados no subject
	nc.Publish(subject, []byte("Hello embedded NATS!"))

	// Aguarda o desligamento do servidor
	ns.WaitForShutdown()
}

```

```bash
Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:37:50
                                                                     in 0.023s (0)
...no additional logs

```

- Agora é possível observar que nenhum hop de rede adicional ocorre, conforme pretendido.

# Por dentro do funcionamento

## TL;DR

![diagram1](/assets/images/embedded-nats/seq1.svg)

- Este é o diagrama de sequência que mostra quais funções são executadas internamente e como funcionam quando o código é executado a partir de `main.go`. Em linhas gerais, os pontos principais são os seguintes.
  - Com `DontListen: true`, o servidor omite a fase de escuta de clientes denominada `AcceptLoop`.
  - Se a opção `InProcessServer` do Connect do client estiver ativada, é criada uma conexão em memória, um pipe é construído via `net.Pipe` e a extremidade do pipe é retornada ao client como um tipo `net.Conn`.
  - Client e server realizam a comunicação in-process por essa conexão.

## Servidor

### AcceptLoop

```go
// nats-server/server/server.go

// Aguarda os clientes.
if !opts.DontListen {
	s.AcceptLoop(clientListenReady)
}
```

- Primeiramente, quando `DontListen` é true, a fase de escuta de clientes denominada `AcceptLoop` é omitida.

```go
// nats-server/server/server.go

// AcceptLoop é exportada para facilitar os testes.
func (s *Server) AcceptLoop(clr chan struct{}) {
	// Se saíssemos antes de o listener ser configurado corretamente,
	// garanta que o canal seja fechado.
	defer func() {
		if clr != nil {
			close(clr)
		}
	}()

	if s.isShuttingDown() {
		return
	}

	// Captura um snapshot das opções do servidor.
	opts := s.getOpts()

	// Configura o estado que permite o shutdown
	s.mu.Lock()
	hp := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))
	l, e := natsListen("tcp", hp)
	s.listenerErr = e
	if e != nil {
		s.mu.Unlock()
		s.Fatalf("Error listening on port: %s, %q", hp, e)
		return
	}
	s.Noticef("Listening for client connections on %s",
		net.JoinHostPort(opts.Host, strconv.Itoa(l.Addr().(*net.TCPAddr).Port)))

	// Alerta que TLS está habilitado.
	if opts.TLSConfig != nil {
		s.Noticef("TLS required for client connections")
		if opts.TLSHandshakeFirst && opts.TLSHandshakeFirstFallback == 0 {
			s.Warnf("Clients that are not using \"TLS Handshake First\" option will fail to connect")
		}
	}

	// Se o servidor foi iniciado com RANDOM_PORT (-1), opts.Port seria igual
	// a 0 no início desta função. Por isso, precisamos obter a porta real
	if opts.Port == 0 {
		// Grava a porta resolvida de volta nas opções.
		opts.Port = l.Addr().(*net.TCPAddr).Port
	}

	// Agora que a porta foi definida (caso tenha sido RANDOM), configura o
	// Host/Port de INFO do servidor com os valores de Options ou
	// ClientAdvertise.
	if err := s.setInfoHostPort(); err != nil {
		s.Fatalf("Error setting server INFO with ClientAdvertise value of %s, err=%v", opts.ClientAdvertise, err)
		l.Close()
		s.mu.Unlock()
		return
	}
	// Mantém o registro das URLs de conexão dos clientes, que podem ser necessárias depois.
	s.clientConnectURLs = s.getClientConnectURLs()
	s.listener = l

	go s.acceptConnections(l, "Client", func(conn net.Conn) { s.createClient(conn) },
		func(_ error) bool {
			if s.isLameDuckMode() {
				// Sinaliza que não estamos aceitando novos clientes
				s.ldmCh <- true
				// Agora aguarda o Shutdown...
				<-s.quitCh
				return true
			}
			return false
		})
	s.mu.Unlock()

	// Informa ao chamador que estamos prontos
	close(clr)
	clr = nil
}
```

- Como referência, a função AcceptLoop executa o seguinte processo. Trata-se de partes relacionadas à comunicação em rede, como `TLS` e `hostPort`, que são desnecessárias na comunicação in-process e podem ser omitidas sem prejuízo.

## Client

### InProcessServer

```go

// nats-go/nats.go

// Connect tentará conectar-se ao sistema NATS.
// A url pode conter semântica de username/password. por exemplo nats://derek:pass@localhost:4222
// Também são suportados arrays separados por vírgula, por exemplo urlA, urlB.
// As opções começam com os padrões, mas podem ser sobrescritas.
// Para conectar-se à porta websocket de um servidor NATS, use o esquema `ws` ou `wss`, como
// `ws://localhost:8080`. Observe que esquemas websocket não podem ser misturados com outros (nats/tls).
func Connect(url string, options ...Option) (*Conn, error) {
	opts := GetDefaultOptions()
	opts.Servers = processUrlString(url)
	for _, opt := range options {
		if opt != nil {
			if err := opt(&opts); err != nil {
				return nil, err
			}
		}
	}
	return opts.Connect()
}
```

```go
// nats-go/nats.go

// Options pode ser usada para criar uma conexão personalizada.
type Options struct {
	// Url representa uma única URL de servidor NATS à qual o client
	// se conectará. Se a opção Servers também estiver definida, ela
	// passa a ser o primeiro servidor do array Servers.
	Url string

	// InProcessServer representa um servidor NATS em execução dentro do
	// mesmo processo. Se estiver definida, tentaremos conectar-nos ao
	// servidor diretamente, em vez de usar conexões TCP externas.
	InProcessServer InProcessConnProvider

	//...
}
```

```go
// nats-go/nats.go

type InProcessConnProvider interface {
	InProcessConn() (net.Conn, error)
}
```

- A função `Connect`, que realiza a conexão entre o servidor nats e o client nats, recebe a URL do client e as opções de conexão, e a struct Options que reúne essas opções possui um campo chamado `InProcessServer` do tipo interface `InProcesConnProvider`.

```go
// main.go of example code

// Inicializa um novo servidor com as opções
ns, err := server.NewServer(opts)

//...

// Conecta ao servidor via conexão in-process
nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
```

- Quando o client nats executa Connect e passa `nats.InProcessServer(ns)` para o campo `InProcessServer`,

```go
// nats-go/nats.go

// InProcessServer é uma Option que tenta estabelecer uma conexão com um servidor NATS
// em execução dentro do processo, em vez de discar via TCP.
func InProcessServer(server InProcessConnProvider) Option {
	return func(o *Options) error {
		o.InProcessServer = server
		return nil
	}
}
```

- o InProcessServer da option é substituído pelo servidor NATS embutido,

```go
// nats-go/nats.go

// createConn conecta-se ao servidor e envolve as estruturas bufio
// apropriadas. Ele faz a coisa certa quando já existe uma
// conexão estabelecida.
func (nc *Conn) createConn() (err error) {
	if nc.Opts.Timeout < 0 {
		return ErrBadTimeout
	}
	if _, cur := nc.currentServer(); cur == nil {
		return ErrNoServers
	}

	// Se temos uma referência a um servidor in-process, estabelecemos uma
	// conexão usando-a.
	if nc.Opts.InProcessServer != nil {
		conn, err := nc.Opts.InProcessServer.InProcessConn()
		if err != nil {
			return fmt.Errorf("failed to get in-process connection: %w", err)
		}
		nc.conn = conn
		nc.bindToNewConn()
		return nil
	}

	//...
}
```

- Essa interface é usada na função `createConn`, que cria a conexão. Quando a option `InProcessServer` não é nil (ou seja, é válida), ela executa o `InProcessConn` do InProcessServer presente na option,

```go
// nats-server/server/server.go

// InProcessConn retorna uma conexão in-process com o servidor,
// evitando a necessidade de um listener TCP para conectividade local
// dentro do mesmo processo. Pode ser usada independentemente do
// estado da opção DontListen.
func (s *Server) InProcessConn() (net.Conn, error) {
	pl, pr := net.Pipe()
	if !s.startGoRoutine(func() {
		s.createClientInProcess(pl)
		s.grWG.Done()
	}) {
		pl.Close()
		pr.Close()
		return nil, fmt.Errorf("failed to create connection")
	}
	return pr, nil
}
```

- chamando o `InProcessConn` implementado no server.
- Essa função é chamada pelo `nats.go`, o client em Go do NATS, quando o `InProcessServer` do nc (conexão nats) não é nil; ela cria a conexão (`net.Conn`) e a vincula à conexão do server.

# Interface de Go definida pelo consumidor

> Um tipo implementa uma interface simplesmente implementando seus métodos. Não há declaração explícita de intenção, nem palavra-chave "implements". Interfaces implícitas desacoplam a definição de uma interface de sua implementação, que pode então aparecer em qualquer pacote sem combinação prévia.

[Interfaces are implemented implicitly, A Tour of Go](https://go.dev/tour/methods/10)

> Se um tipo existe apenas para implementar uma interface e nunca terá métodos exportados além dessa interface, não há necessidade de exportar o próprio tipo.

[Generality, Effective Go](https://go.dev/doc/effective_go#generality)

- Esse design de interface incorpora bem a chamada consumer defined interface e o structural typing (duck typing) que se costuma mencionar em Go, por isso gostaria de apresentar também esse tema.

```go
// nats-go/nats.go

// Options pode ser usada para criar uma conexão personalizada.
type Options struct {
	// Url representa uma única URL de servidor NATS à qual o client
	// se conectará. Se a opção Servers também estiver definida, ela
	// passa a ser o primeiro servidor do array Servers.
	Url string

	// InProcessServer representa um servidor NATS em execução dentro do
	// mesmo processo. Se estiver definida, tentaremos conectar-nos ao
	// servidor diretamente, em vez de usar conexões TCP externas.
	InProcessServer InProcessConnProvider

	//...
}
```

```go
// nats-go/nats.go

type InProcessConnProvider interface {
	InProcessConn() (net.Conn, error)
}
```

- Voltando ao código: no client nats.go, o campo da struct de opções `InProcessServer` foi definido como a interface `InProcessConnProvider`, que realiza apenas a operação `InProcessConn`.

```go
// nats-server/server/server.go

// InProcessConn retorna uma conexão in-process com o servidor,
// evitando a necessidade de um listener TCP para conectividade local
// dentro do mesmo processo. Pode ser usada independentemente do
// estado da opção DontListen.
func (s *Server) InProcessConn() (net.Conn, error) {
	pl, pr := net.Pipe()
	if !s.startGoRoutine(func() {
		s.createClientInProcess(pl)
		s.grWG.Done()
	}) {
		pl.Close()
		pr.Close()
		return nil, fmt.Errorf("failed to create connection")
	}
	return pr, nil
}
```

- No entanto, o tipo que é passado a ele é o `Server` do nats-server, que executa diversas outras funcionalidades além de InProcessConn.
- Isso ocorre porque, nesse contexto, o interesse do client se limita a saber se o objeto oferece a interface `InProcessConn` ou não; as demais funcionalidades não são relevantes para ele.
- Por essa razão, o nats.go client utiliza apenas a interface `InProcessConnProvider`, uma consumer defined interface que define somente a funcionalidade `InProcessConn() (net.Conn, error)`.

# Conclusão

- Foram abordados de forma breve o embedded mode do NATS, seu funcionamento e a consumer defined interface do Go que pode ser observada no código do NATS.
- Espero que essas informações sejam úteis para quem utiliza o NATS com objetivos semelhantes, e encerro o texto por aqui.
