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. 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.And I know that I can create a wrapper for unsupported types. My problem is exactly that – I don't know what types are supported. Is error supported, for example? Should I create a wrapper for it? And, as a handler author, should I support it directly or not?
Now, please point me to the place in the documentation that says if I can or can't use a value implementing the error interface as an attribute value, and will the handler or something else would call the `Error() string` method.
My definition of "supported" is simple – I could pass a supported value to the logger and get a reasonable representation from any handler. In my example, the JSON handler does not provide it for the fmt.Stringer.
> Values are formatted as with an encoding/json.Encoder with SetEscapeHTML(false), with two exceptions.
> First, an Attr whose Value is of type error is formatted as a string, by calling its Error method. Only errors in Attrs receive this special treatment, not errors embedded in structs, slices, maps or other data structures that are processed by the encoding/json package.
So the json handler more or less works as if you called json.Marshal, which sounds pretty reasonable.
for a reasonable substitute subset, use the core language types, and implement LogValuer for anything complex.
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.
Does it, though? Why would the log producer care about how the log entires are formatted? Only the log consumer cares about that.
As with everything in life, there are tradeoffs to that approach, of course, and it might be hard to grasp if you come from languages which different idioms that prioritize producer over consumer, but if you look closely everything about Go is designed to prioritize the needs of the consumer over the needs of the producer. That does seem to confuse a lot of people, interestingly. I expect because it isn't idiomatic to prioritize the consumer in a lot of other languages and people get caught up in trying to write code in those other languages using Go syntax instead of actually learning Go.