Method Expressions

Understand method expressions in Go - deriving a function from a method where the receiver becomes an explicit first argument, and when this feature is useful

A method expression turns a method into a plain function. The receiver, normally hidden before the method name, becomes the first parameter. Given a type Dog with a method Bark, you can write Dog.Bark (value receiver) or (*Dog).Bark (pointer receiver) and get a function that takes the dog instance as its first argument.

This is not a separate language construct. It is a direct consequence of how the compiler represents methods internally. For every method declaration, the compiler generates an implicit function where the receiver is a normal parameter. A method expression gives you access to that implicit function.

The Implicit Function Behind Every Method

Consider a simple type with one method:

type Counter struct {
    n int
}
func (c Counter) Value() int {
    return c.n
}
func (c *Counter) Increment() {
    c.n++
}

The compiler translates these methods into two hidden functions with the signatures:

  • Counter.Value(c Counter) int
  • (*Counter).Increment(c *Counter)

These function names are not valid Go identifiers (they contain a period), so you cannot write them directly in source code. But the compiler accepts the same notation when you use a method expression: Counter.Value and (*Counter).Increment are valid expressions that evaluate to function values matching those implicit signatures.

When you call a method normally — c.Value() — the compiler rewrites it to call the implicit function: Counter.Value(c). A method expression lets you capture that function explicitly.

Syntax for Value and Pointer Receivers

The form you use depends on the receiver type of the method.

  • For a method with a value receiver on type T, write T.Method. The resulting function type is func(T, ...) where ... are the method’s parameters.
  • For a method with a pointer receiver on *T, write (*T).Method. The resulting function type is func(*T, ...).

If the method is declared on a pointer receiver, you must use (*T).Method, not T.Method. The compiler rejects T.Method in that case, because the method set of T does not include pointer-receiver methods.

c := Counter{n: 10}
// Value receiver method expression.
fnValue := Counter.Value
fmt.Println(fnValue(c)) // prints 10
// Pointer receiver method expression.
fnIncr := (*Counter).Increment
fnIncr(&c)
fmt.Println(fnValue(c)) // prints 11

Parentheses around *T are required:

The expression *Counter.Increment would be parsed as *(Counter.Increment), which is invalid. You need to write (*Counter).Increment to group the pointer indirection with the type name.

Why Method Expressions Exist

The normal method call syntax — receiver.Method(args) — is convenient when you know the receiver upfront. But sometimes the receiver and the method name come from different places, or you want to pass the method itself as a function value to another function. A method expression disentangles the receiver from the method, making the method a first-class function that can be stored, passed, or mapped just like any other function.

A concrete scenario: you have a set of actions represented by methods on a struct, and you want to select which action to run based on a string input.

type TaskRunner struct{}
func (t *TaskRunner) Build()  { fmt.Println("building") }
func (t *TaskRunner) Deploy() { fmt.Println("deploying") }
func main() {
    actions := map[string]func(*TaskRunner){
        "build":  (*TaskRunner).Build,
        "deploy": (*TaskRunner).Deploy,
    }
    runner := &TaskRunner{}
    actions["build"](runner) // prints "building"
}

Here actions stores method expressions. When you index the map, you get a function that still needs a *TaskRunner receiver. You supply the receiver later, at the call site. This separates the choice of action from the target object, which is not possible with normal method calls or method values (where the receiver would be bound at the time the function is stored).

Method Expressions and Method Values — A Brief Distinction

It is easy to confuse method expressions with method values, especially because both involve the dot notation. The difference lies in what happens to the receiver.

  • A method expression does not bind a receiver. T.Method is a function that still expects a receiver as its first argument.
  • A method value binds a specific receiver. x.Method is a function where the receiver x is already captured; you call it with only the method’s remaining arguments.
c := Counter{n: 5}
// Method expression: needs receiver.
fn := Counter.Value
fn(c)   // explicit receiver
// Method value: receiver already bound.
fn2 := c.Value
fn2()   // no receiver argument

Don't expect a method expression to work without the receiver:

A common mistake is writing fn := Counter.Value and then calling fn() without arguments. The compiler will tell you that you supplied too few arguments. The function signature includes the receiver, so you must pass an instance of the type.

Method values are the topic of the next section. The current discussion focuses exclusively on method expressions.

