skip to content

In Go's reflect package, how does reflect.Zero(t) differ from Value.IsZero()?

level: middleimportance: nice to knowfreq 26%

answer

  1. one is a constructor, one is a predicate
  2. the constructed one is deliberately read-only
  3. comparing via interfaces has a runtime trap
  4. uncomparable dynamic types panic on ==
  5. for a writable blank you need New plus Elem

basics

~20 s

reflect.Zero(t) constructs a value: the zero value of type t, explicitly not addressable and not settable. Value.IsZero() inspects a value and reports whether it already equals the zero value of its own type. One builds, one asks.

solid answer

~50 s

`reflect.Zero(t)` is a constructor. It returns a `reflect.Value` holding the zero value of `t` — `0`, `""`, `nil` for reference kinds, a zeroed struct — and the documentation is explicit that the result is neither addressable nor settable, so it is a blank to read or pass along, never something to fill in. `Value.IsZero()` is a predicate on a value you already have: it reports whether that value is the zero value for its type, recursing into struct fields. The practical reason to prefer `IsZero` over comparing against `reflect.Zero` is that the comparison `v.Interface() == reflect.Zero(v.Type()).Interface()` panics at runtime when the dynamic type is uncomparable — a slice, a map, a function — while `IsZero` handles every kind. When you need a blank you can actually write into, the answer is neither of these: it is `reflect.New(t).Elem()`.

code

go · 6 lines
go
z := reflect.Zero(reflect.TypeOf(0)) // read-only: not addressable, not settable
fmt.Println(z.IsZero())              // prints: true

v := reflect.New(reflect.TypeOf(0)).Elem() // allocated, writable
v.SetInt(7)
fmt.Println(v.IsZero()) // prints: false

go deeper

for a junior

Keep the two roles straight: reflect.Zero(t) makes a blank value of type t, Value.IsZero() asks whether a value you already hold is still blank. Remember the blank from Zero cannot be written to.

for a middle

Explain IsZero kind by kind, especially that reference kinds are zero only when nil, and why comparing through Interface() risks a runtime panic on slices, maps and funcs that the compiler cannot warn about.

for a senior

Show the judgment in a walker: IsZero decides whether a caller already supplied a field, Len decides whether a container is empty, and confusing the two silently overwrites data or skips fields that needed filling.

for a principal

Own what blank means in your library's contract — untouched, nil, or empty — because callers build conditional logic on it, and a builder that changes its answer later breaks fixtures no one can trace back to you.

## Two functions that sound alike and do opposite jobs `reflect.Zero` **produces** a value. `Value.IsZero` **asks a question about** a value. The names are close enough that they get swapped in review, so it is worth being precise about each. ## reflect.Zero(t) `func Zero(typ Type) Value` returns a `reflect.Value` representing the zero value for `typ`. For `int` that is `0`; for `string`, `""`; for a pointer, slice, map, channel, function or interface, `nil`; for a struct or array, every element zeroed recursively. The critical property, stated in the package documentation, is that **the returned Value is neither addressable nor settable**. It is a read-only handle. That makes it useful for three things: - as a placeholder to pass where a `reflect.Value` argument is required — for instance filling in a missing argument, or clearing a field by `Set`ting it to the zero of its type - as a comparison target, where the type is comparable - as the result of a lookup that found nothing And it makes it useless for one thing people constantly try: building a value to fill in. `reflect.Zero(t)` then `Set` on it panics, because there is no memory behind it to write to. The tool for that is `reflect.New(t).Elem()`, which allocates real storage and hands back an addressable handle. Note also that `reflect.Zero` costs nothing for small types — it does not necessarily allocate, since it can hand back a read-only view of a shared zeroed region — whereas `reflect.New` always allocates. ## Value.IsZero() `func (v Value) IsZero() bool` reports whether `v` is the zero value for its type. It is kind-aware: - numeric kinds compare against zero - `string` compares against the empty string - pointer, slice, map, channel, func and interface kinds are zero when they are `nil` - arrays and structs are zero when **every** element or field is zero, checked recursively It panics only in one case: when `v` is the zero `reflect.Value` itself — an invalid Value, `Kind() == reflect.Invalid`, such as the one returned by `reflect.ValueOf(nil)` or by a failed lookup. Guard with `v.IsValid()` when the Value came from a `MapIndex` or a `FieldByName` that might have missed. ## Why not just compare against reflect.Zero? The tempting one-liner is: ``` if v.Interface() == reflect.Zero(v.Type()).Interface() { ... } ``` It works for `int`, `string` and comparable structs. It **panics at runtime** the moment `v`'s dynamic type is uncomparable — a slice, a map, or a function — because comparing two interface values with uncomparable dynamic types is a runtime error in Go, not a compile-time one. The compiler cannot see it coming because both sides are `any`. `IsZero` has no such hole. It is also cheaper: no boxing into interfaces, no allocation. ## Nil versus empty, one more time `IsZero` reports **nil**, not **empty**. A map allocated with `reflect.MakeMap` and never written to has `Len() == 0` but `IsZero() == false`, because the zero value of a map type is nil and this map is not nil. The same holds for a slice from `reflect.MakeSlice(t, 0, 8)`. If what you actually want to know is "does this container hold anything", ask `Len() == 0`; if you want to know "was this field ever populated", `IsZero` is the right question — those are genuinely different, and choosing the wrong one produces a builder that either re-allocates fields it already made or skips fields it should fill. ## In a builder The pattern that uses both correctly: walk the target's fields, and for each one, use `IsZero` to decide whether the caller already supplied something (leave it alone) or not (fill it in). When filling it in, allocate with `reflect.New(fieldType).Elem()` — never with `reflect.Zero(fieldType)`, which cannot be written to — and when the value is complete, `Set` it into the field. Reserve `reflect.Zero` for the case where the builder needs to *clear* a field back to its blank state, which is the one job it is exactly right for.

  • When does Value.IsZero() panic?
    Only on an invalid Value — one whose `Kind()` is `reflect.Invalid`, such as the result of `reflect.ValueOf(nil)`, a `MapIndex` for a key that is absent, or a `FieldByName` that matched nothing. Every real kind has a defined zero and is handled. Guard the uncertain cases with `v.IsValid()` before asking `IsZero()`.
  • Does IsZero report true for a map allocated with reflect.MakeMap that has no entries?
    No. The zero value of a map type is nil, and that map is not nil, so `IsZero()` is false even though `Len()` is 0. The same applies to a slice from `reflect.MakeSlice(t, 0, 8)`. If the question you actually mean is "is it empty", ask `Len() == 0`; `IsZero` answers "is it still the blank the type starts with".
  • You need a blank value of type t that a builder will then populate. Which call do you use?
    `reflect.New(t).Elem()`. It allocates real storage, so the resulting Value is addressable and can be written into. `reflect.Zero(t)` produces the same *contents* but is documented as neither addressable nor settable, so any attempt to fill it in panics. Reach for `Zero` only when you want a read-only blank to compare against or to clear a field back to.

saying these in an interview costs you the question

  • Tries to Set into the result of reflect.Zero
  • Compares uncomparable types through Interface() and ==
  • Thinks IsZero means the container is empty
  • Believes reflect.Zero allocates like reflect.New
  • Calls IsZero on an invalid Value without checking IsValid