---
id: 8f79af5c6af89b748112aa8b77b348ab
author: prravda
title: Configuración de un servidor NATS integrado en aplicaciones Go y estructura de comunicación en proceso
description: Se abordan la configuración para ejecutar un broker NATS (nats-server/v2) embebido dentro de un proceso Go, así como el diseño de la comunicación en memoria basada en InProcessConn y de la interfaz de consumidor definida por el usuario.
language: es
date: 2025-08-04T01:05:22.150350794Z
path: /blog/posts/how-embedded-nats-communicate-with-go-application-z36089af0
category: network
---

# Primeros pasos

## Acerca de NATS

> Las aplicaciones de software y los servicios necesitan intercambiar datos. NATS es una infraestructura que permite dicho intercambio de datos, segmentados en forma de mensajes. Denominamos a esto "**message oriented middleware**" (middleware orientado a mensajes).
>
> Con NATS, los desarrolladores de aplicaciones pueden:
>
> - Construir aplicaciones cliente-servidor distribuidas y escalables sin esfuerzo.
> - Almacenar y distribuir datos en tiempo real de manera general. Esto puede lograrse de forma flexible en diversos entornos, lenguajes, proveedores de nube y sistemas on-premises.
>
> [What is NATS, NATS docs](https://docs.nats.io/nats-concepts/what-is-nats)

- NATS es un message broker construido en Go.

## NATS embebido

> Si su aplicación está escrita en Go, y si se ajusta a su caso de uso y a sus escenarios de despliegue, incluso puede incrustar un servidor NATS dentro de su aplicación.

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

>

- Y existe una particularidad de NATS: en el caso de las aplicaciones construidas en Go, se admite el modo embedded.
- Es decir, en lugar de la forma habitual de los message brokers, que consiste en ejecutar un servidor de broker aparte y comunicarse con él a través de los clientes de la aplicación, el propio broker puede integrarse (embed) en la aplicación construida en Go.

## Beneficios y casos de uso de NATS embebido

- Existe un [video de YouTube](https://youtu.be/cdTrl8UfcBo?si=KYcXOQpiyLUft6AN) que lo explica bien, por lo que se sustituye por el enlace al video.
- Sin desplegar un servidor de message broker independiente, es posible crear una modular monolith application y lograr separation of concerns, aprovechando la ventaja de incrustar NATS como embedded. Además, también es posible el single binary deployment.
- Resulta útil no solo en platform with no network (wasm), sino también en offline-first application.

# Ejemplo de la documentación oficial

```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{}

    // Inicializar un nuevo servidor con las opciones
    ns, err := server.NewServer(opts)

    if err != nil {
        panic(err)
    }

    // Iniciar el servidor mediante una goroutine
    go ns.Start()

    // Esperar a que el servidor esté listo para las conexiones
    if !ns.ReadyForConnections(4 * time.Second) {
        panic("not ready for connection")
    }

    // Conectarse al servidor
    nc, err := nats.Connect(ns.ClientURL())

    if err != nil {
        panic(err)
    }

    subject := "my-subject"

    // Suscribirse al subject
    nc.Subscribe(subject, func(msg *nats.Msg) {
        // Imprimir los datos del mensaje
        data := string(msg.Data)
        fmt.Println(data)

        // Apagar el servidor (opcional)
        ns.Shutdown()
    })

    // Publicar datos en el subject
    nc.Publish(subject, []byte("Hello embedded NATS!"))

    // Esperar al apagado del servidor
    ns.WaitForShutdown()
}
```

- Este es el [ejemplo de NATS embebido](https://dev.to/karanpratapsingh/embedding-nats-in-go-19o) que figura en la documentación oficial de NATS; sin embargo, si se sigue el código de ejemplo tal cual, la comunicación no se realiza en modo embedding.

```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
```

- Al ejecutar el archivo go con `go run .` mientras se observa la red que circula por [localhost](http://localhost) (127.0.0.1) mediante el comando `watch 'netstat -an | grep 127.0.0.1'`, se puede ver que se añaden nuevas solicitudes de red que parten del puerto `4222`, el puerto por defecto de NATS.

# Configuraciones correctas para el modo embedding

- Para comunicarse en modo embedded tal como se pretende, se necesitan las dos opciones siguientes.
  - Client: hay que incluir la opción `InProcessServer`.
  - Server: en `Server.Options` hay que indicar explícitamente el flag `DontListen` con el valor `true`.
- Estos aspectos no están documentados oficialmente, y el origen de esta funcionalidad puede rastrearse en este [PR](https://github.com/nats-io/nats-server/pull/2360).

  > This PR adds three things:
  >
  > 1. `InProcessConn()` function to `Server` which builds a `net.Pipe` to get a connection to the NATS server without using TCP sockets
  > 2. `DontListen` option which tells the NATS server not to listen on the usual TCP listener
  > 3. `startupComplete` channel, which is closed right before we start `AcceptLoop`, and `readyForConnections` will wait for it
  >
  > The main motivation for this is that we have an application that can run either in a monolith (single-process) mode or a polylith (multi-process) mode. We'd like to be able to use NATS for both modes for simplicity, but the monolith mode has to be able to cater for a variety of platforms where opening socket connections either doesn't make sense (mobile) or just isn't possible (WASM). These changes will allow us to use NATS entirely in-process instead.
  >
  > An accompanying PR [nats-io/nats.go#774](https://github.com/nats-io/nats.go/pull/774) adds support to the client side.
  >
  > This is my first PR to this project so apologies in advance if I've missed anything obvious anywhere.
  >
  > /cc @nats-io/core

# Ejemplo funcional para el modo embedded

```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 el servidor NATS embebido
		// establecer DonListen como true
		DontListen: true,
	}

	// Inicializar un nuevo servidor con las opciones
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

	// Iniciar el servidor mediante una goroutine
	go ns.Start()

	// Esperar a que el servidor esté listo para las conexiones
	if !ns.ReadyForConnections(10 * time.Second) {
		panic("not ready for connection")
	}

	// Conectarse al servidor mediante una conexión in-process
	nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Suscribirse al subject
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Imprimir los datos del mensaje
		data := string(msg.Data)
		fmt.Println(data)

		// Apagar el servidor (opcional)
		ns.Shutdown()
	})

	// Publicar datos en el subject
	nc.Publish(subject, []byte("Hello embedded NATS!"))

	// Esperar al apagado del 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

```

- Ahora se puede observar que no se produce ningún salto de red adicional, tal como se pretendía.

# Por debajo

## TL;DR

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

- Este es el diagrama de secuencia que muestra qué funciones se ejecutan internamente y cómo lo hacen al ejecutar este código desde `main.go`. Los puntos principales son los siguientes.
  - Mediante `DontListen: true`, el servidor omite la fase de escucha de clientes denominada `AcceptLoop`.
  - Si la opción `InProcessServer` del Connect del cliente está activada, se crea una conexión en memoria: se genera un `net.Pipe`, y el otro extremo del pipe se devuelve al cliente como un tipo `net.Conn`.
  - El cliente y el servidor realizan la comunicación in-process a través de esa conexión.

## Servidor

### AcceptLoop

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

// Esperar a los clientes.
if !opts.DontListen {
	s.AcceptLoop(clientListenReady)
}
```

- En primer lugar, cuando `DontListen` es true, se omite la fase de escucha de clientes denominada `AcceptLoop`.

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

// AcceptLoop se exporta para facilitar las pruebas.
func (s *Server) AcceptLoop(clr chan struct{}) {
	// Si saliéramos antes de que el listener se configure correctamente,
	// asegurarse de cerrar el canal.
	defer func() {
		if clr != nil {
			close(clr)
		}
	}()

	if s.isShuttingDown() {
		return
	}

	// Tomar una instantánea de las opciones del servidor.
	opts := s.getOpts()

	// Configurar el estado que puede permitir el apagado
	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)))

	// Avisar de 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")
		}
	}

	// Si el servidor se inició con RANDOM_PORT (-1), opts.Port sería igual
	// a 0 al principio de esta función. Por eso necesitamos obtener el puerto real
	if opts.Port == 0 {
		// Escribir el puerto resuelto de nuevo en las opciones.
		opts.Port = l.Addr().(*net.TCPAddr).Port
	}

	// Ahora que el puerto se ha establecido (si era RANDOM), configurar el
	// Host/Port de INFO del servidor con los valores de Options o
	// 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
	}
	// Guardar las URL de conexión de los clientes. Podríamos necesitarlas más adelante.
	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() {
				// Indicar que ya no se aceptan nuevos clientes
				s.ldmCh <- true
				// Ahora esperar al Shutdown...
				<-s.quitCh
				return true
			}
			return false
		})
	s.mu.Unlock()

	// Informar al llamador de que estamos listos
	close(clr)
	clr = nil
}
```

- Como referencia, la función AcceptLoop realiza los siguientes pasos. Las partes relacionadas con la comunicación de red, como `TLS` o `hostPort`, no son necesarias en la comunicación in-process, por lo que pueden omitirse sin problema.

## Cliente

### InProcessServer

```go

// nats-go/nats.go

// Connect intentará conectarse al sistema NATS.
// La url puede contener semántica de usuario/contraseña. p. ej. nats://derek:pass@localhost:4222
// También se admiten arrays separados por comas, p. ej. urlA, urlB.
// Las opciones parten de los valores por defecto, pero pueden sobrescribirse.
// Para conectarse al puerto websocket de un servidor NATS, use el esquema `ws` o `wss`, por ejemplo
// `ws://localhost:8080`. Tenga en cuenta que los esquemas websocket no pueden mezclarse con otros (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 puede usarse para crear una conexión personalizada.
type Options struct {
	// Url representa una única URL del servidor NATS al que el cliente
	// se conectará. Si también se establece la opción Servers,
	// pasa a ser el primer servidor del array Servers.
	Url string

	// InProcessServer representa un servidor NATS que se ejecuta dentro del
	// mismo proceso. Si se establece, intentaremos conectarnos al
	// servidor directamente en lugar de usar conexiones TCP externas.
	InProcessServer InProcessConnProvider

	//...
}
```

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

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

- La función `Connect`, que establece la conexión entre el servidor y el cliente NATS, permite configurar la URL del cliente y las opciones de conexión. El struct Options, que reúne dichas opciones, contiene un campo llamado `InProcessServer` de tipo interface `InProcessConnProvider`.

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

// Inicializar un nuevo servidor con las opciones
ns, err := server.NewServer(opts)

//...

// Conectarse al servidor mediante una conexión in-process
nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
```

- Cuando el cliente NATS llama a Connect y pasa `nats.InProcessServer(ns)`, como valor del campo `InProcessServer`,

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

// InProcessServer es una Option que intentará establecer una conexión directa con un servidor NATS
// que se ejecuta dentro del proceso, en lugar de conectarse mediante TCP.
func InProcessServer(server InProcessConnProvider) Option {
	return func(o *Options) error {
		o.InProcessServer = server
		return nil
	}
}
```

- la opción InProcessServer del option se sustituye por el servidor NATS embebido,

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

// createConn se conectará al servidor y envolverá las
// estructuras bufio apropiadas. Actuará correctamente cuando
// ya exista una conexión.
func (nc *Conn) createConn() (err error) {
	if nc.Opts.Timeout < 0 {
		return ErrBadTimeout
	}
	if _, cur := nc.currentServer(); cur == nil {
		return ErrNoServers
	}

	// Si tenemos una referencia a un servidor in-process, establecer una
	// conexión usando esa referencia.
	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
	}

	//...
}
```

- Esta interface se utiliza en la función `createConn`, que crea la conexión. Si la opción `InProcessServer` no es nil (es válida), ejecuta el `InProcessConn` de la opción InProcessServer,

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

// InProcessConn devuelve una conexión in-process con el servidor,
// evitando la necesidad de usar un listener TCP para la conectividad local
// dentro del mismo proceso. Puede usarse independientemente del
// estado de la opción 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
}
```

- Se invoca la función `InProcessConn` implementada en el servidor.
- Esta función se llama desde `nats.go`, el cliente Go de NATS, cuando la opción `InProcessServer` del nc (conexión NATS) no es nil; crea la conexión (`net.Conn`) y la vincula a la conexión del servidor.

# Interfaces de Go definidas por el consumidor

> A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword. Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement.

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

> If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself.

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

- Este diseño de interfaces encaja bien con lo que en Go se conoce comúnmente como consumer defined interface y con el tipado estructural (duck typing), por lo que me gustaría presentar también este tema.

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

// Options puede usarse para crear una conexión personalizada.
type Options struct {
	// Url representa una única URL del servidor NATS al que el cliente
	// se conectará. Si también se establece la opción Servers,
	// pasa a ser el primer servidor del array Servers.
	Url string

	// InProcessServer representa un servidor NATS que se ejecuta dentro del
	// mismo proceso. Si se establece, intentaremos conectarnos al
	// servidor directamente en lugar de usar conexiones TCP externas.
	InProcessServer InProcessConnProvider

	//...
}
```

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

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

- Volvamos al código. En el cliente nats.go, el campo de struct de la opción `InProcessServer` se ha definido como la interface `InProcessConnProvider`, que solo realiza `InProcessConn`.

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

// InProcessConn devuelve una conexión in-process con el servidor,
// evitando la necesidad de usar un listener TCP para la conectividad local
// dentro del mismo proceso. Puede usarse independientemente del
// estado de la opción 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
}
```

- Sin embargo, el tipo que se pasa es el `Server` de nats-server, que realiza muchas otras funciones además de `InProcessConn`.
- Esto se debe a que, en este caso, al cliente solo le interesa si se proporciona la interface `InProcessConn` o no; el resto de funciones no le importan demasiado.
- Por ello, nats.go solo utiliza la interface `InProcessConnProvider`, una consumer defined interface que define únicamente la función `InProcessConn() (net.Conn, error)`.

# Conclusión

- Se ha tratado brevemente el modo embedded de NATS, su funcionamiento y la consumer defined interface de Go que puede verse en el código de NATS.
- Espero que esta información resulte útil para quienes utilizan NATS con este mismo propósito, y doy por concluido este artículo.
