skip to content

Calling Methods Dynamically

reflect can invoke a method it was handed by name and even synthesise a function at runtime, which is how generated doubles and dispatch tables get built without knowing the signature in advance.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In Go's reflect package, how do you invoke a method chosen by its name at runtime?

level: juniorimportance: should knowfreq 38%

answer

  1. find it, then invoke it
  2. the lookup takes a string, not an index
  3. the receiver is already attached
  4. arguments and results are both []reflect.Value
  5. an unknown name is not an error

basics

~10 s

Wrap the value with reflect.ValueOf, look the method up with Value.MethodByName, then invoke it with Value.Call, passing arguments as a []reflect.Value. Call hands back the results as []reflect.Value, which you unwrap with Value.Interface.

solid answer

~40 s

Three steps. `reflect.ValueOf(target)` gives you a `reflect.Value`; `MethodByName("Drain")` looks the method up by string and returns a *bound method value* — the receiver is already attached, so it behaves like a plain func of the declared parameters. You then call it with `Call([]reflect.Value{...})`, wrapping each argument with `reflect.ValueOf`, and get back a `[]reflect.Value` holding the declared results, which you unwrap with `Value.Interface()` plus a type assertion. Two things bite immediately: an unknown name returns the *zero* `reflect.Value` rather than an error, so guard with `IsValid()` before calling, and only **exported** methods are visible to reflection. Nothing here is checked at compile time — a wrong argument count or type is a run-time panic out of `Call`.

code

go · 17 lines
go
type Admin struct{ node string }

func (a *Admin) Drain(reason string) error {
	fmt.Printf("draining %s: %s\n", a.node, reason)
	return nil
}

// name arrives from an operator, as a plain string
func dispatch(target any, name, reason string) error {
	m := reflect.ValueOf(target).MethodByName(name)
	if !m.IsValid() {
		return fmt.Errorf("unknown command %q", name)
	}
	out := m.Call([]reflect.Value{reflect.ValueOf(reason)})
	err, _ := out[0].Interface().(error) // nil error yields a nil any
	return err
}

go deeper

for a junior

Be ready to write the three lines from memory: reflect.ValueOf, MethodByName, Call with a []reflect.Value. Know that the receiver is already bound and that results come back as a slice.

for a middle

Explain what MethodByName actually returns, why an unknown name is a zero Value rather than an error, and why only exported methods are visible. Say how you unwrap an error result.

for a senior

Show the guards you would put around this in production: IsValid before Call, signature validation before arguments are built, and a recover boundary so an operator typo cannot kill the process.

for a principal

Own the question of whether name-addressable dispatch belongs in the codebase at all, and what a team gives up — compile-time references, rename safety, a bounded surface — when it resolves methods from strings.