Calling Method Expressions with a nil Receiver

Passing nil as the receiver to a method expression is legal as long as the method itself can handle a nil receiver. If the method dereferences the receiver pointer, it will panic.

type Logger struct{}
func (l *Logger) Log(msg string) {
    if l == nil {
        fmt.Println("nil logger:", msg)
        return
    }
    fmt.Println("log:", msg)
}
func main() {
    logFn := (*Logger).Log
    logFn(nil, "something happened") // prints "nil logger: something happened"
}

Methods that guard against nil receivers are idiomatic in Go. A method expression preserves this behavior because it simply makes the receiver an explicit argument. There is no implicit dereference or automatic nil check.

Panic on nil receiver without a nil check:

If you pass a nil pointer to a method expression for a method that accesses fields through the receiver, the program panics with a nil pointer dereference. Always ensure the method is nil-safe if nil is a possible argument.

Method Expressions for Interface Types

Method expressions also work with interface types. If an interface I contains method M, the expression I.M yields a function of type func(I, ...). The first argument is a value of the interface type.

This can be surprising, but it follows the same logic: the method’s receiver is an interface value, so the function takes that interface value as its first parameter.

type Stringer interface {
    String() string
}
type Point struct{ x, y int }
func (p Point) String() string {
    return fmt.Sprintf("(%d,%d)", p.x, p.y)
}
func main() {
    p := Point{3, 4}
    fn := fmt.Stringer.String
    fmt.Println(fn(p)) // prints "(3,4)"
}

The expression fmt.Stringer.String is a function func(fmt.Stringer) string. It works because Point satisfies fmt.Stringer, and p can be passed as the interface argument. The compiler ensures the concrete value fits the interface.

Interface method expressions work with any implementation:

Any type that satisfies the interface can be passed as the first argument. The method expression is not tied to a specific concrete type; it only cares that the receiver implements the interface.

Practical Pattern — Sorting with a Less Function

The sort package in the standard library provides a Slice function that takes a slice and a comparison function. A method expression can supply that comparison if your type already defines a Less method.

type Person struct {
    Name string
    Age  int
}
func (p Person) Less(other Person) bool {
    return p.Age < other.Age
}
func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
    }
    sort.Slice(people, func(i, j int) bool {
        // Use the method expression to compare two elements.
        return Person.Less(people[i], people[j])
    })
    fmt.Println(people) // [{Bob 25} {Alice 30}]
}

The expression Person.Less expects two Person arguments (the receiver and the other person). Inside the callback, you provide both. This avoids duplicating the comparison logic and keeps the Less method as the single source of truth.

Common Pitfalls

Mixing up value and pointer receivers

If a method is defined on a pointer receiver, T.Method does not exist. You must write (*T).Method. If you try T.Method for a pointer-receiver method, the compiler error will tell you that the method is not in the method set of T.

type Data struct{ v int }
func (d *Data) Set(v int) { d.v = v }
// Wrong: Data.Set does not exist because Set has a pointer receiver.
// setFn := Data.Set
// Correct:
setFn := (*Data).Set
d := &Data{}
setFn(d, 42)

Forgetting that the function signature now includes the receiver

When you pass a method expression as an argument to a higher-order function, the expected signature must match. Forgetting the receiver parameter leads to type errors.

func apply(f func(*Counter)) {
    c := &Counter{}
    f(c)
}
// Wrong: (*Counter).Increment is func(*Counter), which matches.
apply((*Counter).Increment) // works
// But Counter.Value is func(Counter) int, not func(*Counter).
// apply(Counter.Value) would be a compile error.

Assuming method expressions are the only way to pass methods around

Method values (discussed in the next section) are often more natural when the receiver is already known. Use method expressions when the receiver needs to be supplied later or when you need to decouple the action from the instance.

Summary

A method expression takes a method and exposes it as an ordinary function with the receiver as the first argument. The compiler already generates such a function internally for every method. The expression T.M or (*T).M gives you that function in your code.

This becomes useful when you need to treat a method as a standalone function — passing it as a callback, storing it in a map, or supplying it as a comparison function. It separates the action (which method to call) from the target (which instance to call it on). Interface types also support method expressions, yielding functions that accept an interface value as the explicit receiver.