skip to content

Why does reflect.Value.Call panic on an argument mismatch, and how do you validate a call first?

level: middleimportance: should knowfreq 33%

answer

  1. the compiler never saw this call
  2. no error return, so only one option left
  3. the method value describes its own signature
  4. NumIn, In and AssignableTo before Call
  5. recover at the boundary, under the validation

basics

~20 s

Nothing about a reflected call is checked at compile time, so reflect.Value.Call reports a wrong argument count or type the only way it can, by panicking. Validate first with the method value's Type: NumIn, In(i) and AssignableTo.

solid answer

~40 s

`Call` has no static type information to work with — the method came from a string and the arguments came from a `[]reflect.Value` — so a mismatch is discovered inside the runtime and reported as a panic: `reflect: Call with too few input arguments` or `reflect: Call using string as type int`. The fix is a pre-flight check against the method value's own type. `m.Type()` describes the bound signature: `NumIn()` gives the parameter count, `In(i)` the i-th parameter type, `IsVariadic()` whether the last one is a `...T`, and `arg.Type().AssignableTo(t.In(i))` tells you whether an argument will be accepted. Reject bad input as an ordinary error before any `reflect.Value` reaches `Call`, and put a `recover` at the dispatcher boundary so a case you missed degrades into a failed command rather than a dead process.

code

go · 15 lines
go
func check(m reflect.Value, args []reflect.Value) error {
	t := m.Type() // the bound signature: parameters only
	if t.IsVariadic() {
		return errors.New("variadic commands are not dispatchable")
	}
	if len(args) != t.NumIn() {
		return fmt.Errorf("want %d arguments, got %d", t.NumIn(), len(args))
	}
	for i, a := range args {
		if !a.Type().AssignableTo(t.In(i)) {
			return fmt.Errorf("argument %d: %s is not assignable to %s", i, a.Type(), t.In(i))
		}
	}
	return nil
}

go deeper

for a junior

Know that a reflected call is not type-checked by the compiler and that a wrong argument count or type panics at run time rather than returning an error.

for a middle

Explain the mismatch messages and reconstruct the signature from the method value: NumIn, In(i), IsVariadic, AssignableTo. Be able to write the validation loop on a whiteboard.

for a senior

Show the operational shape: validate before building arguments, recover at the dispatch boundary, and log the resolved command name because the panic message names only types.

for a principal

Argue about where this class of error should live at all — what it costs a team to re-implement, by hand and per call site, the argument checking the compiler used to do.

