Hands-on with Go’s slog Package
Grace Collins
Solutions Engineer · Leapcell

Preface
Go version 1.21.0 introduced a new package, log/slog
, which provides structured logging functionality. Compared to traditional logging, structured logging is more popular because it offers better readability and significant advantages in processing, analysis, and searching.
The slog Package
The slog package provides structured logs, where each log entry contains a message, severity level, and various other attributes, all represented as key-value pairs.
The main features of the slog package are as follows:
- Structured logging
- Log severity levels
- Custom log handlers
- Log grouping
First Experience
package main import ( "context" "log/slog" ) func main() { slog.Info("slog msg", "greeting", "hello slog") // Carrying context slog.InfoContext(context.Background(), "slog msg with context", "greeting", "hello slog") }
In the above example, we directly output an info-level log by calling the package function slog.Info
. Internally, this function uses a default Logger
instance to perform the logging operation. In addition, you can use slog.InfoContext
to output logs with an associated context.
Besides Info()
and InfoContext()
, there are also functions like Debug()
, Warn()
, and Error()
for logging at different levels.
Running the above program will produce the following output:
2025/06/18 21:08:08 INFO slog msg greeting="hello slog"
2025/06/18 21:08:08 INFO slog msg with context greeting="hello slog"
Creating a Logger
By default, when using slog package functions to output logs, the format is just plain text. If you want to output in JSON or key=value format, you need to create a Logger instance using slog.New()
. When using this function, you must pass in an implementation of slog.Handler
. The slog package provides two implementations: TextHandler
and JsonHandler
.
TextHandler
TextHandler is a log handler that writes log records as a series of key-value pairs to an io.Writer
. Each key-value pair is represented in the form key=value, separated by spaces.
package main import ( "context" "log/slog" "os" ) func main() { textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) textLogger.InfoContext(context.Background(), "TextHandler", "Name", "Leapcell") }
In the above example, we create a log handler using slog.NewTextHandler
. The first parameter, os.Stdout
, indicates that logs will be output to the console. The handler is then passed as a parameter to slog.New
to create a Logger instance, which is used to perform logging operations.
The output of the program is as follows:
time=2025-06-18T21:09:03.912+00:00 level=INFO msg=TextHandler Name=Leapcell
JsonHandler
JsonHandler is a log handler that writes log records in JSON format to an io.Writer
.
package main import ( "context" "log/slog" "os" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) jsonLogger.InfoContext(context.Background(), "JsonHandler", "name", "Leapcell") }
In the example above, we use slog.NewJsonHandler
to create a JSON log handler. The first parameter, os.Stdout
, indicates output to the console. The handler is passed to slog.New
to create a Logger instance, which is then used for logging operations.
The program output is as follows:
{ "time": "2025-06-18T21:09:22.614686104+00:00", "level": "INFO", "msg": "JsonHandler", "name": "Leapcell" }
Global Logger Instance
slog has a default Logger instance. If you want to obtain the default Logger, you can refer to the following code:
logger := slog.Default()
In previous examples, we always used a specifically created Logger instance to output logs. However, if you don’t want to log through a specific Logger instance every time but instead want to operate globally, you can use the slog.SetDefault
function to set and replace the default Logger instance. This makes logging more convenient and flexible.
package main import ( "context" "log/slog" "os" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(jsonLogger) slog.InfoContext(context.Background(), "JsonHandler", "name", "Leapcell") //{"time":"2025-06-18T21:11:22.41760604+00:00","level":"INFO","msg":"JsonHandler","name":"Leapcell"} }
Grouping
Grouping refers to grouping related attributes (key-value pairs) in a log record. Here’s an example:
package main import ( "context" "log/slog" "os" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).WithGroup("information") jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"), slog.Int("phone", 1234567890)) textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)).WithGroup("information") textLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"), slog.Int("phone", 1234567890)) }
The result of running this program is as follows:
{"time":"2025-06-18T21:12:23.124255258+00:00","level":"INFO","msg":"json-log","information":{"name":"Leapcell","phone":1234567890}}
time=2025-06-18T21:12:23.127+00:00 level=INFO msg=json-log information.name=Leapcell information.phone=1234567890
According to the output, if you group a Logger instance with a JsonHandler
, the group name becomes a key, and the value is a JSON object composed of all key-value pairs.
If you group a Logger with a TextHandler
, the group name is combined with the keys of all key-value pairs, and ultimately displayed as groupName.key=value
.
Efficient Logging with LogAttrs
If you need to log frequently, compared to the previous examples, using the slog.LogAttrs
function together with the slog.Attr
type is more efficient, because it reduces the process of type parsing.
package main import ( "context" "log/slog" "os" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) jsonLogger.LogAttrs(context.Background(), slog.LevelInfo, "Efficient log output", slog.String("Name", "Leapcell"), slog.Int("Contact", 12345678901)) }
In the example above, we use the LogAttrs
method to output a log entry. The method’s signature is:
func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)
Based on the signature, the first parameter is a context.Context
, the second parameter is a Level
(the log severity level defined in the slog package), and the third parameter is an Attr
key-value pair.
When using other methods like Info
to output logs, the key-value pairs are internally converted to the Attr
type. By using the LogAttrs
method, you can directly specify the Attr
type, reducing the conversion process, and thus making logging more efficient.
With: Setting Common Attributes
If every log needs to contain the same key-value pair, you can consider setting a common attribute.
package main import ( "context" "log/slog" "os" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger := jsonLogger.With("systemID", "s1") logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k1", "v1")) logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k2", "v2")) }
You can use the With
method to add one or more fixed attributes and return a new Logger instance. Any logs output by this new instance will include the added fixed attributes, thus avoiding the need to add the same key-value pairs to every log statement.
The output of this program is as follows:
{"time":"2025-06-18T21:19:51.338328238+00:00","level":"INFO","msg":"json-log","systemID":"s1","k1":"v1"} {"time":"2025-06-18T21:19:51.338604943+00:00","level":"INFO","msg":"json-log","systemID":"s1","k2":"v2"}
HandlerOptions: Configuration Options for Log Handlers
Careful readers may have noticed that in previous examples, whether using NewJSONHandler
or NewTextHandler
, the second parameter was set to nil
, which means the default configuration is used.
This parameter is of type *HandlerOptions
. With it, you can configure whether to display the source code location of log statements, the minimum log output level, and how to rewrite key-value pair attributes.
package main import ( "context" "log/slog" "os" "time" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelError, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { if t, ok := a.Value.Any().(time.Time); ok { a.Value = slog.StringValue(t.Format(time.DateTime)) } } return a }, })) jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell")) jsonLogger.ErrorContext(context.Background(), "json-log", slog.String("name", "Leapcell")) }
In this example, we create a Logger instance with a JsonHandler
. When creating the JsonHandler
, the following configurations are specified via the HandlerOptions
parameter:
- Output the source code (Source information) of the log statement
- Set the minimum log level to Error
- Rewrite the format of the attribute with key
"time"
to"2006-01-02 15:04:05"
The output of this program is as follows:
{ "time": "2025-06-18 21:21:31", "level": "ERROR", "source": { "function": "main.main", "file": "D:/goproject/src/gocode/play/main.go", "line": 24 }, "msg": "json-log", "name": "Leapcell" }
The output matches expectations: logs of level INFO are not output, the Source information is included, and the value of the "time"
key has been rewritten.
Customizing the Value in Key-Value Pairs
In a previous example, we used the HandlerOptions
configuration to modify the value in a key-value pair. Besides this method, the slog package also supports another way to change the value.
package main import ( "context" "log/slog" ) type Password string func (Password) LogValue() slog.Value { return slog.StringValue("REDACTED_PASSWORD") } func main() { slog.LogAttrs(context.Background(), slog.LevelInfo, "Sensitive Data", slog.Any("password", Password("1234567890"))) }
In the above example, we implement the slog.LogValuer
interface (by adding the LogValue() slog.Value
method to a type), which allows us to override the value of a key-value pair. When logging, the value will be replaced by the return value of the LogValue
method.
The output of this program is as follows:
2025/06/18 21:37:11 INFO Sensitive Data password=REDACTED_PASSWORD
As expected, the value of password
has been changed.
Summary
This article provides a detailed introduction to the slog package in Go, including basic usage, creating Logger instances, efficient logging, and customizing log information.
After reading this article, you should have a deeper understanding of the slog package and be able to use it more effectively to manage and record logs.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