## The problem reflection solves here Ordinary Go dispatch is decided when you write the code: you name a method, the compiler resolves it, the linker wires it up. Sometimes the name only exists at run time — an operator types `drain` into an admin console, and a command bus has to find the handler that matches. `reflect` is the standard library's answer: it lets a program look at a value's type and methods as data, and invoke one chosen by a string. ## The three steps **1. Get a `reflect.Value`.** `reflect.ValueOf(x)` takes any value and returns a `reflect.Value` — a run-time handle on the value together with its dynamic type. **2. Look the method up.** `Value.MethodByName(name string) Value` searches the value's method set for a method with that name and returns a *method value*: a `reflect.Value` of func kind whose receiver is already bound. That binding matters — the func type it reports through `Type()` lists only the declared parameters, so you do **not** pass the receiver yourself. (The other route, `Type.Method(i)`, gives you a `Func` whose first parameter *is* the receiver; the two are easy to confuse.) If no such method exists, `MethodByName` does not return an error and does not panic. It returns the **zero** `reflect.Value`, for which `IsValid()` reports false. Calling `Call` on that zero value panics with `call of reflect.Value.Call on zero Value`, so a name arriving from a user is always checked first. Only **exported** methods are reachable. For a non-interface type, `reflect` deliberately reports only the exported method set, so `MethodByName("drain")` on a method named `drain` finds nothing — reflection does not breach package encapsulation. **3. Call it.** `Value.Call(in []Value) []Value` invokes the func. Each element of `in` must be *assignable* to the corresponding declared parameter type; the results come back as a slice in declaration order. To build an argument you wrap it: `reflect.ValueOf("node-7")`. To read a result you unwrap it: `out[0].Interface()` returns an `any`, which you type-assert. For a variadic method, `Call` collects the trailing arguments into the variadic slice for you; `CallSlice` is the variant that takes an already-built slice as the final argument. ## Getting an error result out A handler that returns `error` needs one extra piece of care. `out[1].Interface()` on a nil error yields a nil `any`, and a single-value type assertion on a nil interface **panics**. Use the comma-ok form: err, _ := out[1].Interface().(error) When the call succeeded, `err` is nil and `ok` is false, which is exactly what you want. `out[1].IsNil()` works too, because the result's kind is `Interface`. ## What is not checked Nothing about this call is verified by the compiler. The method name is a string, the arguments are a slice, and every mismatch — wrong count, wrong type, unexported name, nil receiver — shows up as a run-time panic or a silent miss. That is the trade you are making: you buy dispatch from data, and you pay with checks the compiler used to do for you. A dispatcher that takes operator input therefore validates the resolved method's signature before calling, and a panic escaping `Call` should not be allowed to take the process down. ## Which methods are visible `reflect.ValueOf(v)` exposes the method set of the value you handed it. Hand it a `T` and you see the methods declared on `T`; hand it a `*T` and you see those declared on `T` and on `*T`. A command bus therefore registers pointers to its handler structs, not copies of them. ## Cost A reflected call is far slower than a direct one — the lookup walks a name table, the arguments are boxed into `reflect.Value`s, and the call goes through the runtime rather than a direct branch. For an admin command that runs once a minute that is irrelevant; on a hot path it is not, and a plain `map[string]func(...)` registry is both faster and safer.

  • What does MethodByName give you back when the name matches nothing, and what happens if you call it anyway?
    It returns the zero `reflect.Value` — no error, no panic at lookup time. `IsValid()` on it reports false. Calling `Call` on that zero value panics with `call of reflect.Value.Call on zero Value`, so a dispatcher that takes a name from a user must check `IsValid()` first and turn a miss into an ordinary `unknown command` error.
  • The method returns (string, error). How do you pull the error out of the []reflect.Value results safely?
    Use the comma-ok assertion: `err, _ := out[1].Interface().(error)`. A nil error result unwraps to a nil `any`, and the single-value form `out[1].Interface().(error)` panics on that. `out[1].IsNil()` is an equivalent guard, since the result's kind is `Interface`.
  • Why can't MethodByName reach an unexported method?
    For non-interface types `reflect` reports only the exported method set — `NumMethod` counts exported methods and `MethodByName` searches only those. Reflection is not a way around package encapsulation; an unexported helper stays unreachable from a name string, which is one of the few things that bounds a dynamic dispatch surface.

saying these in an interview costs you the question

  • Thinks Call takes plain values rather than []reflect.Value
  • Passes the receiver as Call's first argument
  • Expects MethodByName to return an error for an unknown name
  • Believes reflection can invoke unexported methods by name
  • Type-asserts a nil error result without the comma-ok form
  • Assumes the compiler checks the argument types of a reflected call
open as a page

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

level: middleimportance: should knowfreq 33%

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.

open as a page

Why does a reachable reflect.Value.MethodByName stop Go's linker from pruning methods?

level: seniorimportance: should knowfreq 27%

basics

~20 s

The name is a run-time string, so the linker cannot prove which method it selects. Once MethodByName is reachable it conservatively keeps exported methods, so the binary grows and every exported method stays callable from data.

open as a page

What does reflect.MakeFunc create, and when would you reach for it in Go?

level: middleimportance: nice to knowfreq 21%

basics

~20 s

reflect.MakeFunc builds a function value of a func type you choose at runtime, implemented by a callback that receives arguments as []reflect.Value and returns results the same way. Ordinary typed code can then call it like any other function.

open as a page

What would you require before approving reflect.MethodByName dispatch of admin commands in a Go service?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Treat it as opening an API, not adding a feature. Require an allowlist of command names on one handler type, per-command authorization, validation before any call, and a test resolving every registered name so a rename cannot break dispatch silently.

open as a page