skip to content

Why should you use unsafe.SliceData instead of casting a slice to *reflect.SliceHeader?

level: seniorimportance: nice to knowfreq 26%

answer

  1. one of the two carries a plain integer
  2. the collector does not follow integers
  3. typed pointer versus uintptr
  4. Data is a uintptr; SliceData returns *T

basics

~20 s

reflect.SliceHeader stores its Data field as a uintptr, which the garbage collector does not trace, so a hand-built header can name an already-collected array. unsafe.SliceData returns a typed pointer that keeps the backing array alive.

solid answer

~50 s

`reflect.SliceHeader` describes the three words as `Data uintptr`, `Len int` and `Cap int`. A `uintptr` is just an integer: the collector does not count it as a live reference, so nothing keeps the backing array reachable while your header holds its address, and a header you build yourself can point at freed memory. The type is documented as valid only when it aliases a slice that already exists in memory, never as a value you populate, and it is deprecated for that reason. The supported replacement is `unsafe.SliceData(s)`, added in Go 1.20, which returns a typed `*T` to the underlying array - a genuine pointer the collector tracks - with `unsafe.Slice(ptr, n)` to build a slice back from it. Both are typed, so the compiler and the collector can both see what you are doing.

code

go · 8 lines
go
readings := make([]float64, 3, 8)

// A real *float64 that the collector tracks.
first := unsafe.SliceData(readings)

// Back to a slice; the result has length AND capacity 3.
again := unsafe.Slice(first, 3)
fmt.Println(len(again), cap(again)) // 3 3

go deeper

for a junior

Know that unsafe.SliceData is the supported way to get a slice's underlying pointer, and that hand-built header structs are not something to reach for at this level.

for a middle

Explain the difference between a uintptr and a real pointer: only a pointer keeps the object it names reachable, so a header whose Data is an integer can outlive the array it describes.

for a senior

Argue the review case for replacing a reflect.SliceHeader cast with unsafe.SliceData plus unsafe.Slice, and state what each returns for a nil or zero-capacity slice.

for a principal

Own the policy on unsafe in the codebase: where it is permitted, who reviews it, and the standing rule that a typed replacement in the standard library retires the older pattern rather than sitting beside it.

## Two ways to reach a slice's underlying pointer Safe Go gives you `len(s)` and `cap(s)` but no way to read the header's pointer word. Code that genuinely needs it - a `syscall` boundary, a zero-copy conversion, a fast path over a foreign buffer - has historically used one of two mechanisms. The old one: ``` h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) addr := h.Data // a uintptr ``` The modern one, from Go 1.20: ``` p := unsafe.SliceData(s) // a *T ``` They are not equivalent, and the difference is about what the garbage collector can see. ## uintptr is not a pointer `reflect.SliceHeader` is declared with `Data uintptr`, `Len int` and `Cap int`. A `uintptr` is an ordinary unsigned integer that happens to hold an address. The collector traces **pointers**; it does not trace integers. So a `uintptr` holding the address of a heap object does nothing at all to keep that object alive. That has two consequences. First, a header you build yourself is unsound. Compute an address, store it in `Data`, and between that statement and the cast the object may already have become unreachable and been collected - and the runtime is free to move objects, so an address captured as an integer is not guaranteed to stay correct. What you get back is a slice pointing at memory nobody owns, and the failure is silent: it works in testing and corrupts data under load. Second, even *reading* through the type is delicate. The documentation is explicit that `reflect.SliceHeader` may only be used to alias a slice value that already exists somewhere in memory - `(*reflect.SliceHeader)(unsafe.Pointer(&s))` where `s` is a live slice variable - and that a `SliceHeader` you construct as a standalone value is invalid. That distinction is subtle enough that most uses in the wild got it wrong, which is precisely why the type (along with `reflect.StringHeader`) carries a deprecation notice today. It has not been removed: the Go 1 compatibility promise keeps it in the standard library, and the marker exists to steer new code elsewhere. ## What unsafe.SliceData gives you instead `unsafe.SliceData(s)` returns `*T` for a `[]T` - a typed, ordinary Go pointer. The collector traces it like any other pointer, so as long as you hold it the backing array stays alive and stays valid. The compiler also type-checks it, so a `*float64` cannot be quietly used as a `*int32`. Its exact contract is worth knowing: - if `cap(s) > 0`, it returns `&s[:1][0]` - a pointer to the first element of the backing array; - if `s` is nil, it returns nil; - otherwise (non-nil, capacity zero) it returns a non-nil pointer to an unspecified address, which you must not dereference. The inverse direction is `unsafe.Slice(ptr, n)`, which builds a `[]T` from a `*T` and a length. Note that the result has **length and capacity both equal to `n`** - `unsafe.Slice` takes one count, not two, so a slice's original spare capacity does not survive a round trip through pointer and back unless you pass the capacity as the length and re-slice. Neither function carries the length or capacity for you. `SliceData` gives you one of the three header words; the other two are yours to keep track of, and getting them wrong is how a zero-copy fast path turns into out-of-bounds reads that the bounds checker cannot catch. ## Reviewing it in practice When a `*reflect.SliceHeader` cast appears in a diff, the question to ask is what the code is actually trying to do. Reading the pointer becomes `unsafe.SliceData`. Building a slice over foreign memory becomes `unsafe.Slice`. Reading the length or capacity never needed the cast at all - `len` and `cap` were always available. In most diffs the whole construct disappears, replaced by two typed calls the reviewer can reason about. The wider point generalises past slices: in Go, an address stored as a `uintptr` is a dead address as far as the runtime is concerned. Anywhere a `uintptr` survives across a statement boundary and is later turned back into something dereferenceable, the code is relying on luck. Typed pointers are the only representation the collector will defend.

  • What does unsafe.SliceData return for a nil slice, and for a non-nil slice whose capacity is zero?
    For a nil slice it returns nil. For a non-nil slice with capacity zero it returns a non-nil pointer to an unspecified address, which must not be dereferenced. Only when `cap(s) > 0` is the result guaranteed to be `&s[:1][0]`, a pointer to the first element of the backing array.
  • If reflect.SliceHeader is deprecated, why is it still in the standard library?
    The Go 1 compatibility promise means exported API is not removed from the standard library, so existing code keeps compiling. The deprecation marker is a signal to tools and to authors of new code, not a removal schedule. It also still works for its one documented use - aliasing a slice value that already exists in memory - just not for headers you construct yourself.
  • Does unsafe.SliceData preserve the slice's capacity?
    No. It returns only the pointer word; length and capacity are yours to carry separately. `unsafe.Slice(ptr, n)` then produces a slice whose length *and* capacity are both `n`, so a round trip through pointer and back loses any spare capacity unless you deliberately pass the old capacity and re-slice down to the length you want.

A uintptr is a street address copied onto a scrap of paper; a real pointer is the lease on the building. Only the lease stops the place from being demolished while you are on your way there.

saying these in an interview costs you the question

  • Treats uintptr as a pointer the collector tracks
  • Populates a reflect.SliceHeader by hand and casts it
  • Thinks deprecated means scheduled for removal
  • Assumes unsafe.SliceData carries length and capacity too
  • Uses a header cast just to read len or cap