skip to content

Why does reflect.Value.String() on a Value holding an int return a placeholder instead of panicking?

level: middleimportance: nice to knowfreq 25%

answer

  1. most accessors are gated by category
  2. one of them refuses to panic
  3. printing must never crash the program
  4. the result looks like angle brackets
  5. silent wrong output, not a crash

basics

~20 s

String is the one typed accessor that never panics on the wrong Kind, because reflect.Value must satisfy the Stringer convention and be printable. On a non-string value it returns a form like <int Value>. Int, Float and Bool panic instead.

solid answer

~40 s

Every other typed accessor on `reflect.Value` — `Int()`, `Uint()`, `Float()`, `Bool()`, `Bytes()`, `Len()` — panics when the value's `Kind` does not match, which is why `Kind` is the gate you check first. `String()` is deliberately special-cased: Go's convention is that a `String() string` method makes a type printable, and `reflect.Value` must be safe to print, so instead of panicking on a non-string it returns a placeholder of the form `"<int Value>"`. The practical hazard is that this fails *silently*: code that builds output from `v.String()` on arbitrary values emits `<int Value>` text instead of crashing where the bug is. Render through `fmt.Sprint(v.Interface())` instead, or check `Kind() == reflect.String` first. Note also that `fmt` itself special-cases a `reflect.Value` operand and prints the value it holds, so `fmt.Println(v)` prints `42` while `v.String()` does not.

code

go · 8 lines
go
v := reflect.ValueOf(42)

fmt.Println(v.String()) // <int Value>
fmt.Println(v)          // 42  (fmt replaces a reflect.Value operand)
fmt.Println(v.Int())    // 42

// v.Float() panics:
// reflect: call of reflect.Value.Float on int Value

go deeper

for a junior

Remember that reading data out of a reflect.Value only works when the value's category matches the accessor, and that String is the odd one that returns a placeholder instead of failing.

for a middle

Explain why the exception exists — a printable type must never crash the printer — and which accessors are strict, including that Int refuses unsigned values.

for a senior

Point at the silent-failure mode: rendering code built on Value.String emits plausible placeholder text instead of failing, so route through Interface or gate on Kind and cover it with a test.

for a principal

Weigh whether helpers your teams share should be total and forgiving like this method, or strict and loud, and be able to defend the debuggability cost either choice imposes.

## The rule, and its one exception `reflect.Value` exposes typed accessors that read the underlying data: `Int() int64`, `Uint() uint64`, `Float() float64`, `Complex() complex128`, `Bool() bool`, `Bytes() []byte`, `Len() int`, `String() string`. Each is only meaningful for certain `Kind`s, and the package's rule is blunt: call one on the wrong `Kind` and it panics, with a message like `reflect: call of reflect.Value.Float on int Value`. That is why reflective code is written as a `Kind` switch first, accessor second. `String()` is the exception, and the documentation says so explicitly. On a `Kind` `String` value it returns the string. On anything else it does **not** panic — it returns a placeholder of the form `"<int Value>"`, `"<[]int Value>"`, `"<invalid Value>"` for the zero `Value`. ## Why the exception exists Go has a pervasive convention: a type with a `String() string` method is a `fmt.Stringer`, and printing machinery calls that method. `reflect.Value` needs to be printable — it turns up in log lines, in test failure output, in `%v` verbs inside the standard library itself. If `String()` panicked whenever the wrapped value was not a string, then printing a `reflect.Value` during debugging would crash the program precisely when someone was trying to work out what was wrong. So the method was made total: it always returns something. ## Why that is a hazard The cost of a method that never fails is that a mistake produces plausible-looking output instead of an error. A helper that renders arbitrary values with `v.String()` will happily emit ``` user id: <int Value> ``` and nothing anywhere reports a problem. The value was there, the type was right, the code just asked the wrong question. Compare `v.Int()`, which would have stopped at the exact line. The fixes are small: - If you want the data as a Go value, use `v.Interface()` and let `fmt` format it: `fmt.Sprint(v.Interface())`. - If you want to print the `reflect.Value` directly, just do it. `fmt` special-cases a `reflect.Value` operand by replacing it with the concrete value it holds and continuing, so `fmt.Println(v)` on a `Value` wrapping `42` prints `42`, not `<int Value>`. This surprises people in the opposite direction: `fmt.Println(v)` and `fmt.Println(v.String())` print different things. - If you really want the string only when it is a string, gate on `v.Kind() == reflect.String`. ## The accessors that do panic, and how to pre-check The rest of the family follows the strict rule, and the mapping is worth knowing: - `Int()` covers `Int`, `Int8`, `Int16`, `Int32`, `Int64` and returns `int64`. It does **not** accept unsigned kinds — `Uint()` is separate, and returns `uint64`. Assuming `Int()` handles every integer is a common bug when walking arbitrary structs. - `Float()` covers `Float32` and `Float64`; `Complex()` covers the two complex kinds. - `Len()` covers `Array`, `Chan`, `Map`, `Slice` and `String`. - `Bytes()` requires a slice of bytes (or an addressable byte array). - `IsNil()` requires one of the nilable kinds: `Chan`, `Func`, `Interface`, `Map`, `Pointer`, `Slice`. Rather than a bare `Kind` switch you can use the `CanInt`, `CanUint`, `CanFloat` and `CanComplex` predicates, which report whether the corresponding accessor is legal. They read better in a numeric normaliser than enumerating five integer kinds by hand: ```go switch { case v.CanInt(): n = float64(v.Int()) case v.CanUint(): n = float64(v.Uint()) case v.CanFloat(): n = v.Float() default: return fmt.Errorf("not numeric: %s", v.Kind()) } ``` ## What an interviewer is listening for Two things. First, the general rule — accessors are `Kind`-gated and panic otherwise — because that is the discipline all reflective code is written under. Second, the awareness that `String()` breaks the rule for a *reason* (printability) and that the reason creates a silent-failure mode in rendering code. A candidate who has actually shipped reflective code usually volunteers the third fact unprompted: `fmt` unwraps a `reflect.Value` operand, so printing the `Value` and calling its `String()` method give different answers.

  • Which reflect.Value methods must you gate on Kind before calling?
    All the typed readers: `Int`, `Uint`, `Float`, `Complex`, `Bool`, `Bytes`, `Len`, `Index`, `MapKeys`, `NumField` and `IsNil` each panic on a mismatched `Kind`. Use a `Kind` switch, or the `CanInt`/`CanUint`/`CanFloat`/`CanComplex` predicates for the numeric family, before reading.
  • Does Value.Int() work for a value whose Kind is Uint64?
    No — it panics. `Int()` covers only the signed kinds and returns `int64`; unsigned values need `Uint()`, which returns `uint64`. A normaliser that assumes `Int()` handles every integer crashes the first time it meets a `uint64` field, which is why `CanInt` and `CanUint` are separate predicates.
  • How do you print the data a reflect.Value holds, the way fmt would print the original?
    Either pass the `reflect.Value` straight to `fmt` — it replaces a `reflect.Value` operand with the concrete value it holds — or call `fmt.Sprint(v.Interface())` when you need the `any` for other reasons. What you must not do is build output from `v.String()`, which silently yields a placeholder for non-strings.

saying these in an interview costs you the question

  • Expects String() to panic like Int() does
  • Renders arbitrary values with Value.String()
  • Assumes Int() also reads unsigned values
  • Thinks fmt prints <int Value> for a reflect.Value
  • Calls a typed accessor without checking Kind