---
id: 8f79af5c6af89b748112aa8b77b348ab
author: prravda
title: Konfigurasjon av innebygd NATS-server i Go-applikasjoner og struktur for kommunikasjon i samme prosess
description: Dette dokumentet omhandler oppsett for å kjøre en NATS-meglerserver (nats-server/v2) innebygd i en Go-prosess, samt design av in-memory-kommunikasjon basert på InProcessConn og et grensesnitt for forbrukere.
language: "no"
date: 2025-08-04T01:05:22.150350794Z
path: /blog/posts/how-embedded-nats-communicate-with-go-application-z36089af0
category: network
---

# Komme i gang

## Om NATS

> Programvareapplikasjoner og -tjenester må utveksle data. NATS er en infrastruktur som muliggjør slik datautveksling, segmentert i form av meldinger. Vi kaller dette for «**message oriented middleware**».
>
> Med NATS kan applikasjonsutviklere:
>
> - Uanstrengt bygge distribuerte og skalerbare klient-server-applikasjoner.
> - Lagre og distribuere data i sanntid på en generell måte. Dette kan oppnås fleksibelt på tvers av ulike miljøer, programmeringsspråk, skyleverandører og lokale systemer.
>
> [Hva er NATS, NATS-dokumentasjon](https://docs.nats.io/nats-concepts/what-is-nats)

- NATS er en meldingsmegler (message broker) implementert i Go.

## Innebygd NATS

> Dersom applikasjonen din er skrevet i Go, og dersom dette passer til bruksområdet og distribusjonsscenarioene dine, kan du til og med bygge inn en NATS-server i applikasjonen.

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

>

- I tillegg har NATS en særegenhet: for applikasjoner skrevet i Go støttes en embedded mode (innebygd modus).
- Med andre ord betyr dette at meldingsmegleren i seg selv kan innebygges (embed) i en applikasjon skrevet i Go, i stedet for den vanlige ordningen der en separat meldingsmeglerserver startes og kommuniseres med via applikasjonens klienter.

## Fordeler og bruksområder for innebygd NATS

- Det finnes en godt forklart [YouTube-video](https://youtu.be/cdTrl8UfcBo?si=KYcXOQpiyLUft6AN), og vi viser derfor til videoen i stedet for å gjengi innholdet her.
- Selv uten å distribuere en egen meldingsmeglerserver kan man bygge en modulær monolitt (modular monolith) applikasjon og oppnå separation of concerns, og dermed dra nytte av fordelen ved å bygge inn NATS. I tillegg muliggjøres distribusjon som én enkelt binærfil (single binary deployment).
- Den er nyttig ikke bare på plattformer uten nettverk (wasm), men også i offline-first-applikasjoner.

# Eksempel fra den offisielle dokumentasjonen

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

    // Initialiser ny server med alternativer
    ns, err := server.NewServer(opts)

    if err != nil {
        panic(err)
    }

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

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

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

    if err != nil {
        panic(err)
    }

    subject := "my-subject"

    // Abonner på emnet
    nc.Subscribe(subject, func(msg *nats.Msg) {
        // Skriv ut meldingsdataene
        data := string(msg.Data)
        fmt.Println(data)

        // Stopp serveren (valgfritt)
        ns.Shutdown()
    })

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

    // Vent på at serveren stopper
    ns.WaitForShutdown()
}
```

- Dette er et [eksempel på Embedded NATS](https://dev.to/karanpratapsingh/embedding-nats-in-go-19o) som er lenket fra den offisielle NATS-dokumentasjonen, men dersom man følger eksempelkoden slavisk, skjer kommunikasjonen ikke i embedding-modus.

```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 å bruke kommandoen `watch 'netstat -an | grep 127.0.0.1'` for å overvåke nettverkstrafikken til [localhost](http://localhost) (127.0.0.1), og samtidig kjøre filen med `go run .`, ser man nye nettverksforespørsler som går ut fra `4222`, NATS' standardport.

# Korrekte konfigurasjoner for embedding-modus

- For å kommunisere i embedded mode slik det er tenkt, kreves to alternativer:
  - Klient: alternativet `InProcessServer` må settes.
  - Server: flagget `DontListen` må settes til `true` i `Server.Options`.
- Disse detaljene er ikke formelt dokumentert, men opprinnelsen til funksjonen kan spores gjennom denne [PR-en](https://github.com/nats-io/nats-server/pull/2360).

  > Denne PR-en legger til tre ting:
  >
  > 1. Funksjonen `InProcessConn()` for `Server`, som bygger en `net.Pipe` for å få en forbindelse til NATS-serveren uten bruk av TCP-sockets
  > 2. Alternativet `DontListen`, som forteller NATS-serveren at den ikke skal lytte på den vanlige TCP-lytteren
  > 3. Kanalen `startupComplete`, som lukkes rett før vi starter `AcceptLoop`, og `readyForConnections` vil vente på den
  >
  > Hovedmotivasjonen for dette er at vi har en applikasjon som kan kjøres enten i monolittmodus (én prosess) eller polylittmodus (flere prosesser). Vi ønsker å kunne bruke NATS i begge modi for enkelhets skyld, men monolittmodus må kunne støtte en rekke plattformer der åpning av socket-forbindelser enten ikke gir mening (mobil) eller rett og slett ikke er mulig (WASM). Disse endringene gjør det mulig å bruke NATS helt og holdent innenfor prosessen i stedet.
  >
  > En tilhørende PR [nats-io/nats.go#774](https://github.com/nats-io/nats.go/pull/774) legger til støtte på klientsiden.
  >
  > Dette er min første PR til dette prosjektet, så jeg beklager på forhånd dersom jeg har oversett noe åpenbart et sted.
  >
  > /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 å konfigurere den innebygde NATS-serveren
		// sett DonListen til true
		DontListen: true,
	}

	// Initialiser ny server med alternativer
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

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

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

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

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Abonner på emnet
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Skriv ut meldingsdataene
		data := string(msg.Data)
		fmt.Println(data)

		// Stopp serveren (valgfritt)
		ns.Shutdown()
	})

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

	// Vent på at serveren stopper
	ns.WaitForShutdown()
}

