DEV Community

Cover image for Einfache Datenspeicherung in Go: Von In-Memory zu Redis
Amin Mohammadi
Amin Mohammadi

Posted on

Einfache Datenspeicherung in Go: Von In-Memory zu Redis

Einfache Daten-Speicherung in Go: Von In-Memory zu Redis

In vielen kleinen Go-Projekten fängt man damit an, Daten einfach im Speicher (In-Memory) zu halten. Das ist schnell und leicht zu verstehen – aber es hat klare Grenzen. In diesem Artikel schauen wir uns an:

  • Wie eine einfache In-Memory-Speicherung in Go aussieht
  • Welche Probleme dabei in der Praxis auftreten
  • Wie wir dieselbe Idee mit Redis robuster und skalierbar umsetzen

Beispiele sind bewusst einfach gehalten und richten sich an Einsteiger.


1. In-Memory-Speicher in Go – Der Einfache Start

Angenommen, wir wollen ein sehr kleines Key-Value-"Storage" bauen, z.B. um Tokens oder einfache Konfigurationen zu speichern.

1.1. Ein Einfacher Speicher mit map

package main

import (
    "fmt"
    "sync"
)

type InMemoryStore struct {
    mu   sync.RWMutex
    data map[string]string
}

func NewInMemoryStore() *InMemoryStore {
    return &InMemoryStore{
        data: make(map[string]string),
    }
}

func (s *InMemoryStore) Set(key, value string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.data[key] = value
}

func (s *InMemoryStore) Get(key string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    val, ok := s.data[key]
    return val, ok
}

func main() {
    store := NewInMemoryStore()

    store.Set("username", "alice")
    if val, ok := store.Get("username"); ok {
        fmt.Println("Username:", val)
    }
}
Enter fullscreen mode Exit fullscreen mode

Was passiert hier?

  • map[string]string hält unsere Daten im RAM
  • sync.RWMutex schützt die Map vor Race Conditions bei parallelen Zugriffen

Für ein einzelnes kleines Service funktioniert das zunächst sehr gut.


2. Wo In-Memory in Go zum Problem Wird

Sobald die Anwendung wächst, tauchen typische Probleme auf:

2.1. Daten Gehen beim Neustart Verloren

  • In-Memory-Daten leben nur so lange, wie der Prozess lebt
  • Neustart des Services = alle Daten sind weg

2.2. Skalierung mit Mehreren Instanzen

Wenn du mehrere Instanzen deiner Go-App hinter einem Load-Balancer betreibst, hat jede Instanz ihre eigene Map:

  • Instanz A speichert den Key username = alice
  • Instanz B kennt diesen Eintrag nicht
  • Je nach Load-Balancer-Routing bekommst du unterschiedliche Ergebnisse

2.3. Mehr Speicherverbrauch im Go-Prozess

  • Alle Daten liegen im Heap deiner Go-Applikation
  • Je mehr du speicherst, desto größer wird der Prozess und desto mehr Arbeit hat der Garbage Collector

Fazit: In-Memory in Go ist super für Cache, kurzfristige Daten oder Tests – aber nicht ideal für geteilten, dauerhaften Speicher.


3. Einführung in Redis als Externe In-Memory-Datenbank

Redis ist eine In-Memory-Datenbank, die speziell dafür gebaut wurde:

  • Daten im RAM zu halten (sehr schnell)
  • Von vielen Services gleichzeitig verwendet zu werden
  • Optional Daten auf Platte zu persistieren (Snapshots, AOF)

Anstatt unsere Daten in einer lokalen Go-map zu behalten, speichern wir sie im Redis-Server. Alle Instanzen deiner Anwendung reden mit demselben Redis.


4. Go + Redis: Ein Einfaches Beispiel

Wir verwenden das beliebte Go-Client-Package github.com/redis/go-redis/v9.

4.1. Abhängigkeit Hinzufügen

go get github.com/redis/go-redis/v9
Enter fullscreen mode Exit fullscreen mode

4.2. Einen Einfachen Redis-Client in Go Bauen

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

var ctx = context.Background()

type RedisStore struct {
    client *redis.Client
}

func NewRedisStore(addr string) *RedisStore {
    rdb := redis.NewClient(&redis.Options{
        Addr:     addr,  // z.B. "localhost:6379"
        Password: "",    // kein Password per Default
        DB:       0,     // Default-DB
    })

    // Testverbindung
    if err := rdb.Ping(ctx).Err(); err != nil {
        log.Fatalf("Redis Verbindung fehlgeschlagen: %v", err)
    }

    return &RedisStore{client: rdb}
}

func (s *RedisStore) Set(key, value string, ttl time.Duration) error {
    // TTL = 0 bedeutet "ohne Ablaufdatum"
    return s.client.Set(ctx, key, value, ttl).Err()
}

func (s *RedisStore) Get(key string) (string, bool, error) {
    val, err := s.client.Get(ctx, key).Result()
    if err == redis.Nil {
        // Key existiert nicht
        return "", false, nil
    }
    if err != nil {
        return "", false, err
    }
    return val, true, nil
}

