---
id: 8f79af5c6af89b748112aa8b77b348ab
author: prravda
title: Konfiguration af en indlejret NATS-server i Go-applikationer og arkitekturen for kommunikation i samme proces
description: Dette afsnit beskriver, hvordan man konfigurerer og starter en NATS-broker (nats-server/v2) indlejret i en Go-proces, og behandler design af in-memory-kommunikation baseret på InProcessConn samt forbrugerdefinerede grænseflader.
language: da
date: 2025-08-04T01:05:22.150350794Z
path: /blog/posts/how-embedded-nats-communicate-with-go-application-z36089af0
category: network
---

# Kom godt i gang

## Om NATS

> Softwareapplikationer og -tjenester skal udveksle data. NATS er en infrastruktur, der muliggør en sådan dataudveksling, opdelt i form af meddelelser. Vi kalder dette for en "**message oriented middleware**".
>
> Med NATS kan applikationsudviklere:
>
> - Nemt bygge distribuerede og skalerbare klient-server-applikationer.
> - Lagre og distribuere data i realtid på en generel måde. Dette kan fleksibelt opnås på tværs af forskellige miljøer, sprog, cloududbydere og lokale systemer (on-premises).
>
> [What is NATS, NATS docs](https://docs.nats.io/nats-concepts/what-is-nats)

- NATS er en meddelelsesbroker bygget i Go.

## Indlejret NATS

> Hvis din applikation er skrevet i Go, og hvis det passer til dit use case og dine udrulningsscenarier, kan du endda indlejre en NATS-server i din applikation.

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

>

- Der er også en særlig egenskab ved NATS: for applikationer skrevet i Go understøttes en embedded mode.
- Det vil sige, at brokeren ikke behøver at køre som en separat brokerserver, som applikationens klienter kommunikerer med, som det er almindeligt for meddelelsesbrokere. I stedet kan selve brokeren indlejres (embed) i den applikation, der er skrevet i Go.

## Fordele og use cases for indlejret NATS

- Der findes en velforklaret [Youtube-video](https://youtu.be/cdTrl8UfcBo?si=KYcXOQpiyLUft6AN), som vi henviser til i stedet for at gentage indholdet.
- Uden at udrulle en separat meddelelsesbrokerserver kan man bygge en modular monolith-applikation og opnå separation of concerns, og samtidig drage fordel af at indlejre NATS. Derudover bliver single binary deployment mulig.
- Det er nyttigt ikke kun i platforme uden netværk (wasm), men også i offline-first-applikationer.

# Eksempel fra den officielle dokumentation

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

    // Opret en ny server med options
    ns, err := server.NewServer(opts)

    if err != nil {
        panic(err)
    }

    // Start serveren via goroutine
    go ns.Start()

    // Vent på, at serveren er klar til forbindelser
    if !ns.ReadyForConnections(4 * time.Second) {
        panic("not ready for connection")
    }

    // Opret forbindelse til serveren
    nc, err := nats.Connect(ns.ClientURL())

    if err != nil {
        panic(err)
    }

    subject := "my-subject"

    // Abonner på subjectet
    nc.Subscribe(subject, func(msg *nats.Msg) {
        // Udskriv meddelelsens data
        data := string(msg.Data)
        fmt.Println(data)

        // Luk serveren ned (valgfrit)
        ns.Shutdown()
    })

    // Publicer data til subjectet
    nc.Publish(subject, []byte("Hello embedded NATS!"))

    // Vent på, at serveren lukker ned
    ns.WaitForShutdown()
}
```

- Dette er [eksemplet på Embedded NATS](https://dev.to/karanpratapsingh/embedding-nats-in-go-19o), som den officielle NATS-dokumentation henviser til. Hvis man dog følger eksempelkoden som den er, sker kommunikationen ikke i 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
```

- Ved at køre `watch 'netstat -an | grep 127.0.0.1'` kan man følge netværkstrafikken til [localhost](http://localhost) (127.0.0.1). Når man kører Go-filen med `go run .`, ser man nye netværksforespørgsler, der starter fra NATS' standardport `4222`.

# Korrekte konfigurationer for embedding mode

- For at kommunikere i embedded mode som tilsigtet kræves to indstillinger:
  - Klient: `InProcessServer`-option skal angives.
  - Server: Flaget `DontListen` i `Server.Options` skal sættes til `true`.
- Disse dele er ikke officielt dokumenteret, og begyndelsen på denne funktion kan findes i denne [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

# Fungerende eksempel for 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{
		// for configuring the embeded NATS server
		// set DonListen as true
		DontListen: true,
	}

	// Opret en ny server med options
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

	// Start serveren via goroutine
	go ns.Start()

	// Vent på, at serveren er klar til forbindelser
	if !ns.ReadyForConnections(10 * time.Second) {
		panic("not ready for connection")
	}

	// Opret forbindelse til serveren via in-process forbindelse
	nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Abonner på subjectet
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Udskriv meddelelsens data
		data := string(msg.Data)
		fmt.Println(data)

		// Luk serveren ned (valgfrit)
		ns.Shutdown()
	})

	// Publicer data til subjectet
	nc.Publish(subject, []byte("Hello embedded NATS!"))

	// Vent på, at serveren lukker ned
	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

