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


Attribute evaluation

log.With isn’t the only trick available for improving performance of logging. Many values you may want to pass to a logger need to be calculated. And sometimes that calculation is expensive. And if a log is omitted, because it’s a debug log, and our logger is only configured for info-and-up level, that calculation should be skipped. Performance considerations … The arguments to a log call are always evaluated, even if the log event is discarded.


Performance considerations

You’ve likely wondered why the log/slog package has some odd-looking functions and concepts in some places. Why do you set a handler’s level to a Leveler value, rather than a simple Level? Why so many ways to create key/value pairs ("key", "value" vs "key", slog.AnyValue("value") vs "key", slog.StringValue("value") vs slog.Any("key", "value") vs slog.String("key", "value"))? It mostly comes down to one thing: Performance. Or, more accurately, trying to balance performance with an easy-to-use API.


Working with Records

In my experience, it’s rare you’ll need to worry about slog Records, unless you’re writing a Handler, or some kind of middleware/transform. But even if you never need to manipulate a Record directly, understanding the concept can be useful. Working with Records Sometimes a Handler will need to modify a Record before passing it on to another Handler or backend. A Record contains a mixture of simple public fields (e.g. Time, Level, Message) and hidden fields that refer to state (such as attributes) indirectly.

Get daily content like this in your inbox!

Subscribe