AlgoMaster Logo

Dynamic Function Invocation

Low Priority12 min readUpdated June 6, 2026

Calling a function in Go usually looks like cart.AddItem(...), where the compiler resolves the name at build time. Reflection flips that around: you hold a function or a method as a reflect.Value, build its arguments as a []reflect.Value, and invoke it through Value.Call. This lesson covers how that machinery works, how to dispatch methods by their string name on a service struct, and how to handle the panics that misuse can trigger.

Calling a Plain Function With reflect

The starting point is reflect.ValueOf(fn). Given any function value, it returns a reflect.Value whose Kind() is reflect.Func. Once you have that value, Call runs the function. Both the arguments and the return values flow through []reflect.Value slices.

Three things happened in that small program. reflect.ValueOf(discountedPrice) wrapped the function as a Value. The args slice was built by wrapping each Go value in another reflect.ValueOf. fn.Call(args) ran the function and returned a []reflect.Value whose length matches the function's return count. To get a usable Go value back out, results[0].Interface() returns an interface{}, and the type assertion .(float64) recovers the original type.

The shape of args has to match the function's signature exactly: same number of values, same types, same order. Anything else panics. We'll come back to that.

Building the Args Slice

The args slice is the part most people get wrong. Every entry must be the type the function declares for that parameter, no implicit conversions. An int is not an int64. A float32 is not a float64. A named type like type ProductID int is not interchangeable with int.

The second call panics because int32 and int are different types to the reflect package, even though they hold the same number. The panic message is consistent: reflect: Call using X as type Y. When you see it, the fix is to coerce the value to the declared parameter type before wrapping it in reflect.ValueOf, or to use Value.Convert(targetType) to cast a reflect value into another reflect type.

Convert is useful when you're reading argument values from an outside source (JSON, a config file, a CLI flag) and need to massage them into the function's exact types before calling.

Reading Return Values

Call returns []reflect.Value. Each entry is one of the function's return values, in declaration order. To pull a typed value out, you have two options. The safest is .Interface() plus a type assertion, which gets you back to an ordinary Go value. The other option is the reflect.Value accessors like .Int(), .String(), .Float(), which return the value as the matching primitive Go type.

The error return needs an extra step. error is an interface, and the zero value of an interface is nil. Calling .Interface().(error) on a nil interface value works, but you usually want to check IsNil() first to avoid a confusing nil-but-typed result later in your code. The pattern shown above is the one most reflection-heavy libraries (validators, RPC dispatchers, test runners) use.

Methods on a Receiver

Functions are one half of the story. Methods, which carry a receiver, need a slightly different lookup path. Once you have a reflect.Value for the receiver, two methods get you the method:

  • Value.Method(i) returns the i-th method, where i runs from 0 to Type.NumMethod()-1.
  • Value.MethodByName(name) looks up the method by its exported name. Returns a zero Value if the method doesn't exist.

Both return a reflect.Value that you can call with Call. The receiver is already bound, so the args slice does not include it.

A few details deserve attention. reflect.ValueOf(cart) wraps the pointer, not the struct value. That matters because AddItem is defined on *CartService, so it only belongs to the method set of the pointer type. If you wrapped *cart (the dereferenced struct), MethodByName("AddItem") would return a zero Value, and the subsequent Call would panic with reflect: call of reflect.Value.Call on zero Value. The same rule that governs method sets in normal Go code governs them under reflection.

When Checkout takes no arguments, you can pass nil to Call. An empty []reflect.Value{} also works. Both are equivalent.

Listing the Methods on a Type

Type.NumMethod() reports how many methods belong to the type, and Type.Method(i) returns metadata for the i-th one. The metadata is a reflect.Method struct with the name, the function type, the function value, and the package path (which is empty for exported methods).

Two things to flag. The method order is alphabetical, not declaration order, so Cancel appears before Place even though Place was declared first. And Type.Method(i) reports the method's type as a free function whose first parameter is the receiver. That's why the printed signature shows func(main.OrderService, int) string rather than func(int) string. If you reach the method through reflect.ValueOf(svc).Method(i) instead, the receiver is already bound and the type drops the first parameter.

