When do you require a team to replace fmt.Sprintf with strconv.AppendInt-style calls, and how do you stop that spreading?
answer
- the trigger is evidence, not taste
- how far does the exception reach
- who can overrule you, and on what
- the readable version does not disappear
- the comment has to outlive the workload
basics
~20 sOnly where a measurement ties formatting allocations to a cost you care about, and only inside a boundary narrow enough to write in one sentence. Everywhere else fmt.Sprintf stays, because readability is the default and the rewrite has to be maintained.
solid answer
~50 sI would never make it a style rule. The trigger is evidence: allocations per call from a benchmark, times the real call rate, shown to be a meaningful share of the process's allocation or collector CPU. Once that exists I scope the exception as narrowly as it can be written down — this emitter, this encoder's inner loop — and require three things with it: a named helper so the ugliness lives in one place, a test asserting the fast rendering is byte-identical to the readable one, and a comment recording the measurement and its date so a future reader can delete it when the premise expires. Outside that boundary `fmt.Sprintf` stays the default. I also expect to be overruled sometimes: whoever owns the readability bar can decide a small allocation win is not worth the shape of the code.
go deeper
Understand that the faster form is not automatically the better one, and that a reviewer will ask what evidence made someone abandon the readable call.
Be able to produce the arithmetic yourself: allocations per call from a benchmark, calls per second from production, and what fraction of the process's allocation that product represents.
Own the boundary. Say exactly which functions the exception covers, what test keeps it honest, and what observation would make you revert it.
Frame it as a standing convention with an owner and an expiry condition, accept that the person who owns the readability bar can overrule your win, and name the cheaper alternatives — emitting less, batching, pre-rendering — before spending the team's clarity budget.
### The decision, stated honestly `fmt.Sprintf` reads well and costs allocations. `strconv.Append*` on a reused buffer costs nothing at run time and reads like assembly by comparison. Which one a hot path is allowed to use is not a technical fact — both work — it is a standing decision about where the codebase spends its clarity budget. Somebody owns it, and somebody can overrule it. ### The trigger has to be evidence, not intuition Two numbers, together: - **Allocations per call**, from a benchmark run with `-benchmem`. This is cheap to get and easy to over-read: a ten-times ratio between two microbenchmarks says nothing about a service. - **The call rate in production.** Multiply. Compare the product against the process's total allocation rate, or against the garbage collector's share of CPU. If formatting is 2% of the allocation, the rewrite is not worth a permanent readability cost, however impressive the ratio looked. If it is 30%, you have an argument. "It is on the request path, so it is hot" is a hypothesis, not evidence, and it is wrong more often than it is right. ### Scope it so it can be written down The failure mode is not the first rewrite; it is the fifth, done by someone who read the first and took it as house style. So the exception should be as narrow as a sentence: *this emitter*, *this encoder's inner loop*. Everything outside that sentence keeps `fmt` as the reviewed default, and "it might be hot one day" is not accepted in review. Three things travel with the exception: 1. **A named helper**, so the ugliness is one function with a clear name rather than smeared through the caller. 2. **A test against the readable implementation**, asserting byte-identical output over a table of cases, so the optimisation cannot drift into a bug. 3. **A comment carrying the measurement and its date**, so a future reader can check whether the premise still holds and delete the whole thing when it does not. An optimisation with no recorded reason is undeletable; nobody dares. ### Expect to be overruled, and treat that as legitimate The person who owns the codebase's readability bar — a tech lead, a maintainer — can look at a 5% allocation win and decide the shape of the code is not worth it, particularly in a package that many people edit and few people profile. That is a real tradeoff, not obstruction. Your job is to bring the number and the boundary; theirs is to weigh it against everything else the team has to read. If you find yourself arguing that performance always wins, you are not making the decision, you are skipping it. ### Look one level up before spending the budget Formatting faster is the third-best answer to "we allocate too much while emitting". Ahead of it: - **Emit less.** Sampling, aggregating in-process, batching several records into one write. This removes the format cost *and* the syscall cost, and it removes them by a factor, not a percentage. - **Pre-render what is constant.** A metric name or a line prefix that never changes can be built once when the emitter is constructed rather than on every line. - **Change the shape.** Writing into the connection's buffered writer rather than building a string and converting it back to bytes removes two copies without touching a single verb. Only when those are exhausted does the per-verb rewrite pay for itself. ### The organisational shape Write it as a short convention with an owner and an expiry condition: which packages are exempt from the readable default, what evidence admitted them, who reviews an addition to that list, and what would remove one. Then enforce it by review, not by a codebase-wide prohibition. A blanket ban on `fmt.Sprintf` is the worst outcome available: it costs readability everywhere to buy performance in the small fraction of code that was ever measured, and it teaches the team that performance claims do not need evidence.
- What evidence would actually convince you?Two numbers together: allocations per formatted call, from a benchmark run with `-benchmem`, and the measured call rate in production. Multiply them and compare against the service's total allocation rate or the collector's CPU share. A rewrite that removes 2% of allocations is not worth a permanent readability cost, however impressive the microbenchmark ratio looked.
- How do you keep an approved fast path from rotting?Keep the readable implementation as the reference and test the fast one against it for byte-identical output across a table of cases, including negatives, empty strings and boundary values. Then pin the reason in a comment with the date and the measurement, so the next reader can check whether it still holds and delete it when it does not.
- Someone wants to apply the same change across the whole service. What do you say?That the change only pays where it was measured, and costs readability everywhere it is applied. I would do the arithmetic with them on a cold path — a few thousand calls a day removes nothing — and point out that widening the exception teaches the team that performance claims do not need evidence, which is the expensive part.
- What would you try before the rewrite?Reduce the work rather than speed it up: sample or aggregate before emitting, batch several records into one write, and pre-render the parts of a line that never change when the emitter is constructed. Those remove cost by a factor instead of a percentage, and they cost no clarity at all.
saying these in an interview costs you the question
- Bans fmt.Sprintf across the whole codebase on principle
- Optimizes on a microbenchmark ratio with no call rate
- Leaves no test proving both renderings agree
- Treats the readability owner's objection as illegitimate
- Writes no comment explaining why the fast version exists
- Assumes code on the request path is hot without measuring