slog in einem Beispiel
log/slog (Go 1.21) schreibt strukturierte Datensätze: eine Nachricht, ein Level und Schlüssel-Wert-Attribute.
Jede Zeile kommt als time=... level=INFO msg="user logged in" user=ada attempts=1 heraus. Weil jeder Wert ein eigenes Feld ist, kann ein Log-Collector (Loki, Elasticsearch, CloudWatch, Datadog) nach user=ada oder level=ERROR filtern, ohne reguläre Ausdrücke über Freitext.
Die Beispiele auf dieser Seite schreiben nach os.Stdout, damit die Ausgabe in der richtigen Reihenfolge erscheint. In einem echten Service gehen Logs meist nach os.Stderr, und dorthin schreibt auch der Standard-Logger.
Levels
| Level | Wert | Wofür |
|---|---|---|
slog.LevelDebug | -4 | Details für Entwickler, in Produktion aus |
slog.LevelInfo | 0 | normale Ereignisse: gestartet, Request bedient, Job fertig |
slog.LevelWarn | 4 | etwas Unerwartetes, das das Programm behandelt hat |
slog.LevelError | 8 | eine Operation ist fehlgeschlagen |
Der Handler verwirft Datensätze unterhalb seines Mindest-Levels, und der Standard ist Info. Deshalb gibt slog.Debug(...) nichts aus, bis du einen Handler mit Level: slog.LevelDebug konfigurierst. Die Lücken zwischen den Werten lassen Platz für eigene Levels wie slog.Level(2).
Um das Level zur Laufzeit zu ändern (per Flag, über einen Admin-Endpoint oder ein Signal), leg eine slog.LevelVar in die Optionen und ruf später Set darauf auf:
var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written
Text oder JSON
slog.NewTextHandler schreibt key=value-Paare, gut lesbar im Terminal. slog.NewJSONHandler schreibt ein JSON-Objekt pro Zeile, das Format, das die meisten Log-Pipelines erwarten. Die Logging-Aufrufe bleiben gleich; nur der Handler ändert sich.
Werte behalten ihre Typen: status ist in der JSON-Ausgabe eine Zahl und retry ein Boolean, eine time.Duration erscheint im Text als 42ms und in JSON als Nanosekunden, und ein error gibt seine Meldung aus. ReplaceAttr ist der Hook zum Umschreiben oder Entfernen von Attributen, hier genutzt, um den Zeitstempel wegzulassen, in der Praxis, um Schlüssel umzubenennen (msg zu message) oder Werte zu schwärzen.
Attribute
Die lose typisierte Form wechselt Schlüssel und Werte ab: "user", "ada", "attempts", 3. Sie ist kurz und hat genau eine Fehlerquelle: eine ungerade Anzahl von Argumenten. Der übrige Wert wird unter dem Schlüssel !BADKEY geloggt. go vet findet das:
./main.go:14:2: call to slog.Info missing a final value
Für Typsicherheit und etwas weniger Allokation nimm die Attribut-Konstruktoren und LogAttrs, wenn du in einem heißen Pfad loggst:
logger.Info("order placed",
slog.Int("order_id", 1017),
slog.String("currency", "EUR"),
slog.Float64("total", 59.90),
slog.Duration("took", elapsed),
)
logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))
Verwende in der ganzen Codebasis eine Namenskonvention für Schlüssel (überall user_id, nicht userID in einem Paket und uid in einem anderen). Die Abfragen in deinem Log-System hängen davon ab.
With: Logger, die Kontext mitführen
logger.With(attrs...) gibt einen neuen Logger zurück, der diese Attribute jedem Datensatz hinzufügt. Erzeuge einen pro Request oder pro Job, und jede Zeile, die er schreibt, lässt sich zuordnen:
Jede Zeile trägt service, version, request_id und user, ohne dass du sie bei jedem Aufruf wiederholst. slog.Group verschachtelt Attribute; der JSON-Handler schreibt sie als verschachteltes Objekt ("payment":{"amount":25,"currency":"USD"}) und der Text-Handler als Schlüssel mit Punkt (payment.amount=25). logger.WithGroup("db") legt jedes spätere Attribut dieses Loggers unter eine Gruppe.
Reich den Logger für den Request als Parameter oder Struct-Feld nach unten weiter. Ihn in einem context.Context zu speichern ist möglich, verbirgt aber die Abhängigkeit; die Methoden InfoContext(ctx, ...) von slog geben den Context an den Handler weiter, und ein eigener Handler kann daraus Trace-IDs holen.
Geheimnisse mit LogValuer verbergen
Ein Typ kann steuern, wie er geloggt wird, indem er slog.LogValuer implementiert. So bleiben Passwörter und Tokens aus den Logs heraus, egal wer den Wert loggt:
User loggt nur seine ID und E-Mail, und ein einzeln geloggter Token erscheint als REDACTED. Der Handler ruft LogValue erst auf, wenn der Datensatz tatsächlich geschrieben wird, also funktioniert es auch für Werte, deren Berechnung teuer ist.
Das klassische Paket log
log ist älter als slog und für kleine Programme und Skripte nach wie vor in Ordnung. Es schreibt Zeilen mit einem Präfix aus Datum und Uhrzeit auf die Standardfehlerausgabe:
| Flag | Fügt hinzu |
|---|---|
log.LstdFlags (der Standard) | Datum und Uhrzeit 2009/11/10 23:00:00 |
log.Lmicroseconds | Mikrosekunden an der Uhrzeit |
log.LUTC | Uhrzeit in UTC |
log.Lshortfile / log.Llongfile | main.go:14 / den vollen Pfad |
log.Lmsgprefix | setzt das Präfix vor die Nachricht statt an den Zeilenanfang |
Drei Funktionsgruppen beenden das Programm oder lösen eine Panic aus, und der Unterschied zählt:
log.Fatal,log.Fatalf,log.Fatallngeben aus und rufen dannos.Exit(1)auf. Per defer eingeplante Aufrufe laufen nicht. Nutz sie inmainfür Fehler beim Start, nie in Bibliothekscode oder Request-Handlern.log.Panicund Verwandte geben aus und lösen dann eine Panic aus, also laufen die defer-Aufrufe, und die Panic lässt sich abfangen.- Jede andere Funktion schreibt einfach eine Zeile.
Um in eine Datei zu loggen, öffne sie und übergib sie an log.New oder log.SetOutput; io.MultiWriter(os.Stderr, f) schreibt in beide.
log und slog zusammen
slog.SetDefault(logger) macht logger zum Standard für die Funktionen slog.Info auf oberster Ebene und leitet auch die Ausgabe des Pakets log durch ihn. Vorhandene Aufrufe von log.Printf in deinem Code oder in Abhängigkeiten kommen dann als strukturierte Datensätze auf Level Info heraus:
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}
Vor SetDefault schreibt der Standard-Logger von slog über das Paket log, deshalb gibt ein schlichtes slog.Info("hi") 2026/09/23 14:30:00 INFO hi aus.
Praktische Regeln
- Einen Fehler loggen oder zurückgeben, nicht beides. Eine Funktion, die einen Fehler loggt und zurückgibt, sorgt dafür, dass derselbe Fehlschlag auf jeder Ebene des Aufrufstapels geloggt wird. Gib Fehler mit Kontext nach oben zurück und logge sie einmal dort, wo sie behandelt werden.
- Variable Daten in Attribute, nicht in die Nachricht.
logger.Info("user created", "user_id", id)lässt sich im Log-System gut gruppieren;logger.Info(fmt.Sprintf("user %d created", id))erzeugt für jeden Nutzer eine andere Nachricht. - Nie Geheimnisse oder vollständige Request-Bodys loggen. Schwärze mit
LogValueroderReplaceAttr. - JSON in Produktion, Text in der Entwicklung. Wähl den Handler beim Start per Flag oder Umgebungsvariable.
- Levels bewusst wählen. Wird alles auf Error geloggt, werden Alarme bei Fehlern zu Rauschen.
Häufig gestellte Fragen
Was ist slog in Go?
log/slog ist das Paket für strukturiertes Logging, das mit Go 1.21 in die Standardbibliothek kam. Statt formatierter Strings hat jeder Datensatz eine Nachricht, ein Level (Debug, Info, Warn, Error) und Schlüssel-Wert-Attribute, und ein Handler schreibt ihn als key=value-Text oder als JSON: slog.Info("login", "user", "ada", "attempts", 3).
Wie aktiviere ich Debug-Logs in slog?
Das minimale Standard-Level ist Info, also gibt slog.Debug nichts aus. Erzeuge einen Handler mit niedrigerem Level und mach ihn zum Standard: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Nimm eine slog.LevelVar statt einer Konstanten, wenn du das Level während der Laufzeit ändern willst.
Was ist der Unterschied zwischen log und slog in Go?
log schreibt frei formatierte Zeilen mit optionalem Zeitstempel-Präfix und kennt keine Levels. slog schreibt Datensätze mit Levels und typisierten Schlüssel-Wert-Attributen, die Log-Collectoren parsen und filtern können. Beide gehören zur Standardbibliothek; slog.SetDefault leitet auch die Ausgabe des Pakets log durch den slog-Handler.
Führt log.Fatal defer-Funktionen aus?
Nein. log.Fatal und log.Fatalf geben die Nachricht aus und rufen os.Exit(1) auf, was alle per defer eingeplanten Aufrufe überspringt. Nutz sie nur in main oder in Setup-Code, in dem es nichts aufzuräumen gibt. log.Panic löst stattdessen eine Panic aus, also laufen die defer-Aufrufe.