What makes printing a large struct with fmt's %+v verb expensive at run time?
answer
- the verb chooses the code path
- there is a fast lane and a slow lane
- field names have to come from somewhere
- it recurses, and it sorts
- cost follows the value's shape, not the format
basics
~20 sThe %+v verb drops fmt out of its fast type switch into a reflective walk: every field is visited by name, nested structs, slices and maps are recursed into, map keys are sorted, and String methods found on the way are called.
solid answer
~50 s`fmt` is not uniformly reflective. It checks the formatting interfaces it honours — `fmt.Formatter`, `error`, `fmt.Stringer` — and then type-switches the common concrete kinds: the integer and float types, `bool`, `string`, `[]byte`. Printing an int with `%d` never leaves that fast lane. A struct matches none of those cases, so it falls through to a reflection-driven walk that visits each field, prints its name for the `+` flag, and recurses into nested structs, arrays, slices and maps. Map keys are sorted so the output is deterministic, which is a sort per map. Unexported fields are printed too. Each field may trigger its own `String` or `Error` method and its own allocations, and the output buffer grows as the rendering does. The cost therefore tracks the shape and size of the value, not the length of the format string.
code
go · 8 linestype Endpoint struct {
Name string
Tags map[string]string
Inner *Endpoint
}
e := Endpoint{Name: "api", Tags: map[string]string{"z": "1", "a": "2"}}
fmt.Printf("%+v\n", e) // field names, map keys in sorted order, Inner as <nil>go deeper
Know that %+v prints each field with its name, and that this convenience is not free: fmt inspects the value at run time rather than compiling into fixed field accesses.
Explain the two paths inside fmt — a type switch for the common concrete types and interfaces, and a reflective walk for everything else — and say which one a struct takes and what that walk does per field.
Judge where it matters. A %+v in an error path is fine; the same call inside a per-message loop is a measurable allocation source. Say what you would replace it with and how you would confirm the improvement.
Decide what your services may dump wholesale into logs at all. The reflection cost and the accidental exposure of unexported internals are one decision, and it belongs in a written convention rather than in a review comment each time.
### fmt has a fast lane and a slow lane It is a common misconception that `fmt` reflects over everything. It does not. When it takes an argument, it first checks whether the value implements the formatting interfaces it honours — `fmt.Formatter`, then `error`, then `fmt.Stringer` — and then type-switches the common concrete kinds: `bool`, the signed and unsigned integer types, the float and complex types, `string`, `[]byte`, `uintptr`. Printing an `int` with `%d` or a `string` with `%s` never leaves that fast lane. Anything that matches none of those — a struct, a map, an array, a slice of something other than bytes, a named type over an unusual kind — falls through to a reflection-driven walk of the value. That walk is where the cost of `%+v` on a large struct lives. ### What the walk does For a struct, the walk visits every field in declaration order. With the plain `%v` verb it prints the values separated by spaces inside braces; the `+` flag additionally prints each field's **name** followed by a colon, which is exactly why `%+v` is the verb people reach for when debugging. Every field is a fresh reflective read: obtain the field value, decide its kind, recurse or format it. Four things make this add up: 1. **Recursion into composite fields.** A nested struct, array, slice or map field is walked the same way, element by element. The cost tracks the *shape of the value*, not the length of the format string. One `%+v` over a struct holding a 500-element slice of structs does thousands of times the work of ten `%d` verbs. 2. **Map keys are sorted.** Go randomises map iteration deliberately, so `fmt` sorts the keys of every map it prints to keep output stable for tests and diffs. That is a sort per map, per call. 3. **Methods are called.** If a field's type implements `String` or `Error`, `fmt` calls it, and whatever that method does — including its own `Sprintf` — is now part of your print cost. A `String` method that formats its own nested values can multiply the walk. 4. **The output buffer grows.** The scratch buffer is pooled and reused, but a large rendering still forces it to grow, and the finished bytes are copied into the returned string by `Sprintf`. ### Two bounds worth knowing **Nested pointers are not followed.** When a pointer to a struct, slice, array or map is the whole argument, `fmt` prints it as `&{...}` with the contents expanded. A pointer field *inside* the value prints as a hexadecimal address instead. This is a deliberate loop guard — following pointers at every depth would let a cyclic structure print forever — and it means `%+v` is bounded by the value-shaped tree beneath your argument, not by the whole object graph. It also means `%+v` on a struct of pointers tells you almost nothing. **Unexported fields are printed.** The walk reads unexported fields' values and includes them, which is why printing a struct that embeds a mutex, a large internal cache or a connection dumps far more than the author intended. Because those values cannot be handed back out as interfaces, their own `String` methods are *not* called; you get the raw shape instead. ### The cost model to carry away `%d` on an `int`: a type switch and a digit loop. `%+v` on a struct: a reflective walk proportional to the number of fields and elements reachable by value, plus a sort per map, plus every `String` method along the way, plus the buffer growth to hold the result. The verb is one character longer; the work can be three orders of magnitude larger. ### What to do about it If a `%+v` is on a cold path — an error message, a start-up dump, a test failure — leave it alone; it is the most readable thing you can write. If it is inside a loop or on a per-message path, the fixes are, in order of preference: - Format only the fields you actually need, with explicit verbs, so the walk never happens. - Give the type a hand-written renderer that emits a short, stable line of straight-line code. - Reconsider whether the value should be printed whole at all — the reflection cost and the accidental disclosure of unexported internals are the same decision seen from two sides. Deciding which of `%v`, `%+v` and `%#v` reads best, and how to implement a `String` method well, is a separate question about output; the point here is that the choice of verb also chooses a code path with a very different price.
- Does %+v follow a pointer field that sits inside the struct?Only at the top level. When a pointer to a struct, slice, array or map is the whole argument, `fmt` expands it as `&{...}`. A pointer field nested inside prints as a hexadecimal address instead — a deliberate guard so cyclic data cannot print forever. So `%+v` is bounded by the value-shaped tree beneath your argument, not by the whole object graph.
- Are unexported fields included in the output?Yes. The walk reads and prints unexported fields, which is why printing a struct that embeds a mutex, a connection or a large internal cache dumps far more than the author intended. Because those values cannot be handed back out as interfaces, their own `String` methods are not called, so you see the raw shape rather than a tidy rendering.
- How would you make a frequently printed struct cheap?Print only the fields that matter, with explicit verbs, so the reflective walk never happens; or give the type a hand-written renderer that emits a short fixed line of straight-line code. Both replace run-time inspection with compiled field accesses. On a cold path, leave `%+v` alone — it is the most readable thing available.
saying these in an interview costs you the question
- Says fmt uses reflection for every argument, even a plain int
- Thinks %+v is just %v with a flag and costs the same
- Assumes unexported fields are skipped
- Believes the cost tracks the format string's length
- Expects %+v to dereference nested pointer fields