skip to content

Why does storing an int in an any variable allocate when storing a *bytes.Buffer does not?

level: middleimportance: nice to knowfreq 30%

answer

  1. the second word is only one word
  2. the collector has to scan it
  3. some values already are a pointer
  4. everything else needs somewhere to point
  5. escape analysis can still avoid it

basics

~20 s

An interface's second word is one pointer the garbage collector scans, so a pointer-shaped value like *bytes.Buffer goes in directly. An int is not a pointer, so the conversion copies the value somewhere addressable — often the heap.

solid answer

~50 s

The data word of an interface value is a single pointer, and the collector must be able to scan it as one. A value whose representation already is one pointer — a `*bytes.Buffer`, a map, a channel, a func value — is stored in that word as is, so the conversion is a couple of stores and costs nothing extra. Anything else, an `int`, a `string`, a struct, cannot fit and cannot be scanned as a pointer, so the conversion copies the value into a box and stores the box's address. That box is usually a heap allocation, which is why boxing shows up in allocation profiles. Two escapes exist: the compiler can put the box on the stack when it proves the interface value does not outlive the frame, and the runtime serves small integer values from storage it has preallocated.

code

go · 12 lines
go
var a any

buf := new(bytes.Buffer)
a = buf     // pointer-shaped: the data word is buf itself

m := map[string]int{}
a = m       // a map is a single-pointer handle: stored directly

n := 1 << 20
a = n       // not pointer-shaped: the value is copied into a box

_ = a

go deeper

for a junior

Know that putting a value into an interface variable is not always free: the value may be copied, and what the interface keeps is the concrete type plus a pointer to the data.

for a middle

Explain the data word: one pointer wide and scannable by the collector, so pointer-shaped values sit in it directly and anything else must be copied into a box first.

for a senior

Be able to say when the box costs nothing — the compiler proves it does not escape, or the value is a small integer the runtime already has storage for — and to measure the claim rather than assert it.

for a principal

Own the tradeoff at the API line. A parameter typed any pushes a conversion onto every caller, so decide deliberately where in a package's surface values become interface values, and keep that boundary shallow.

### The constraint An interface value is two words: a type word and a **data word**. The data word is one machine word and the garbage collector treats it as a pointer — it has to, because for a `*bytes.Buffer` in an interface, that word is the only thing keeping the buffer alive. There is no per-value flag saying "this time the word is really an integer", so the rule has to be uniform: **the data word always holds a pointer**. Everything about boxing cost follows from that one constraint. ### Pointer-shaped values ride free If the concrete type's representation *is* a single pointer, the conversion just copies it into the data word. That covers: - pointers (`*bytes.Buffer`, `*os.File`, `*T` for any `T`) - maps and channels, which are single-pointer handles to runtime structures - func values - `unsafe.Pointer` So `var a any = buf` where `buf` is a `*bytes.Buffer` allocates nothing. The conversion is two stores: the type descriptor into word one, the pointer into word two. Anything that was already on the heap stays where it is; the interface just refers to it. ### Everything else gets a box An `int` is not a pointer. A `string` is two words (data pointer and length). A struct is however wide its fields are. None of these fit in one pointer-sized, pointer-scannable word, so the conversion **copies the value into separate storage and puts that address in the data word**. That storage is the "box", and allocating it is what shows up in a profile as an allocation attributed to the conversion. Note the word *copies*. The box holds a snapshot. Changing the original variable after the conversion does not change what the interface holds — a second, quieter consequence of the same mechanism. ### The two ways the heap is avoided 1. **Escape analysis.** The box only has to outlive the frame if the interface value does. When the compiler can prove the interface value stays local — passed to a function it can see through, never stored, never returned — it can place the box on the stack, and the conversion costs a copy but no allocation. 2. **Preallocated small values.** The runtime keeps storage for small integer values, so converting a value in that range to an interface hands back a pointer into that storage rather than allocating. This is why microbenchmarks that box `0`, `1` or `7` sometimes report zero allocations while the same code boxing a large number reports one. Both are optimisations you should confirm rather than assume, because both depend on code the compiler can see. ### Where this bites in ordinary code The most common source of surprise boxing is not an explicit `var a any = x` at all — it is a variadic `...any` parameter. Every argument to such a function is converted to an interface value first, and non-pointer arguments get boxed. That is the mechanism behind allocations attributed to formatting and logging calls in otherwise allocation-free code. The second most common is a container of interfaces: an `[]any` or a `map[string]any` built from non-pointer values holds one box per element, and each of those boxes is a separate object for the collector to track. ### What this is not - It is not the method table. The type word points at a descriptor or an itab that already exists; nothing is allocated for it per value. - It is not "Go boxes everything". Pointer-shaped values genuinely go in directly. - It is not unconditional. "Converting to an interface allocates" is a rule of thumb whose exceptions are exactly the two above. ### The mental model The data word is a one-pointer letterbox. If what you are posting is already an address, it goes straight in. If it is not, the runtime has to put a copy somewhere and post that address instead — and finding somewhere is the cost.

  • Does storing a string in an any variable allocate?
    Usually yes. A string value is two words — a data pointer and a length — so it does not fit the single data word and gets copied into a box the interface points at. Note that only the header is copied; the text itself is not duplicated, because strings are immutable and share their backing bytes.
  • Why can passing an int to a function with a ...any parameter allocate?
    Because each argument is converted to an interface value before the call, and an `int` is not pointer-shaped, so it has to be boxed. The slice of arguments is then handed to code the compiler generally cannot see through, so the box escapes and the copy lands on the heap.
  • Does putting a pointer into an interface ever allocate?
    The conversion itself does not — the pointer is copied straight into the data word. What may allocate is whatever the pointer refers to, and that decision was already made when the pointed-at value was created, not at the moment it was converted to an interface.

saying these in an interview costs you the question

  • Says Go boxes every value, including pointers
  • Claims the int is stored inline in the data word
  • Blames the allocation on the method table rather than the data word
  • Assumes every conversion to any reaches the heap regardless of escape analysis
  • Thinks boxing a string copies the text as well as the header