func main() {
    store := NewRedisStore("localhost:6379")

    // Beispiel: Key mit TTL von 1 Minute speichern
    if err := store.Set("username", "alice", time.Minute); err != nil {
        log.Fatalf("Set fehlgeschlagen: %v", err)
    }

    val, ok, err := store.Get("username")
    if err != nil {
        log.Fatalf("Get fehlgeschlagen: %v", err)
    }
    if ok {
        fmt.Println("Username:", val)
    } else {
        fmt.Println("Key nicht gefunden")
    }
}
Enter fullscreen mode Exit fullscreen mode

Wichtige Punkte:

  • NewRedisStore baut den Client und prüft direkt mit PING, ob Redis erreichbar ist
  • Set unterstützt eine TTL (Time-to-Live)
  • Get unterscheidet zwischen "Key existiert nicht" (redis.Nil) und echten Fehlern

5. Vergleich: In-Memory vs. Redis

5.1. Architektur

In-Memory (Go-Map):

  • Daten liegen im gleichen Prozess wie deine App
  • Schnell, aber nur lokal verfügbar
  • Kein Sharing zwischen Instanzen

Redis:

  • Externer Service (Docker, VM, Kubernetes, ...)
  • Alle Instanzen deiner App greifen auf denselben Speicher zu
  • Besser geeignet für Skalierung und geteilten Zustand

5.2. Neustarts und Deployments

In-Memory:

  • Beim Restart sind alle Daten weg

Redis:

  • Redis kann so konfiguriert werden, dass Daten über Neustarts hinweg erhalten bleiben (Persistence)
  • Deine Go-App-Instanzen können ohne Datenverlust neu deployt werden

5.3. Typische Use-Cases

In-Memory in Go:

  • Kleine Caches innerhalb eines Requests
  • Kurzfristige Daten, die nur für eine Instanz relevant sind
  • Tests und Prototypen

Redis:

  • Session-Speicher (z.B. für Web-User)
  • Token-Storage (Access-Tokens, Refresh-Tokens, API-Keys)
  • Rate-Limiting (Zählung von Aufrufen pro IP/User)
  • Shared Cache zwischen mehreren Services

6. Schrittweise Migration: Von In-Memory zu Redis

Wenn du schon einen In-Memory-Store hast, kannst du relativ leicht auf Redis umstellen:

6.1. Gemeinsames Interface Definieren

type Store interface {
    Set(key, value string) error
    Get(key string) (string, bool, error)
}
Enter fullscreen mode Exit fullscreen mode

6.2. In-Memory-Implementierung

type InMemoryStore struct {
    mu   sync.RWMutex
    data map[string]string
}

func (s *InMemoryStore) Set(key, value string) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.data[key] = value
    return nil
}

func (s *InMemoryStore) Get(key string) (string, bool, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    val, ok := s.data[key]
    return val, ok, nil
}
Enter fullscreen mode Exit fullscreen mode

6.3. Redis-Implementierung, die Dasselbe Interface Erfüllt

type RedisStore struct {
    client *redis.Client
}

func (s *RedisStore) Set(key, value string) error {
    return s.client.Set(ctx, key, value, 0).Err()
}

func (s *RedisStore) Get(key string) (string, bool, error) {
    val, err := s.client.Get(ctx, key).Result()
    if err == redis.Nil {
        return "", false, nil
    }
    if err != nil {
        return "", false, err
    }
    return val, true, nil
}
Enter fullscreen mode Exit fullscreen mode

6.4. Nutzung im Code

func main() {
    // Für lokale Entwicklung: InMemory
    // var store Store = &InMemoryStore{data: make(map[string]string)}

    // Für Produktion: Redis
    var store Store = NewRedisStore("localhost:6379")

    _ = store.Set("foo", "bar")
    val, ok, _ := store.Get("foo")
    if ok {
        fmt.Println("foo =", val)
    }
}
Enter fullscreen mode Exit fullscreen mode

Jetzt kannst du je nach Umgebung (Dev/Prod) entscheiden, ob du In-Memory oder Redis verwendest, ohne Business-Logik zu ändern.


7. Fazit

  • In-Memory-Speicherung in Go ist ein guter, einfacher Einstieg – aber nur für einzelne Instanzen und nicht-kritische Daten
  • Mit Redis verschiebst du den Zustand aus deinem Go-Prozess in einen zentralen, gemeinsamen Speicher:
    • Mehrere Instanzen können dieselben Daten sehen
    • Neustarts deiner Go-Services verlieren keine wichtigen Daten
    • Du bekommst Features wie TTL, Persistenz und fortgeschrittene Datentypen (Listen, Sets, Hashes, ...)

Für den nächsten Schritt kannst du ausprobieren:

  • Redis in Docker starten
  • Deine Go-App im Docker-Compose-Verbund mit Redis laufen lassen
  • Komplexere Strukturen in Redis nutzen (z.B. Hashes für User-Profile)

So baust du Stück für Stück von einem einfachen In-Memory-Prototypen zu einer robusteren, produktionsreifen Architektur auf.


Weitere Ressourcen

Top comments (0)