Lazy attribute evaluation for JSONHandler

May 26, 2026

As I was writing yesterday’s post, a portion of the GoDoc confused me. I’ve now spent over 3 hours with Claude trying to parse the prose grammatically, build test cases, and make general sense of it. I think I finally have… Here’s hoping!

So, yesterday we saw how you can lazy-evaluate some values when using TextHandler. But the proposed solution (pass a fmt.Stringer rather than a literal string) has other, likely uninintended, consequences if you’re using JSONHandler:

Performance considerations

… Avoiding the call to String also preserves the structure of the underlying value. For example JSONHandler emits the components of the parsed URL as a JSON object.

Oops! This means that by passing &r.URL, we get lazy string evaluation for TextHandler, but for JSONHandler, we get:

"url":{"Scheme":"http","Opaque":"","User":null,"Host":"example.com","Path":"","Fragment":"","RawQuery":"","RawPath":"","RawFragment":"","ForceQuery":false,"OmitHost":false}

Meh… that’s probably NOT what you want. So how can you get that lazy-evaluation, but in string format, for JSONHandler?

… If you want to avoid eagerly paying the cost of the String call without causing the handler to potentially inspect the structure of the value, wrap the value in a fmt.Stringer implementation that hides its Marshal methods.

There’s the clue, but it’s opaque—and I believe actually wrong. Let me first explain what I think it’s trying to point us to. To get the lazy-evaluation benefit with JSONHandler, we need to rely on MarshalJSON or MarshalText instead of String. Easily done with a custom type that wraps the url.URL:

type myURL url.URL

func (u *myURL) MarshalJSON() ([]byte, error) {
  return json.Marshal((*url.URL)(u).String())
}

Or you could add a MarshalText method, which would work for both TextHandler and JSONHandler equally.

So there’s the full arc, as I believe it’s meant to be understood. You can stop reading now if you want to.

However, that’s not actually what the doc says.

The doc says “… wrap the value in a fmt.Stringer implementation that hides its Marshal methods.” — The solution we just came up with adds a Marshal method, it doesn’t hide any. And in fact, the url.URL example in the doc doesn’t have marshal methods (if it did, we wouldn’t need this remedy!)

So, I believe the doc simply has a small error, and means to say:

“… wrap the value with an implementation that adds Marshal methods.”

And that’s where I got hung up while trying to make sense of this. Maybe the doc is actually trying to say something else? Maybe it’s talking about data hiding? Maybe you want a custom fmt.Stringer that omits api_key query parameters for example? But that seems to be a stretch for what appears to be an aside in a section about performance—and doubly so, since the prescription points to fmt.Stringer, which is what introduced the problem it’s meant to be solving for JSONHandler. Which is why I think it’s just worded in a confusing/incorrect way.

How do you read it? Is there an interpretation I’ve overlooked?


Share this

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

Unsure? Browse the archive .

Related Content


slog.Handler

I’m probably doing this in backwards order, but that’s the order the GoDoc presents it… Now that we’ve looked at each of the methods of the Handler interface, we come to its general description: type Handler A Handler handles log records produced by a Logger. A typical handler may print log records to standard error, or write them to a file or database, or perhaps augment them with additional attributes and pass them on to another handler.


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.

Get daily content like this in your inbox!

Subscribe