```

- Nu ses det, at der ikke opstår yderligere netværkshop, som tilsigtet.

# Under overfladen

## TL;DR

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

- Dette er et sequence diagram, der viser, hvilke funktioner der kører internt, og hvordan de arbejder sammen, når koden i `main.go` udføres. Hovedpunkterne er følgende:
  - Med `DontListen: true` springer serveren `AcceptLoop`-fasen over, som er client listening phase.
  - Hvis `InProcessServer` er aktiveret i klientens Connect-option, oprettes en in-memory-forbindelse. Der oprettes en pipe via `net.Pipe`, hvorefter den ene ende af pipen returneres til klienten som en `net.Conn`-type.
  - Klienten og serveren kommunikerer via denne forbindelse i in-process communication.

## Server

### AcceptLoop

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

// Vent på klienter.
if !opts.DontListen {
	s.AcceptLoop(clientListenReady)
}
```

- Hvis `DontListen` er true, springer serveren først `AcceptLoop`-fasen over, som er client listening phase.

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

// AcceptLoop er eksporteret for lettere testning.
func (s *Server) AcceptLoop(clr chan struct{}) {
	// Hvis vi skulle afslutte, før listeneren er sat korrekt op,
	// skal vi sørge for at lukke kanalen.
	defer func() {
		if clr != nil {
			close(clr)
		}
	}()

	if s.isShuttingDown() {
		return
	}

	// Tag et snapshot af serverens options.
	opts := s.getOpts()

	// Opsæt state, der muliggør nedlukning
	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)))

	// Underret om at TLS er aktiveret.
	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")
		}
	}

	// Hvis serveren blev startet med RANDOM_PORT (-1), vil opts.Port være lig med
	// 0 i begyndelsen af denne funktion. Så vi skal hente den faktiske port
	if opts.Port == 0 {
		// Skriv den fundne port tilbage til options.
		opts.Port = l.Addr().(*net.TCPAddr).Port
	}

	// Nu hvor porten er sat (hvis den blev sat til RANDOM), sæt
	// serverens info Host/Port med enten værdier fra Options eller
	// 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
	}
	// Hold styr på klientens connect-URL'er. Vi har muligvis brug for dem senere.
	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() {
				// Signalér at vi ikke accepterer nye klienter
				s.ldmCh <- true
				// Vent nu på nedlukningen...
				<-s.quitCh
				return true
			}
			return false
		})
	s.mu.Unlock()

	// Lad kalderen vide, at vi er klar
	close(clr)
	clr = nil
}
```

- Bemærk, at AcceptLoop-funktionen gennemfører følgende trin. `TLS` og `hostPort` hører til netværkskommunikation, og da de ikke er nødvendige ved in-process communication, kan de udelades.

## Klient

### InProcessServer

```go

// nats-go/nats.go

// Connect forsøger at oprette forbindelse til NATS-systemet.
// URL'en kan indeholde username/password-oplysninger, f.eks. nats://derek:pass@localhost:4222.
// Kommaseparerede arrays understøttes også, f.eks. urlA, urlB.
// Options starter med standardværdierne, men kan overskrives.
// For at oprette forbindelse til en NATS-servers websocket-port, brug `ws`- eller `wss`-skemaet, f.eks.
// `ws://localhost:8080`. Bemærk, at websocket-skemaer ikke kan blandes med andre (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 kan bruges til at oprette en tilpasset forbindelse.
type Options struct {
	// Url angiver en enkelt NATS-server-URL, som klienten
	// forbinder til. Hvis Servers-optionen også er sat, bliver
	// den den første server i Servers-arrayet.
	Url string

	// InProcessServer angiver en NATS-server, der kører inden for
	// samme proces. Hvis denne er sat, forsøger vi at oprette forbindelse
	// direkte til serveren i stedet for at bruge eksterne TCP-forbindelser.
	InProcessServer InProcessConnProvider

	//...
}
```

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

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

- `Connect`-funktionen, der forbinder NATS-serveren med NATS-klienten, tager en client-URL og connect-options. Options-structen, der samler disse options, indeholder et felt ved navn `InProcessServer` af typen `InProcessConnProvider` interface.

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

// Opret en ny server med options
ns, err := server.NewServer(opts)

//...

// Opret forbindelse til serveren via in-process forbindelse
nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
```

