Go’s log/slog package, introduced in Go 1.21, fundamentally shifts how we think about logging by treating log records not as strings, but as structured data.

Let’s see it in action. Imagine a simple web server:

package main

import (
	"log/slog"
	"net/http"
	"os"
)

func main() {
	// Use the default JSON handler for structured output
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	slog.SetDefault(logger) // Make this logger the global default

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// Log request details as structured attributes
		slog.Info("Incoming request",
			"method", r.Method,
			"url", r.URL.String(),
			"remote_addr", r.RemoteAddr,
		)
		w.Write([]byte("Hello, world!"))
	})

	slog.Info("Starting server", "port", "8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		slog.Error("Server failed", "error", err)
	}
}

When a request comes in, the output on stdout will look something like this:

{"time":"2023-10-27T10:30:00.123Z","level":"INFO","msg":"Incoming request","method":"GET","url":"/","remote_addr":"127.0.0.1:54321"}

This isn’t just a string; it’s a JSON object where each piece of information is a distinct key-value pair. This structure is slog’s superpower.

The core problem slog solves is the ambiguity and difficulty of parsing traditional line-based logs. When you have logs like INFO: Processing user ID 12345 took 250ms, how do you easily query for all requests that took longer than 200ms, or filter by userID? You’d need complex regex or string parsing. slog eliminates this by making the data inherently queryable.

Internally, slog uses slog.Logger as the primary interface. You create a Logger by providing a slog.Handler. The Handler is responsible for taking the structured slog.Record (which contains the log level, message, source code location, and attributes) and formatting it for output. The standard library provides slog.JSONHandler (which we used above) and slog.TextHandler. You can also write custom handlers.

The slog.Level enum (LevelDebug, LevelInfo, LevelWarn, LevelError) controls the verbosity. You set the minimum level for your handler. For example, to only see WARN and ERROR messages:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelWarn,
}))

The slog.Attr is the fundamental unit of structured data. You pass them as key-value pairs to the logging functions like slog.Info("message", "key1", value1, "key2", value2). slog automatically converts common Go types into their structured representations. For custom types, you can implement the slog.LogValuer interface.

The slog.Default() function returns a logger that’s globally accessible. slog.Info(), slog.Error(), etc., use this default logger. It’s convenient for quick logging, but for more controlled scenarios, especially in libraries or different modules, it’s best to create and pass around explicit slog.Logger instances. This allows each part of your application to have its own logger with specific configurations or handlers.

The most surprising thing about slog is how effortlessly it integrates with context.Context. You can pass context down through your call stack, and any logger created with logger.WithContext(ctx) will automatically include context-specific values (like request IDs) in its logs. This means you don’t have to manually pass requestID to every single logging call; it’s implicitly carried along.

func processRequest(ctx context.Context) {
    reqID := ctx.Value("requestID").(string) // Assume this is set elsewhere
    logger := slog.Default().WithContext(ctx) // Logger will now include context values

    logger.Info("Processing started", "request_id", reqID)
    // ... do work ...
    logger.Info("Processing finished")
}

When slog.Info("Processing started", "request_id", reqID) is called within processRequest where the logger was created with WithContext, the output will automatically include the request_id if it was present in the context and the logger was configured to propagate it. This implicit propagation through context is a powerful, often overlooked, feature for maintaining contextual logging across complex call chains.

The next step in mastering slog is understanding how to write custom handlers to integrate with external logging systems or perform complex log manipulation.

Want structured learning?

Take the full Golang course →