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 .

Get daily content like this in your inbox!

Subscribe