```

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

```

- Nå ser man at det ikke oppstår noe ekstra nettverkshopp, slik det var ment.

# Under panseret

## TL;DR

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

- Dette er et sekvensdiagram som viser hvilke funksjoner som kjøres internt, og hvordan de virker sammen, når koden i `main.go` kjøres. I korte trekk er hovedpunktene følgende:
  - Gjennom `DontListen: true` hopper serveren over fasen for klientlytting kalt `AcceptLoop`.
  - Dersom alternativet `InProcessServer` i klientens Connect-opsjoner er aktivert, opprettes en forbindelse i minnet. Via `net.Pipe` lages en pipe, og den ene enden av pipen returneres til klienten som en `net.Conn`.
  - Klienten og serveren kommuniserer deretter via denne forbindelsen i en in-process-kommunikasjon.

## Server

### AcceptLoop

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

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

- Først hoppes fasen for klientlytting, `AcceptLoop`, over når `DontListen` er true.

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

// AcceptLoop er eksportert for enklere testing.
func (s *Server) AcceptLoop(clr chan struct{}) {
	// Dersom vi skulle avslutte før lytteren er riktig satt opp, må vi sørge for å lukke kanalen.
	defer func() {
		if clr != nil {
			close(clr)
		}
	}()

	if s.isShuttingDown() {
		return
	}

	// Ta et øyeblikksbilde av serveropsjonene.
	opts := s.getOpts()

	// Sett opp tilstand som gjør det mulig å stoppe serveren
	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)))

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

	// Dersom serveren ble startet med RANDOM_PORT (-1), vil opts.Port være lik 0 i begynnelsen av denne funksjonen. Derfor må vi hente den faktiske porten
	if opts.Port == 0 {
		// Skriv den oppløste porten tilbake til opsjonene.
		opts.Port = l.Addr().(*net.TCPAddr).Port
	}

	// Nå som porten er satt (dersom den var satt til RANDOM), settes serverens info Host/Port med enten verdier 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 oversikt over klientenes connect-URL-er. Vi kan få behov 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() {
				// Signaliser at vi ikke lenger aksepterer nye klienter
				s.ldmCh <- true
				// Vent nå på nedstengingen...
				<-s.quitCh
				return true
			}
			return false
		})
	s.mu.Unlock()

	// La kalleren vite at vi er klare
	close(clr)
	clr = nil
}
```

- For ordens skyld utfører funksjonen AcceptLoop følgende trinn. Disse knytter seg til nettverkskommunikasjon, som `TLS` og `hostPort`; ved in-process-kommunikasjon er de ikke nødvendige, og de kan derfor utelates.

## Client

### InProcessServer

```go

// nats-go/nats.go

// Connect forsøker å koble til NATS-systemet.
// URL-en kan inneholde brukernavn/passord, f.eks. nats://derek:pass@localhost:4222
// Kommaseparerte lister støttes også, f.eks. urlA, urlB.
// Opsjonene starter med standardverdiene, men kan overstyres.
// For å koble til websocket-porten til en NATS-server, bruk skjemaet `ws` eller `wss`, for eksempel
// `ws://localhost:8080`. Merk at websocket-skjemaer ikke kan kombineres 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 brukes til å opprette en tilpasset tilkobling.
type Options struct {
	// Url representerer én enkelt NATS-server-URL som klienten kobler til.
	// Dersom Servers-opsjonen også er satt, blir den
	// den første serveren i Servers-matrisen.
	Url string

	// InProcessServer representerer en NATS-server som kjører innenfor
	// samme prosess. Dersom denne er satt, forsøker vi å koble til
	// serveren direkte i stedet for å bruke eksterne TCP-forbindelser.
	InProcessServer InProcessConnProvider

	//...
}
```

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

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

- Funksjonen `Connect`, som etablerer forbindelsen mellom NATS-serveren og NATS-klienten, tar imot en klient-URL og connect-opsjoner. Structen Options, som samler disse opsjonene, har et felt kalt `InProcessServer` av interface-typen `InProcessConnProvider`.

```go
// main.go i eksempelkoden