Variadic Functions and CallSlice

A variadic function declares its last parameter as ...T, and to the caller it looks like a normal slice on one side and a flat parameter list on the other. Reflection exposes both forms.

Call treats every argument as one positional parameter, so for a variadic function you can pass each variadic value as its own reflect.Value. CallSlice is for when you already have the variadic values packed into a single []T and want reflect to pass them as the spread argument, the same way f(items...) would in normal Go.

The two calls produce equivalent inputs to bulkAdd. The choice is about where the slice lives in your code: if you already have a []string, CallSlice avoids unpacking and re-packing. If you have individual values, Call is simpler. Mixing them up panics. Passing a slice to Call for a variadic param would mean "the next variadic value is itself a slice", and passing individual values to CallSlice panics with a length-mismatch message.

CallSlice only applies to variadic functions. For a normal function, both forms behave the same, but using CallSlice on a non-variadic function panics.

Dispatching by Name: A Command Router

The most useful application of dynamic invocation is dispatching a method by its string name. An HTTP handler receives {"command": "AddItem", "args": [...]}, looks up the method by name on a service struct, builds the args slice from the incoming JSON, and calls it. The same pattern shows up in test runners, RPC servers, plugin systems, and command-line tools.

The flow looks like this:

The diagram captures the runtime path. The name comes in, MethodByName either finds a reflect.Value or returns a zero Value, and the rest of the flow is the args build, the call, and the result unwrap.

Here's the full router as a runnable program:

Read the dispatch function carefully. It's small but handles four kinds of failure that would otherwise panic at runtime:

  1. The method name doesn't exist (m.IsValid() returns false).
  2. The arg count is wrong (compared against mt.NumIn()).
  3. An arg has the wrong type (and isn't convertible to the expected type).
  4. An arg isn't convertible at all (ConvertibleTo returns false).

The validation runs before Call, so the panic-prone parts of reflection are guarded. The function returns ordinary error values, which the caller handles like any other Go API.

Real dispatchers add more, like context propagation, middleware, and structured error types, but the core is exactly this: lookup, validate, convert, call, unwrap.

Errors From Dynamic Calls: Panic vs Return

Reflection mixes two error reporting styles. The function being called returns errors through its own signature, the way any Go function does. The reflection machinery around the call panics when it's misused. Knowing which is which avoids a lot of confusion.

The panic conditions are well-defined. They all come from misusing Call itself, not from anything the called function did:

MisusePanic Message (approximate)
Call on a zero reflect.Valuereflect: call of reflect.Value.Call on zero Value
Call on a non-function Valuereflect: call of reflect.Value.Call on X Value
Wrong arg countreflect: Call with too few input arguments / too many
Wrong arg typereflect: Call using X as type Y
CallSlice on non-variadic functionreflect: CallSlice of non-variadic function
CallSlice with wrong last-arg typereflect: CallSlice with too few input arguments (or type mismatch)

The return values are anything the called function chose to return. If the function's signature is func() (T, error), then out[1] is the error. Treat that error the same way you would in any Go code.

A robust call site handles both. The outer defer recover() catches reflection panics and converts them to errors; the inner out[len(out)-1] check extracts the error from the function's return value when that return follows the Go convention.

Three calls, three different paths. The first one runs cleanly. The second runs cleanly at the reflection layer, but the function returns an error that we unwrap from out[1]. The third panics inside Call, the deferred recover turns it into a normal error return, and the caller never has to handle a panic.

The convention most reflection-based libraries follow is to treat panic-from-Call as a programmer error (a bug to fix) and return-value errors as user errors (something the caller should handle and propagate). The safeCall wrapper lets your dispatcher decide whether to log-and-continue or crash on the first programmer error.

Quiz

Dynamic Invocation Quiz

10 quizzes