skip to content

Why does reflect.New(mapType).Elem() give an unusable map in Go, and what should you use?

level: middleimportance: should knowfreq 38%

answer

  1. New only ever gives you the zero value
  2. three kinds have a nil zero value
  3. the same panic as m[k] = v on var m map
  4. reflection mirrors the make builtin
  5. MakeMap, MakeSlice and MakeChan allocate

basics

~20 s

The zero value of a map type is a nil map, and reflect.New only ever gives you the zero value, so writing an entry panics. Use reflect.MakeMap for maps, reflect.MakeSlice for slices and reflect.MakeChan for channels.

solid answer

~40 s

`reflect.New(t)` allocates the zero value of `t`, and for the three reference kinds the zero value is `nil`: a nil map, a nil slice, a nil channel. A nil map reads fine and reports `Len() == 0`, but `SetMapIndex` on it panics, exactly as `m[k] = v` panics on a nil map in ordinary Go. A nil channel blocks forever on send and receive. A nil slice is the mildest case — `reflect.Append` grows it happily — but it has no storage, so `Index(0)` panics. The constructors that actually allocate are `reflect.MakeMap(t)` (or `MakeMapWithSize(t, n)`), `reflect.MakeSlice(t, len, cap)` and `reflect.MakeChan(t, buffer)`. Each takes the *composite* type, which you get from a value with `reflect.TypeOf` or build with `reflect.MapOf`, `reflect.SliceOf` or `reflect.ChanOf`.

code

go · 8 lines
go
mt := reflect.TypeOf(map[string]int{})

bad := reflect.New(mt).Elem() // a nil map: SetMapIndex would panic
fmt.Println(bad.IsNil())      // prints: true

good := reflect.MakeMap(mt)
good.SetMapIndex(reflect.ValueOf("retries"), reflect.ValueOf(3))
fmt.Println(good.Len()) // prints: 1

go deeper

for a junior

Remember that the zero value of a map, slice or channel is nil, and that reflection gives you exactly the zero value. The fix has an obvious name: MakeMap, MakeSlice, MakeChan, mirroring the make builtin.

for a middle

Explain what each nil reference kind still allows: reads and len on a nil map, appends on a nil slice, and a forever-blocking nil channel. Know the constructor signatures and that SliceOf and MapOf return types, not values.

for a senior

Describe how this surfaces in someone else's test run: a nil-map assignment panic from inside a library, on a type indistinguishable from the ones that pass, because the builder never allocated reference fields it did not have data for.

for a principal

Own the decision your builder documents: whether reference fields come back nil or allocated-and-empty. It changes what callers must defend against and changes the encoded form of every fixture your teams compare against.