// Initialiser ny server med alternativer
ns, err := server.NewServer(opts)

//...

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

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

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

// InProcessServer er en Option som forsøker å etablere en forbindelse til en NATS-server som kjører innenfor prosessen, i stedet for å koble til via TCP.
func InProcessServer(server InProcessConnProvider) Option {
	return func(o *Options) error {
		o.InProcessServer = server
		return nil
	}
}
```

- erstattes opsjonens InProcessServer med den innebygde NATS-serveren,

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

// createConn kobler til serveren og pakker inn de riktige bufio-strukturene. Den håndterer riktig når en eksisterende forbindelse allerede finnes.
func (nc *Conn) createConn() (err error) {
	if nc.Opts.Timeout < 0 {
		return ErrBadTimeout
	}
	if _, cur := nc.currentServer(); cur == nil {
		return ErrNoServers
	}

	// Dersom vi har en referanse til en in-process-server, etableres en forbindelse ved hjelp av 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
	}

	//...
}
```

- Dette interfacet brukes i funksjonen `createConn`, som oppretter forbindelsen. Når opsjonen `InProcessServer` ikke er nil (altså gyldig), kjøres `InProcesConn` på `InProcessServer` i opsjonen,

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

// InProcessConn returnerer en in-process-forbindelse til serveren, og unngår behovet for en TCP-lytter for lokal kommunikasjon innenfor samme prosess. Den kan brukes uavhengig av tilstanden til DontListen-opsjonen.
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
}
```

- som kaller `InProcessConn` implementert i serveren.
- Denne funksjonen kalles i nats.go, nats-klienten for Go, når `InProcessServer` på nc (nats-tilkoblingen) ikke er nil. Den oppretter en forbindelse (`net.Conn`) og binder den til serverens forbindelse.

# Forbrukerdrevne interface i Go

> En type implementerer et interface ved å implementere dets metoder. Det finnes ingen eksplisitt erklæring av hensikt, og ikke noe nøkkelord «implements». Implisitte interface frikobler definisjonen av et interface fra implementasjonen, som dermed kan forekomme i enhver pakke uten forhåndsavtale.

[Interface implementeres implisitt, A Tour of Go](https://go.dev/tour/methods/10)

> Dersom en type kun finnes for å implementere et interface, og aldri vil ha eksporterte metoder utover dette interfacet, er det ikke nødvendig å eksportere selve typen.

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

- Denne interfacedesignen illustrerer godt det Go-miljøet ofte kaller consumer defined interface (forbrukerdefinerte interface) og strukturell typing (duck typing), og derfor ønsker jeg å presentere dette temaet også.

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

// Options kan brukes til å opprette en tilpasset tilkobling.
type Options struct {
	// Url representerer én enkelt NATS-server-URL som klienten kobler til.
	// Dersom Servers-opsjonen også er satt, blir den
	// den første serveren i Servers-matrisen.
	Url string

	// InProcessServer representerer en NATS-server som kjører innenfor
	// samme prosess. Dersom denne er satt, forsøker vi å koble til
	// serveren direkte i stedet for å bruke eksterne TCP-forbindelser.
	InProcessServer InProcessConnProvider

	//...
}
```

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

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

- La oss gå tilbake til koden. I nats.go-klienten er feltet `InProcessServer` i opsjons-structen definert som interfacet `InProcessConnProvider`, som kun utfører `InProcessConn`.

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

// InProcessConn returnerer en in-process-forbindelse til serveren, og unngår behovet for en TCP-lytter for lokal kommunikasjon innenfor samme prosess. Den kan brukes uavhengig av tilstanden til DontListen-opsjonen.
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
}
```

- Typen som settes inn der, er imidlertid `Server` fra nats-server, som utfører en rekke funksjoner utover InProcessConn.
- Årsaken er at klientens eneste interesse i denne situasjonen er om objektet tilbyr `InProcessConn`, mens andre egenskaper i liten grad er av betydning.
- Følgelig definerer nats.go-klienten kun et forbrukerdefinert interface, `InProcessConnProvider`, som består av funksjonen `InProcessConn() (net.Conn, error)`, og bruker dette.

# Konklusjon

- I denne artikkelen har vi kort gjennomgått embedded mode i NATS, hvordan det fungerer, og det forbrukerdefinerte interfacet i Go, som kan observeres i NATS-koden.
- Jeg håper denne informasjonen er nyttig for dem som bruker NATS til lignende formål, og avslutter dermed artikkelen her.
