skip to content

What does unsafe.Sizeof report for a Go string, slice or map value, and what does it not count?

level: middleimportance: must knowfreq 45%

answer

  1. three words, whatever they point at
  2. the number never moves with length
  3. pointer, length, capacity — that is all
  4. a compile-time constant cannot know a length

basics

~20 s

unsafe.Sizeof counts only a value's own fixed representation: on a 64-bit platform 16 bytes for any string, 24 for any slice and 8 for any map. The bytes, elements and hash table those pointers reach are never counted.

solid answer

~50 s

A slice value is three words — a pointer to the backing array, a length and a capacity — so `unsafe.Sizeof` on any slice gives 24 on a 64-bit platform whether it holds three bytes or three million. A string value is two words, a pointer and a length, so 16. A map value is a single pointer to the runtime hash table, so 8. An interface value is two words, 16. In every case Sizeof stops at the value itself and never follows the pointer, because the answer is a compile-time constant and lengths are only known while running. The practical consequence is that summing `unsafe.Sizeof` over a struct's fields gives you the shallow footprint of the struct, which for anything holding strings, slices or maps can understate real memory use by orders of magnitude.

code

go · 10 lines
go
type entry struct {
	key  string            // 16: pointer + length
	data []byte            // 24: pointer + length + capacity
	meta map[string]string // 8: one pointer
}

e := entry{key: "k", data: make([]byte, 1<<20)}

fmt.Println(unsafe.Sizeof(e)) // 48
fmt.Println(cap(e.data))      // 1048576 bytes the line above ignores

go deeper

for a junior

Remember the shape of the answer: a slice, string or map value is just a small record of pointers and counts, so its measured size stays the same however much data it refers to.

for a middle

Be ready to name the words — pointer, length, capacity for a slice; pointer and length for a string; one pointer for a map — and to explain that a compile-time constant cannot know a runtime length.

for a senior

Show that you would never publish this number as a memory figure. Explain what a deep accounting has to add and why the map term can only be measured rather than derived.

for a principal

Decide whether the codebase models memory arithmetically at all. An accounting function is a permanent maintenance obligation that silently drifts whenever a struct changes; bounding by count and measuring is often the cheaper contract.

## Values that are handles Several Go types are, at the machine level, a small fixed record that refers to memory elsewhere: | type | representation | Sizeof on a 64-bit platform | | --- | --- | --- | | `string` | pointer to bytes, length | 16 | | `[]T` | pointer to backing array, length, capacity | 24 | | `map[K]V` | one pointer to the runtime hash table | 8 | | `chan T` | one pointer to the runtime channel | 8 | | `func(...)` | one pointer to a function value | 8 | | any interface type | type word, data word | 16 | | `*T`, `unsafe.Pointer` | one pointer | 8 | `unsafe.Sizeof` reports exactly those fixed records. It cannot do anything else: the specification makes it a compile-time constant for types of constant size, and a length or capacity is a runtime quantity. The compiler would have to run your program to add up the bytes behind a slice pointer. ## The consequence for structs This matters most when you measure a struct. Consider a cache entry: ```go type entry struct { key string // 16 data []byte // 24 meta map[string]string // 8 } ``` `unsafe.Sizeof(entry{})` is 48 on a 64-bit platform, and it stays 48 when `data` holds a megabyte and `meta` holds ten thousand pairs. Forty-eight bytes is a true statement about the struct: that is how much space one `entry` occupies inside an array or a slice of entries. It is not a statement about how much memory an entry costs your process. The deep footprint of that entry is roughly `unsafe.Sizeof(entry{})` plus `len(key)` bytes for the key's backing array, plus `cap(data)` bytes for the slice's backing array, plus whatever the map's runtime table has allocated for its keys, values and spare slots. Only the first term is available at compile time; the rest must be computed at run time or measured. Note also that `unsafe.Sizeof` of a struct is not always the plain sum of its field sizes. The compiler may leave gaps so that fields land on suitable boundaries, and may pad the tail of the struct. Sizeof reports the padded total, which is the honest figure for how much room the struct reserves. In the example above every field is a whole number of words, so no gaps appear and the arithmetic is exactly 16 + 24 + 8. ## Related traps - **An interface value is 16 bytes no matter what it holds.** Storing a large struct in an `any` does not grow the interface value; it grows the heap allocation the data word points at. - **An array is not a slice.** `unsafe.Sizeof([1000]byte{})` really is 1000, because an array's elements are part of the value. That contrast is the clearest demonstration of the rule: Sizeof counts what is inside the value. - **`len` and `Sizeof` answer different questions.** For a string, `len(s)` is the number of bytes of text; `unsafe.Sizeof(s)` is 16, the size of the two-word value that points at them. - **Reflection has the same limitation.** `reflect.TypeOf(x).Size()` returns the same shallow number, for the same reason. ## What to do instead If you need a memory figure, write it explicitly: add `cap` for slices, `len` for strings, and an estimate or a measurement for maps, and then check the total against real heap usage under a representative workload. If you only need to bound growth, counting entries and measuring the resulting heap is usually more honest than an arithmetic model that will drift the first time somebody adds a field.

  • Why can unsafe.Sizeof not follow the pointer and count the elements?
    Because it is resolved while compiling into a constant, and length and capacity are runtime values. The compiler knows a slice value is three words wide; it cannot know how many elements any particular slice will have. The same reasoning applies to a string's byte count and a map's entry count.
  • What does unsafe.Sizeof report for a variable of an interface type holding a large struct?
    16 bytes on a 64-bit platform. An interface value is two words — one identifying the dynamic type, one pointing at the data — so its size is fixed regardless of what is stored. The struct itself lives wherever the data word points and is not counted.
  • How would you compute the real memory a cache entry occupies?
    Add the terms yourself: `unsafe.Sizeof(e)` for the struct, `len(e.key)` for the string's bytes, `cap(e.data)` for the slice's backing array, and an estimate for the map, whose runtime table carries spare slots you cannot derive from its length. Then compare the total against a heap profile taken under a realistic workload.

A slice value is an index card recording where a book sits and how long it is. unsafe.Sizeof weighs the card, not the book.

saying these in an interview costs you the question

  • Says Sizeof of a []byte grows with the number of bytes
  • Uses unsafe.Sizeof as a cache entry's memory cost
  • Thinks a map value stores its entries inline
  • Assumes Sizeof of a string counts its characters
  • Expects an interface value to grow with the value it holds