## Why it panics rather than returning an error When you write `a.Drain("maintenance")` the compiler checks the count and the types of the arguments. A reflected call throws that away: the method was found from a name string, and the arguments arrive as a `[]reflect.Value` that could hold anything. The mismatch can only be discovered inside `reflect` while it is laying the arguments out for the call, at which point there is no error to return — `Call`'s signature is `Call(in []Value) []Value`. So `reflect` does what the runtime does for an out-of-range index: it panics. The messages are specific and worth recognising: - `reflect: Call with too few input arguments` - `reflect: Call with too many input arguments` - `reflect: Call using string as type int` — an argument whose type is not assignable to the parameter - `reflect: call of reflect.Value.Call on zero Value` — the method name matched nothing - `reflect: Call using value obtained using unexported field` — the value came out of an unexported struct field, so reflection refuses to let you use it ## Reading the stack trace The panic surfaces in the goroutine that called `Call`, and the top frames belong to `reflect`, not to you: `reflect.Value.call` sits under `reflect.Value.Call`, and your dispatcher is the first frame you recognise below them. That shape is the tell that you are looking at a reflected-call failure rather than a bug inside the handler. The message names *types*, never the method — `using string as type int` does not tell you which command was being dispatched. So log the resolved name and the argument types next to the recover, or you are reading a stack trace that identifies the machinery but not the request. ## Pre-flight validation Everything you need is on the method value's own type. Because `MethodByName` returns a *bound* method value, `m.Type()` describes just the declared parameters: - `t.NumIn()` — how many parameters - `t.In(i)` — the i-th parameter's `reflect.Type` - `t.NumOut()` / `t.Out(i)` — the results, so you can insist a command returns exactly one `error` - `t.IsVariadic()` — whether the final parameter is `...T` - `arg.Type().AssignableTo(t.In(i))` — whether this argument will be accepted With those you convert an operator's strings into typed values, check each one, and return a normal error listing what was wrong. `Call` then only ever sees arguments you have already proved acceptable. ## Variadic methods A variadic method makes the arithmetic different. `NumIn()` counts the variadic parameter as one, and `In(NumIn()-1)` is a *slice* type, so an individual variadic argument must be assignable to that slice's element type, not to the slice. `Call` will collect the trailing arguments into the slice itself; `CallSlice` is the sibling that takes the finished slice as the last argument. Many command buses simply refuse to register variadic handlers, which is a defensible simplification. ## Assignability, not identity The rule `Call` applies is Go's own assignability rule, not type identity. A `*bytes.Buffer` is assignable to a parameter of type `io.Writer`; a named type with the same underlying type as the parameter is assignable when one of them is unnamed. That is why `AssignableTo` is the right question to ask and `==` on the two `reflect.Type` values is not — the latter rejects calls that would in fact have worked. ## Recover at the boundary, not everywhere Even with validation, a dispatcher that takes user input should wrap the call in a deferred `recover` so that a case you failed to anticipate becomes a failed command with a logged stack trace rather than a crashed process. That recover belongs at the dispatch boundary only. It is a safety net under validation, never a substitute for it: a `recover` with no pre-flight check turns every operator typo into a panic that costs a stack trace to diagnose, and hides genuine bugs inside the handlers behind the same catch. ## The wider point This is the whole trade of dynamic dispatch, made concrete. You have moved a class of error — wrong argument count, wrong argument type — from the compiler to run time. Everything the compiler used to guarantee for free now has to be re-implemented, tested and kept in step by hand.

  • The panic says 'using string as type int'. What does that message not tell you, and what would you log?
    It names the argument type and the parameter type but never the method — with a dozen registered commands you cannot tell which one was dispatched. Log the resolved name, the argument types you built, and the caller, next to the recover. The reflect.Value.call frames above your dispatcher confirm the failure is in the reflected call rather than inside the handler.
  • How do you dispatch to a variadic method through reflection?
    `Call` collects the trailing arguments into the variadic slice itself, so you pass the individual values. `CallSlice` is the alternative that takes an already-built slice as the final argument. Note that `NumIn()` counts the variadic parameter once and `In(NumIn()-1)` is the slice type, so a single variadic argument must be assignable to that slice's element type.
  • Before dispatching, how would you check that a resolved value satisfies a particular interface?
    `Type.Implements(u)` answers it, where `u` must be a `reflect.Type` of interface kind. You get one with the nil-pointer idiom: `reflect.TypeOf((*io.Writer)(nil)).Elem()` — take the type of a nil `*io.Writer` and step through the pointer. Passing a non-interface type to `Implements` panics.
  • Why compare with AssignableTo rather than comparing the two reflect.Type values for equality?
    `Call` applies Go's assignability rule, not type identity. A `*bytes.Buffer` argument is accepted by an `io.Writer` parameter, and a named type is assignable to an unnamed one with the same underlying type. Equality would reject calls that `Call` would happily make, so your validator would be stricter than the thing it guards.

saying these in an interview costs you the question

  • Expects Call to return an error instead of panicking
  • Says the compiler checks a reflected call's arguments
  • Relies on recover alone with no signature validation
  • Compares reflect.Type values with == instead of AssignableTo
  • Passes a variadic slice to Call rather than CallSlice
  • Blames the handler when reflect.Value.call is on the stack