## The rule underneath the question `reflect.New(t)` and `reflect.Zero(t)` both produce the **zero value** of `t`. That is all they promise. For an `int` the zero value is `0` and is perfectly usable; for a struct it is a struct with zeroed fields and is perfectly usable. For a map, a slice or a channel the zero value is `nil`, and `nil` is a value you can hold but mostly not write through. This is not a reflection quirk. It is the same rule as in ordinary Go: `var m map[string]int` gives you a nil map, and `m["a"] = 1` panics with "assignment to entry in nil map". Reflection just makes it easy to forget, because `reflect.New` sounds like it allocates the thing you asked for — and it does allocate, but it allocates the *header*, not the backing store. ## What each nil reference kind actually permits - **A nil map** can be read (`MapIndex` returns the zero Value for a missing key), ranged over (zero iterations) and measured (`Len()` is 0). `SetMapIndex` panics. - **A nil slice** has `len` 0 and `cap` 0. `reflect.Append` on it works and returns a new, allocated slice Value — appending to nil is legal in Go. But `Index(0)` panics because there is no element 0. - **A nil channel** blocks forever on both send and receive. `Value.Send` on it does not panic; it hangs, which in a fixture library means a test that times out rather than fails, and is much harder to read than a panic. - **A nil pointer, func or interface** dereferences or calls into a panic. ## The constructors The `reflect` package mirrors the `make` builtin with three functions, each taking the composite `reflect.Type` and returning an allocated `reflect.Value`: - `reflect.MakeSlice(typ Type, len, cap int) Value` — panics if `typ` is not a slice type, or if `len` is negative or greater than `cap`. - `reflect.MakeMap(typ Type) Value` and `reflect.MakeMapWithSize(typ Type, n int) Value` — the second pre-sizes the map, the way `make(map[K]V, n)` does. - `reflect.MakeChan(typ Type, buffer int) Value` — `buffer` of 0 gives an unbuffered channel. The channel type must be bidirectional. Notice the shape: `make` in source needs a type and up to two ints; these need a `reflect.Type` and the same ints. That parallel is the easiest way to remember which one you need. ## Getting the composite type in the first place You can get a composite type two ways. From an existing value: `reflect.TypeOf(map[string]int{})` gives you the map type, `reflect.TypeOf([]Config(nil))` gives you the slice type — `TypeOf` reads the type out of the interface, so even a nil slice or map carries it. Or you can build one from its parts: - `reflect.SliceOf(elem Type) Type` - `reflect.MapOf(key, elem Type) Type` - `reflect.ChanOf(dir ChanDir, elem Type) Type`, with `reflect.BothDir` for an ordinary channel The common mix-up is using `reflect.ChanOf` or `reflect.SliceOf` and expecting a value: they return a `reflect.Type`, a *description*. Nothing has been allocated until you pass that type to one of the `Make` functions. ## Where this bites a fixture or factory library A library that builds test data from a `reflect.Type` walks the target's structure and allocates as it goes. The struct itself comes from `reflect.New(t).Elem()`, which is correct — a zeroed struct is a fine starting point. But every map field, slice field and channel field inside it is still nil after that, and filling one requires an explicit `MakeMap`/`MakeSlice`/`MakeChan` and then setting the field to it. Code that walks fields and calls `SetMapIndex` without that step works on every struct the tests happen to cover with a pre-populated map, and panics the first time a caller's type has an empty one. The symptom in a test run is characteristic: a panic that names nil-map assignment, thrown from deep inside a library the test author did not write, on a type that looks no different from the ones that pass. ## The nil-versus-empty distinction is real, not cosmetic A nil map and an empty non-nil map behave identically for reads, `len` and `range`. They differ on writes, and they differ when they are serialised — a nil slice encodes as `null` while an empty one encodes as `[]`. So a builder that leaves reference fields nil is not merely fragile; it also produces fixtures whose encoded form differs from the real thing. Deciding whether the builder allocates every reference field or leaves them nil is a genuine API decision, and it should be documented either way.

  • How does a nil channel built this way fail differently from a nil map?
    A nil map panics loudly the moment you write an entry, so the bug surfaces at the call site. A nil channel does not panic at all: send and receive on it block forever, so the goroutine simply parks. In a test that means a timeout or a hang instead of a failure message, and you find it in a goroutine dump rather than a stack trace.
  • Where do you get the reflect.Type to pass to reflect.MakeSlice if no value of that slice type exists yet?
    Build it. `reflect.SliceOf(elem)` produces the slice type from its element type, `reflect.MapOf(key, elem)` the map type, and `reflect.ChanOf(reflect.BothDir, elem)` the channel type. These return a `reflect.Type` only — a description with nothing allocated — so the result still has to be passed to `MakeSlice`, `MakeMap` or `MakeChan` to get a usable Value.
  • Is reflect.Append on a nil slice Value also a bug?
    No. Appending to a nil slice is legal in Go and legal in reflection: `reflect.Append` returns a new, allocated slice Value with the element added. The nil slice is the forgiving one of the three reference kinds. What does fail is indexing it — `Index(0)` on a length-zero slice panics — so a builder that pre-sizes with `MakeSlice` and assigns by index needs the allocation, while one that appends does not.

saying these in an interview costs you the question

  • Thinks reflect.New allocates a usable map or channel
  • Confuses reflect.SliceOf, which returns a Type, with an allocation
  • Says a nil map panics on reads as well as writes
  • Expects a nil channel to panic rather than block forever
  • Treats nil and empty maps as interchangeable when encoding