fmt, log, and log/slog
fmt, log, and log/slog
Overview
Three packages cover output:
| Package | Role |
|---|---|
fmt |
Formatting values for humans and strings |
log |
Legacy line logger (still fine for tiny tools) |
log/slog |
Structured logging (Go 1.21+ default for services) |
Rule of thumb: libraries return errors; programs log. Prefer slog for anything that runs as a service or daemon.
fmt Essentials
fmt.Print("a", 1) // a1 (no spaces between args of different kinds as you'd expect — use Printf)
fmt.Println("hello", 42) // hello 42\n
s := fmt.Sprintf("%s=%d", "n", 3)
fmt.Fprintf(w, "status=%v\n", err)Verbs you actually need
| Verb | Meaning |
|---|---|
%v |
Default format |
%+v |
Struct with field names |
%#v |
Go-syntax representation |
%T |
Type |
%s / %q |
String / quoted |
%d / %x |
Decimal / hex |
%f / %g |
Float |
%w |
Wrap error (fmt.Errorf) |
%+v on errors |
Implementation-defined detail |
err := fmt.Errorf("open %s: %w", path, err)Strings and builders
For hot paths assembling many pieces, prefer strings.Builder or fmt.Fprintf into a builder — not repeated + in a loop.
log (legacy)
import "log"
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("starting")
log.Printf("port=%d", port)
log.Fatal(err) // log + os.Exit(1)Fine for CLIs and examples. Lacks structured fields and levels as first-class data.
log/slog (preferred)
import "log/slog"
slog.Info("server start", "addr", addr)
slog.Error("query failed", "err", err, "query", q)Handlers
// Human text (dev)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
// JSON (prod)
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})))Levels and groups
slog.Debug("cache hit", "key", key)
slog.Warn("retrying", "attempt", n)
logger := slog.Default().With("service", "billing")
logger.Info("charge", "user_id", id, "cents", 199)Context-aware logging
func handle(ctx context.Context, w http.ResponseWriter, r *http.Request) {
log := slog.Default()
if rid, ok := ctx.Value(requestIDKey).(string); ok {
log = log.With("request_id", rid)
}
log.Info("request", "path", r.URL.Path, "method", r.Method)
}For production correlation patterns, see Structured logging.
Anti-patterns
- Logging and returning the same error without a policy — pick one place that owns the user-visible message.
- Logging secrets — tokens, passwords, full auth headers.
fmt.Sprintfthen log the string — lose structure; pass key-value pairs.log.Fataldeep in libraries — exits the process; returnerrorinstead.
Summary
| Situation | Use |
|---|---|
| Build a message string | fmt.Sprintf / Errorf |
| One-off script noise | log or fmt |
| Service / long-running tool | log/slog |
| Wrap errors | fmt.Errorf("...: %w", err) |
Runnable example
Save as main.go:
go mod init example
go run .package main
import (
"fmt"
"log/slog"
"os"
)
func main() {
// fmt: formatting
name := "gopher"
fmt.Printf("hello %s\n", name)
fmt.Printf("type=%T value=%#v\n", name, struct{ N int }{N: 7})
err := fmt.Errorf("open config: %w", os.ErrNotExist)
fmt.Printf("wrapped: %v\n", err)
// slog text
text := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
text.Debug("boot", "pid", os.Getpid())
text.Info("ready", "port", 8080)
// slog JSON
jsonLog := slog.New(slog.NewJSONHandler(os.Stdout, nil))
jsonLog.Info("request",
"method", "GET",
"path", "/health",
"status", 200,
"ms", 3,
)
// grouped attrs
svc := jsonLog.With("service", "demo")
svc.Warn("slow dependency", "dep", "db", "ms", 120)
}Expected output (timestamps/pid vary):
hello gopher
type=string value=struct { N int }{N:7}
wrapped: open config: file does not exist
level=DEBUG msg=boot pid=...
level=INFO msg=ready port=8080
{"time":"...","level":"INFO","msg":"request","method":"GET","path":"/health","status":200,"ms":3}
{"time":"...","level":"WARN","msg":"slow dependency","service":"demo","dep":"db","ms":120}
What to notice: - %#v is invaluable when debugging unexpected types. - slog fields are queryable; free-form strings are not. - Same event shape works for text (local) and JSON (shipped).
Try next: Add a custom slog.Handler wrapper that redacts any attribute key named password or token.