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)
}
}
Was passiert hier?
-
map[string]stringhält unsere Daten im RAM -
sync.RWMutexschü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
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")
}
}
Wichtige Punkte:
-
NewRedisStorebaut den Client und prüft direkt mitPING, ob Redis erreichbar ist -
Setunterstützt eine TTL (Time-to-Live) -
Getunterscheidet 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)
}
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
}
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
}
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)
}
}
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.
Top comments (0)