Go cheatsheet covering the go toolchain, syntax essentials, goroutines and channels, table-driven …
Go Logging Cheatsheet (log/slog) Go Logging Cheatsheet (log/slog)

Summary
log/slog landed in Go 1.21 and settled a decade of everyone picking a different logging library. It is structured by default, it has levels, and it is in the standard library — which means a package can log without dragging a dependency into your build. This sheet is the configuration I use and the handful of traps, the sharpest being that the convenient call signature cannot be checked by the compiler. For the language itself see the Go Cheatsheet.
*slog.Logger or calls slog.Default(); only
main calls slog.SetDefault.The Two Handlers
Docs: log/slog package
Expand your knowledge with Ansible Cheatsheet
import ("log/slog"; "os")
// Human-readable, for a terminal
t := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// One JSON object per line, for CloudWatch / Loki / anything that indexes
j := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true, // file:line, costs a little
}))
slog.SetDefault(j) // in main only; then slog.Info(...) anywhere
$ go run slogdemo.gotime=2026-10-29T17:56:40.353Z level=INFO msg=deployed env=live replicas=3{"time":"2026-10-29T17:56:40.353213471Z","level":"INFO","msg":"deployed","service":"api","version":"1.4.2","env":"live","replicas":3}{"time":"2026-10-29T17:56:40.353219329Z","level":"WARN","msg":"retrying","service":"api","version":"1.4.2","attempt":2}# Same record, two renderings. Choose by destination, not by taste:# text to a terminal, JSON to anything that will index the fields.Levels
slog.LevelDebug // -4
slog.LevelInfo // 0 the default
slog.LevelWarn // 4
slog.LevelError // 8
// A level you can change at runtime, without rebuilding the handler
lvl := new(slog.LevelVar) // starts at Info
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl})
lvl.Set(slog.LevelDebug) // flip it from a signal handler or an endpoint
if slog.Default().Enabled(ctx, slog.LevelDebug) {
slog.Debug("expensive", "dump", buildDump()) // skip the work when off
}
Custom levels are just numbers: const LevelTrace = slog.Level(-8).
Deepen your understanding in Python Logging Cheatsheet
Attributes, and the One Real Trap
Two call styles. The short one is variadic ...any alternating key and value,
which the compiler cannot check:
Explore this further in Bash Basics Cheatsheet
$ go run slogdemo.go{"time":"...","level":"INFO","msg":"odd number of args","service":"api","!BADKEY":"env"}{"time":"...","level":"INFO","msg":"typed, no mistake possible","service":"api","env":"live"}# The first call was slog.Info("odd number of args", "env") — a dangling# key with no value. It compiles, it logs, and the field is garbage.@@ -4,5 +4,7 @@ func deploy(env string, n int) {- slog.Info("deployed", "env", env, "replicas", n)- slog.Info("deployed", "env") // compiles. Logs !BADKEY.+ slog.Info("deployed",+ slog.String("env", env),+ slog.Int("replicas", n)) // a missing value is now a+ // compile error, not a log lineslog.String("env", env) slog.Int("replicas", n)
slog.Bool("dry_run", d) slog.Duration("took", elapsed)
slog.Time("at", t) slog.Float64("ratio", r)
slog.Any("cfg", cfg) // reflection; fine, but typed is better
slog.Group("http", // nests: {"http":{"method":"GET","status":200}}
slog.String("method", m), slog.Int("status", s))
vet catches some of these. go vet ./... reports a dangling key in a
slog call as call to slog.Info missing a final value. It does not catch
every shape, which is why the typed constructors are still worth the keystrokes
on anything important.Context: Attributes That Follow a Request
log := slog.Default().With(
slog.String("service", "api"),
slog.String("version", version),
) // every record from here carries both
func handler(w http.ResponseWriter, r *http.Request) {
log := log.With(slog.String("request_id", reqID(r)))
log.InfoContext(r.Context(), "handling", slog.String("path", r.URL.Path))
}
With returns a child logger; the parent is untouched. The …Context
variants (InfoContext, ErrorContext) pass the context.Context to the
handler, which is how a custom handler can pull a trace ID out of it.
Discover related concepts in Python Logging Cheatsheet
type traceHandler struct{ slog.Handler }
func (h traceHandler) Handle(ctx context.Context, r slog.Record) error {
if id, ok := ctx.Value(traceKey).(string); ok {
r.AddAttrs(slog.String("trace_id", id))
}
return h.Handler.Handle(ctx, r)
}
// WithAttrs and WithGroup must return the wrapper too, or .With() on the
// logger silently drops back to the embedded handler.
func (h traceHandler) WithAttrs(a []slog.Attr) slog.Handler {
return traceHandler{h.Handler.WithAttrs(a)}
}
func (h traceHandler) WithGroup(n string) slog.Handler {
return traceHandler{h.Handler.WithGroup(n)}
}
slog.SetDefault(slog.New(traceHandler{slog.NewJSONHandler(os.Stdout, nil)}))
Embedding slog.Handler gives you Enabled, WithAttrs and WithGroup for
free — but the last two return the embedded handler unless you override them,
which is the bug people hit about ten minutes in.
Errors
if err := deploy(ctx, env); err != nil {
slog.ErrorContext(ctx, "deploy failed",
slog.String("env", env),
slog.Any("err", err)) // Any, so the error's type is preserved
return fmt.Errorf("deploy %s: %w", env, err)
}
Log it or wrap and return it — not both at every level, or one failure appears five times with five stack depths. The usual convention: wrap on the way up, log once at the top where it is handled.
Uncover more details in AWS CloudFormation Cheatsheet
Renaming and Redacting Fields
ReplaceAttr runs over every attribute before it is written. It is how you
match a log schema, and how you keep a secret out of the output:
Journey deeper into this topic with Bash sed & awk Cheatsheet
opts := &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.TimeKey:
a.Key = "ts" // match your schema
case slog.MessageKey:
a.Key = "msg"
case "password", "token", "authorization":
return slog.String(a.Key, "[REDACTED]") // never the value
}
return a
},
}
ReplaceAttr is a backstop, not a policy. It only catches keys you remembered
to name. The rule that actually works is to never pass a whole request, config
struct or environment map as an attribute — log the fields you need, by name.Testing Log Output
func TestDeployLogs(t *testing.T) {
var buf bytes.Buffer
log := slog.New(slog.NewJSONHandler(&buf, nil))
deployWith(log, "live")
var got map[string]any
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
t.Fatal(err)
}
if got["env"] != "live" {
t.Errorf("env = %v, want live", got["env"])
}
}
Structured output is testable output — one of the better arguments for
slog over a formatted string you would have to regex.
Enrich your learning with Bash CLI One-Liners Cheatsheet
Common Pitfalls
slog.SetDefaultin a library or aninit(). It is global; the last package to run wins, and import order decides your logging config.- The old
logpackage’sFatalandPanic.log.Fatalfcallsos.Exit(1), so deferred functions do not run and buffers are not flushed.slogdeliberately has no Fatal. - Logging in a hot loop. Even a filtered-out
Debugevaluates its arguments. Guard withEnabledwhen building the value costs anything. - Interleaved lines from goroutines. Handlers are safe for concurrent use
and write each record in one call, so records do not interleave — but if you
also
fmt.Printlnelsewhere, those will. AddSource: trueeverywhere. It walks the stack for every record. Worth it at Error, rarely worth it at Info in a hot path.
Take your service's last error line. Could a log aggregator filter every occurrence of it by field — or would you need a regex over the message text?
Gain comprehensive insights from Ansible Cheatsheet
Related Cheatsheets
- Go Cheatsheet — syntax, toolchain and stdlib
- Go Concurrency Cheatsheet — logging from many goroutines
- Go Security Cheatsheet — what must never reach a log line
- Python Logging Cheatsheet — the same job, with handlers and dictConfig
Similar Articles
Related Content
More from development
Python basics and CLI cheatsheet: core syntax, f-strings, comprehensions, pathlib, venv, pip, …
Quick-reference Bash basics cheatsheet: variables, parameter expansion, conditionals, loops, arrays, …
You Might Also Like
Go concurrency cheatsheet: goroutines, channels, select, sync.WaitGroup, Mutex, context …
Go security cheatsheet: exec.Command without a shell, crypto/rand vs math/rand, SQL parameters, path …
Knowledge Quiz
Test your general knowledge with this quick quiz!
A set of multiple-choice questions to test your knowledge.
Take as much time as you need.
Your score will be shown at the end.
Question 1 of 5
Quiz Complete!
Your score: 0 out of 5
Loading next question...

