How to Invoke Methods with Reflection in Go

Use the reflect package's MethodByName and Call functions to dynamically invoke methods in Go.

When the compiler can't see the method name

You are building a command-line tool where users type a command and the program runs the matching function. The user types deploy, and you want to call cmd.Deploy(). They type status, and you want cmd.Status(). Writing a giant switch statement works until you have fifty commands. You want the code to figure out which method to call based on the string the user typed.

Or maybe you are writing a generic retry wrapper. You have a function that takes any object and a method name, and it needs to call that method, catch errors, and retry. The method name isn't known until runtime. The compiler can't check the call site because the target depends on user input or configuration.

That is where reflection comes in. Go's reflect package lets you inspect types, values, and methods while the program is running. You can find a method by name and invoke it dynamically. Reflection gives you flexibility that static typing doesn't allow, but it trades away compile-time safety and performance. Use it when you have no other choice.

Reflection is a mirror at runtime

Normal Go code is static. The compiler knows every type and every call. Reflection is dynamic. It exposes the internal representation of Go types to your code. The reflect package provides two main types: reflect.Type and reflect.Value.

reflect.Type describes the shape of a type. It tells you the name, the number of fields, the method signatures, and the underlying kind. You use it for inspection. reflect.Value wraps an actual value. It holds the data and lets you read fields, call methods, and modify values. You use it for interaction.

Think of reflect.Type as the blueprint of a house. It shows the rooms and doors. reflect.Value is the house itself. You can walk into the rooms and turn on the lights.

The entry point is reflect.ValueOf. You pass any value, and it returns a reflect.Value wrapper. From there, you can navigate the structure. Methods are accessed via MethodByName, which returns a reflect.Value representing the bound method. Calling Call on that value executes the method.

The minimal call

Here is the simplest way to invoke a method by name. You need a struct with a method, a way to find the method, and a call to execute it.

package main

import (
	"fmt"
	"reflect"
)

// Greeter holds a name for greeting.
type Greeter struct {
	Name string
}

// Greet prints a message using the receiver's Name field.
func (g *Greeter) Greet() {
	fmt.Println("Hello,", g.Name)
}

func main() {
	g := &Greeter{Name: "Alice"}
	v := reflect.ValueOf(g)
	// MethodByName returns a Value representing the method.
	// It returns an invalid Value if the method doesn't exist.
	method := v.MethodByName("Greet")
	if !method.IsValid() {
		fmt.Println("Method not found")
		return
	}
	// Call executes the method. Arguments must be a slice of reflect.Value.
	// An empty slice works for methods with no parameters.
	method.Call(nil)
}

The code creates a Greeter, wraps it in a reflect.Value, finds the Greet method, and calls it. The output is Hello, Alice. The IsValid check is essential. If the method name is wrong, MethodByName returns a zero reflect.Value. Calling Call on a zero value panics. Always check IsValid before invoking.

What happens under the hood

When you call reflect.ValueOf(g), the runtime allocates a reflect.Value struct on the heap. This struct stores a pointer to the type information and a pointer to the actual data. The data might be on the stack or the heap depending on the value. The wrapper adds indirection.

MethodByName("Greet") iterates over the method set of the type. It compares the string "Greet" against the names of all exported methods. If it finds a match, it returns a new reflect.Value that represents the bound method. This value contains the receiver and the function pointer.

method.Call(nil) prepares the call. It checks that the number of arguments matches the method signature. Since Greet takes no arguments, nil is acceptable. The runtime invokes the function with the receiver bound. The result is a slice of reflect.Value representing the return values. If the method returns nothing, the slice is empty.

The receiver name is usually one or two letters matching the type. Use (g *Greeter), not (this *Greeter) or (self *Greeter). This convention keeps code concise and readable. Most Go developers expect short receiver names.

Pointers, values, and method sets

Reflection follows Go's method set rules. This is where beginners get tripped up. A struct value and a pointer to that struct have different method sets.

If a method has a pointer receiver, you can only call it on a pointer. If you pass a value to reflect.ValueOf, MethodByName might return an invalid value, or the call might panic. The compiler enforces this at compile time. Reflection enforces it at runtime.

package main

import (
	"fmt"
	"reflect"
)

// Counter tracks a number.
type Counter struct {
	N int
}

// Increment adds one to N. It requires a pointer receiver.
func (c *Counter) Increment() {
	c.N++
}

func main() {
	c := Counter{N: 0}
	v := reflect.ValueOf(c)
	// MethodByName returns invalid because Increment has a pointer receiver.
	// c is a value, not a pointer, so the method is not in the set.
	method := v.MethodByName("Increment")
	if !method.IsValid() {
		fmt.Println("Cannot call pointer method on value")
	}

	// Use a pointer to access the method.
	pv := reflect.ValueOf(&c)
	method = pv.MethodByName("Increment")
	if method.IsValid() {
		method.Call(nil)
		fmt.Println("N is now", c.N)
	}
}

The first attempt fails because c is a value. Increment requires *Counter. The method set of Counter does not include Increment. The method set of *Counter does. When you pass &c, MethodByName finds the method, and the call succeeds.