- Når NATS-klienten kalder Connect og sender `nats.InProcessServer(ns)` til feltet `InProcessServer`,

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

// InProcessServer er en Option, der forsøger at etablere en forbindelse til en NATS-server,
// som kører inden for processen, i stedet for at oprette forbindelse via TCP.
func InProcessServer(server InProcessConnProvider) Option {
	return func(o *Options) error {
		o.InProcessServer = server
		return nil
	}
}
```

- bliver option'ens InProcessServer erstattet med den indlejrede NATS-server,

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

// createConn opretter forbindelse til serveren og indpakker de relevante
// bufio-strukturer. Den håndterer korrekt tilfælde, hvor der allerede
// findes en forbindelse.
func (nc *Conn) createConn() (err error) {
	if nc.Opts.Timeout < 0 {
		return ErrBadTimeout
	}
	if _, cur := nc.currentServer(); cur == nil {
		return ErrNoServers
	}

	// Hvis vi har en reference til en in-process server, etablerer vi en
	// forbindelse via denne.
	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
	}

	//...
}
```

- Interfacet bruges i funktionen `createConn`, som opretter forbindelsen. Hvis `InProcessServer`-optionen ikke er nil (dvs. gyldig), kaldes `InProcessConn` på den InProcessServer, der er angivet i optionen,

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

// InProcessConn returnerer en in-process forbindelse til serveren
// og undgår behovet for en TCP-listener til lokal forbindelse
// inden for samme proces. Dette kan bruges uanset
// tilstanden af DontListen-optionen.
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
}
```

- Den implementerede `InProcessConn` på serveren kaldes og udføres.
- Denne funktion kaldes i `nats.go`, NATS' Go-klient, når `InProcessServer` på nc (NATS-forbindelsen) ikke er nil. Den opretter en forbindelse (`net.Conn`) og binder den til serverens forbindelse.

# Forbrugerdefineret interface i Go

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

- Dette interfacedesign illustrerer godt det, man i Go ofte kalder consumer defined interface og structural typing (duck typing), og derfor vil jeg også introducere dette emne.

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

// Options kan bruges til at oprette en tilpasset forbindelse.
type Options struct {
	// Url angiver en enkelt NATS-server-URL, som klienten
	// forbinder til. Hvis Servers-optionen også er sat, bliver
	// den den første server i Servers-arrayet.
	Url string

	// InProcessServer angiver en NATS-server, der kører inden for
	// samme proces. Hvis denne er sat, forsøger vi at oprette forbindelse
	// direkte til serveren i stedet for at bruge eksterne TCP-forbindelser.
	InProcessServer InProcessConnProvider

	//...
}
```

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

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

- Lad os vende tilbage til koden. I nats.go-klienten er feltet `InProcessServer` i option-structen defineret som interfacet `InProcessConnProvider`, som kun udfører `InProcessConn`.

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

// InProcessConn returnerer en in-process forbindelse til serveren
// og undgår behovet for en TCP-listener til lokal forbindelse
// inden for samme proces. Dette kan bruges uanset
// tilstanden af DontListen-optionen.
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
}
```

- Den type, der sendes ind, er dog `Server` fra nats-server, som udfører mange flere funktioner end blot `InProcessConn`.
- Det skyldes, at klientens interesse i denne situation kun er, om serveren leverer interfacet `InProcessConn` eller ej; de øvrige funktioner er ikke særlig relevante for klienten.
- Derfor bruger nats.go-klienten kun et consumer defined interface ved navn `InProcessConnProvider`, som kun definerer funktionen `InProcessConn() (net.Conn, error)`.

# Konklusion

- Vi har kort gennemgået NATS' embedded mode og dets funktionsmåde samt det consumer defined interface i Go, som kan ses i NATS' kode.
- Jeg håber, at denne information er nyttig for dem, der bruger NATS til lignende formål, og hermed afslutter jeg denne artikel.
