Anatomy of a log/slog logger

April 1, 2026

Unlike the older log package, which provides a single *log.Logger type as its primary interface, log/slog has a two-tiered architecture. This is roughly the same architecture used by the database/sql package: One interface implements a handler (or “driver” for database/sql), and another interface is consumed. The package itself provides the intermediate translation.

This is essentially a localized example of ports-and-adaptors or hexagonal architecture.

Here’s how the GoDoc for the package explains it:

Overview

Package slog provides structured logging, in which log records include a message, a severity level, and various other attributes expressed as key-value pairs.

It defines a type, Logger, which provides several methods (such as Logger.Info and Logger.Error) for reporting events of interest.

Each Logger is associated with a Handler. A Logger output method creates a Record from the method arguments and passes it to the Handler, which decides how to handle it. There is a default Logger accessible through top-level functions (such as Info and Error) that call the corresponding Logger methods.

As a consumer of the package (the application doing things that should be logged), you’ll primarily concern yourself with the slog.Logger interface.

If, on the other hand, you want to write your logs in some novel way (translated to Pig Latin?), or to a specialized logging backend (your custom columnar database?), you’d likely be writing a custom implementation of the slog.Handler interface.

As a log/slog consumer, generally your only interaction with a handler is with instantiation:

logger := slog.New(MyCustomHandler())

Pretty simple… unless you are actually writing a log handler. And we’ll get there in this series. Eventually. 😉


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


slog.Handler.WithGroup

I hope everyone had a good Labor Day weekend… even if you’re not in the US and had to/got to labor on Monday. Speaking of labor, I’m looking for new clients! If you could use some world class Go expertise on your project, please reach out! type Handler type Handler interface { … // WithGroup returns a new Handler with the given group appended to // the receiver's existing groups. // The keys of all subsequent attributes, whether added by With or in a // Record, should be qualified by the sequence of group names.


slog.Handler.WithAttrs

type Handler type Handler interface { … // WithAttrs returns a new Handler whose attributes consist of // both the receiver's attributes and the arguments. // The Handler owns the slice: it may retain, modify or discard it. WithAttrs(attrs []Attr) Handler This is likely the most interesting and subtle method of the slog.Handler interface. The important thing to note is that the handler is not mutated in place. Rather, a new handler is meant to be returned.


slog.Handler.Handle()

I had not intended to take a month off, but then GopherCon came, and life happened… but now I’m back, and ready to rock and roll again… Today we’re to the core of the Handler interface: the Handle method. type Handler type Handler interface { … // Handle handles the Record. // It will only be called when Enabled returns true. // The Context argument is as for Enabled. // It is present solely to provide Handlers access to the context's values.

Get daily content like this in your inbox!

Subscribe