If you have a reflect.Value and you're not sure if it's a pointer, check v.Kind() == reflect.Ptr. If you need to call a pointer method but have a value, you can use v.Elem() if the value is addressable, or create a pointer with reflect.New(v.Type()). Addressability is another reflection quirk. You can only modify a value if it's addressable. Values created by reflect.ValueOf are not addressable unless they came from a pointer or a slice element.

A realistic helper with safety checks

In real code, you rarely call reflection directly. You wrap it in a helper that checks types, counts arguments, and handles errors. This keeps the panic risk contained.

package main

import (
	"fmt"
	"reflect"
)

// InvokeMethod calls a method by name on an object with arguments.
// It returns the first return value or an error.
func InvokeMethod(obj any, methodName string, args ...any) (any, error) {
	v := reflect.ValueOf(obj)
	if v.Kind() == reflect.Ptr && v.IsNil() {
		return nil, fmt.Errorf("cannot invoke method on nil pointer")
	}
	method := v.MethodByName(methodName)
	if !method.IsValid() {
		return nil, fmt.Errorf("method %s not found", methodName)
	}
	// Convert arguments to reflect.Value slice.
	// Each argument must match the method's parameter type.
	in := make([]reflect.Value, len(args))
	for i, arg := range args {
		in[i] = reflect.ValueOf(arg)
	}
	// Call executes the method. It panics if argument types or counts mismatch.
	// A recover block could catch this, but explicit checks are clearer.
	results := method.Call(in)
	if len(results) == 0 {
		return nil, nil
	}
	// Interface() returns the underlying value as an empty interface.
	// The caller must type-assert to the expected type.
	return results[0].Interface(), nil
}

// Calculator provides arithmetic methods.
type Calculator struct{}

// Add returns the sum of two integers.
func (c *Calculator) Add(a, b int) int {
	return a + b
}

func main() {
	calc := &Calculator{}
	result, err := InvokeMethod(calc, "Add", 10, 20)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// Use comma-ok idiom for safe type assertion.
	// If the type is wrong, ok is false and sum is zero.
	sum, ok := result.(int)
	if !ok {
		fmt.Println("Result is not an int")
		return
	}
	fmt.Println("Sum:", sum)
}

The helper checks for nil pointers, validates the method exists, and converts arguments. It returns the first result as any. The caller uses a type assertion to extract the concrete type. The comma-ok idiom prevents panics if the type is unexpected.

The if err != nil pattern applies here too. Reflection often returns errors or panics. The community accepts the boilerplate because it makes the unhappy path visible. Don't swallow errors. Return them or handle them explicitly.

Context is plumbing. If you invoke a method via reflection, that method still needs a context.Context as its first argument. Reflection doesn't inject context for you. You must pass it in the argument slice. Functions that take a context should respect cancellation and deadlines, regardless of how they are called.

Pitfalls and runtime panics

Reflection is powerful but dangerous. The compiler can't check your code. Mistakes surface at runtime, often as panics.

If you pass a string where an int is expected, the program panics with reflect: Call of int method with string argument. The error message tells you the mismatch. If you pass too many or too few arguments, you get reflect: Call with too many/few arguments. Always validate argument counts and types before calling.

If you try to call an unexported method, MethodByName returns an invalid value. Go's visibility rules apply. Public names start with a capital letter. Private names start lowercase. Reflection respects these rules. You cannot invoke a method starting with a lowercase letter from another package. The compiler enforces this at compile time; reflection enforces it at runtime by returning an invalid value.

Performance is another concern. Reflection is slow. Every reflect.Value allocation hits the heap. Method lookups iterate over slices. Type checks happen dynamically. A reflected call can be ten to fifty times slower than a direct call. If you're in a hot loop, avoid reflection. Use interfaces or code generation instead.

Goroutine leaks can happen if you invoke a method that starts a goroutine and you lose the handle. Reflection doesn't manage goroutine lifecycles. If the invoked method spawns a background task, you need a cancellation path. The worst goroutine bug is the one that never logs. Always ensure long-running tasks can be stopped.

Trust gofmt. Reflection code can look dense with nested calls and type assertions. The formatter keeps indentation consistent. Don't argue about formatting. Let the tool decide. Most editors run gofmt on save.

When to reach for reflection

Reflection solves specific problems. It is not a general-purpose tool. Use it sparingly.

Use reflection when you need to inspect types or values dynamically, such as in serialization libraries, generic logging frameworks, or plugin systems where the structure is unknown at compile time.

Use interfaces when you know the set of behaviors at compile time. Interfaces are faster, safer, and the compiler checks them for you. If you can define an interface that covers your use case, prefer it over reflection.

Use a map of functions when you have a fixed set of string-to-function mappings. A map lookup is instant and avoids reflection overhead. Register your functions at startup and dispatch via the map.

Use code generation when you need to write repetitive boilerplate. Tools like go generate produce standard Go code that runs at full speed. If you're generating code to avoid reflection, you're doing it right.

Accept interfaces, return structs. If you're writing a library that uses reflection, accept an any or an interface and return concrete values. Don't return reflect.Value to your users. They don't want to deal with reflection internals.

Reflection breaks the compiler's safety net. Use it only when the compiler can't help you.

Where to go next