Logging in Go with Slog: A Practitioner's Guide
18 comments
·September 8, 2025aleksi
0x696C6961
All values are supported.
aleksi
Well, is fmt.Stringer supported? The result might surprise you:
req := expvar.NewInt("requests")
req.Add(1)
attr := slog.Any("requests", req)
slog.New(slog.NewTextHandler(os.Stderr, nil)).Info("text", attr)
slog.New(slog.NewJSONHandler(os.Stderr, nil)).Info("json", attr)
This code produces time=2025-09-12T13:15:42.125+02:00 level=INFO msg=text requests=1
{"time":"2025-09-12T13:15:42.125555+02:00","level":"INFO","msg":"json","requests":{}}
So the code that uses slog but does not know what handler will be used can't rely on it lazily calling the `String() string` method: half of the standard handlers do that, half don't.Philip-J-Fry
That seems to work as expected?
The output of data is handled by the handler. Such behaviour is clearly outlined in the documentation by the JSONHandler. I wouldn't expect a JSONHandler to use Stringer. I'd expect it to use the existing JSON interfaces, which it does.
I'd expect the Text handler to use TextMarshaller. Which it does. Or Stringer, which it does implicitly via fmt.Sprintf.
0x696C6961
If you need more control, you can create a wrapper type that implements `slog.LogValuer`
type StringerValue struct {
fmt.Stringer
}
func (v StringerValue) LogValue() slog.Value {
return slog.StringValue(v.String())
}
Usage example: slog.Any("requests", StringerValue{req})
There might be a case for making the expvar types implement `slog.LogValuer` directly.arcaen
The thing that gets me about slog is that the output key for the slog JSON handler is msg, but that's not compatible with Googles own GCP Stackdriver logging. Since that key is a constant I now need to use an attribute replacer to change it from msg to message (or whatever it is stackdriver wants). Good work Google.
codeduck
I'm surprised this isn't a standard base pattern in languages, to be honest. Apache's commons-logging library was a standard part of enterprise java placements for many years, and only started to go away when Log4J came along.
lmz
Log4j is one of the possible backends for commons logging (and was basically the reason for it - choosing between log4j and the built-in java logging). I think you mean SLF4J?
codeduck
I may be remembering it wrong, but I think log4j only became a commons logging backend several years after it became mainstream; before that I remember the two being entirely different and no interchangeable. It's a long time ago!
null
awesome_dude
I have a gripe with slog - it uses magic for config
What I mean is, if you configure slog in (say) your main package, then, by magic, that config is used by any call to slog within your application.
There's no "Oh you are using this instance of slog that has been configured to have this behaviour" - it's "Oh slog got configured so that's the config you have been given"
I've never tried to see if I can split configs up, and I don't have a usecase, it just strikes me as magic is all
roncesvalles
There's a default logger that's used when you call package-level functions (as opposed to methods on an instance of slog.Logger). The default logger is probably what you configured in your main package.
In my opinion this is perfectly idiomatic Go. Sometimes the package itself hosts one global instance. If you think that's "magic" then you must think all of Go is magic. It helps to think of a package in Go as equivalent to a single Java class. Splitting up a Go package's code into multiple files is purely cosmetic.
sethammons
If you need log output in tests, you should use an instance of a logger.
More teams should be validating their logging and should be leveraging structured logging and getting valuable logging insights/events.
catlifeonmars
I’m not sure I understand what you mean by “magic for config”. You create and configure a logger using slog.New(…). You can use the default logger instead, slog.Default(), which is just a global and has a default config. You can also set the default logger using slog.SetDefault(…).
It’s extremely unmagical in my opinion.
gdbsjjdn
The "magic" is just global state? I agree that I try to avoid global state in my code but it's hardly Spring Boot levels of auto wiring bullshit.
imiric
I'm a big fan of slog, and this is a great overview.
The fact it is so flexible and composable, while still maintaining a simple API is just great design. I wasn't aware of the performance overhead compared to something like zerolog, but this shouldn't be a concern for most applications.
My biggest gripe with slog is that there is no clear guidance on supported types of attributes.
One could argue that supported types are the ones provided by Attr "construct" functions (like slog.String, slog.Duration, etc), but it is not enough. For example, there is no function for int32 – does it mean it is not supported? Then there is slog.Any and some support in some handlers for error and fmt.Stringer interfaces. The end result is a bit of a mess.