Method Values

Understand method values in Go - how binding a receiver to a method creates a reusable function value that carries the original receiver state

A method value is created when you take a method attached to a specific instance and store it as a standalone function value. The expression x.M — where x is a value of a type that has a method M — does not call the method immediately. It produces a function that, when called later, will invoke M with the receiver already bound to the particular x that existed at the moment the method value was created.

This is not the same as calling x.M() directly. It is not the same as a method expression T.M, which takes the receiver as its first argument. A method value locks the receiver in place, turning a two‑argument method into a one‑argument function.

Why Method Values Exist

Go deliberately keeps functions and methods separate. A method is syntax sugar over a function that takes a receiver as its first parameter, but that sugar creates a natural connection between data and behavior. However, there are many situations where you want to hand over a piece of behavior to another function — a callback, a goroutine, or a sort comparator — without writing a small wrapper yourself.

Without method values, you would need to write a closure that calls the method:

sort.Slice(people, func(i, j int) bool {
    return people[i].Less(people[j])
})

A method value collapses that into:

sort.Slice(people, people[i].Less)

The method value people[i].Less is a function of one argument — the other person — because the receiver (people[i]) is already supplied. This is exactly what a method call would look like, but the receiver is captured early and the call is deferred. The result is less boilerplate and code that reads more like a natural property of the value.

Receiver capture:

Method values are not just a shorthand. The receiver is evaluated and saved at the moment of creation, not at the moment of the eventual call. This has important consequences for mutable state, which we will explore.

How Method Values Work

When the compiler sees x.M, it checks the static type of x. If M is in the method set of that type (including any automatically promoted or pointer‑adjusted methods), the expression yields a function value. The signature of that function value is the signature of the method M minus the receiver parameter.

For a method func (t T) M(a int) string, the method value x.M has type func(int) string. For a method with a pointer receiver func (t *T) M(a int) string, x.M still yields func(int) string — the receiver is not part of the visible call interface. This is true whether x is a value or a pointer, as long as the method is reachable via the method set.

The receiver is evaluated immediately. If x is a pointer, the pointer is dereferenced (if necessary) and the concrete value it points to at that exact moment becomes the receiver for all future calls. Any later changes to the original variable do not affect the saved receiver.

This is easiest to see with a small example:

type Counter struct {
    n int
}
func (c *Counter) Inc() {
    c.n++
}
c := &Counter{n: 10}
incFn := c.Inc    // method value: receiver c captured now
c.n = 99          // modify the original pointer target
incFn()           // calls (*Counter).Inc on the captured c from earlier
fmt.Println(c.n)  // prints 100, not 100? Wait... Let's think.

Wait, this needs careful handling. If c is a pointer, c.Inc captures the pointer itself, not the value of c.n. So incFn holds a copy of the pointer (the address). Later, when c.n = 99 modifies the object through the original pointer, the captured pointer still points to the same object. So incFn() increments c.n from 99 to 100. The point is that the pointer is captured, not the dereferenced value. If the method had a value receiver, the value would be copied at creation time. Let's use an example that clearly demonstrates the difference.

type Box struct {
    label string
}
func (b Box) Describe() string {
    return b.label
}
b := Box{label: "original"}
describeFn := b.Describe   // value receiver, so b is copied now
b.label = "changed"
fmt.Println(describeFn()) // prints "original"

Here, b.Describe creates a method value that holds a copy of b (because the receiver is a value type). Changing the original b.label afterward does not affect the copy stored inside describeFn. This is a critical behavioral distinction: value‑receiver method values snapshot the receiver; pointer‑receiver method values snapshot the pointer, so they follow the same object but through a fixed address.

Pointer‑vs‑value snapshots:

If you find that a method value still reflects external changes after creation, check whether the receiver is a pointer. A pointer receiver binds the pointer, not the underlying data, so future mutations to the same pointed‑to memory will be visible. A value receiver binds a copy, making the method value independent of the original variable.

Creating and Using Method Values

A method value is formed by writing value.MethodName. You do not call it; you assign it or pass it around. This pattern appears whenever you need to decouple the caller from the call.

Consider a simple logger that writes to a file:

type Logger struct {
    prefix string
    w      io.Writer
}
func (l Logger) Log(msg string) {
    fmt.Fprintf(l.w, "%s: %s\n", l.prefix, msg)
}
func main() {
    file, _ := os.Create("app.log")
    defer file.Close()
    appLogger := Logger{prefix: "APP", w: file}
    logFunc := appLogger.Log  // method value: func(string)
    // Use the method value as a plain callback
    processItems(logFunc)
}
func processItems(log func(string)) {
    log("processing started")
    // ...
    log("processing finished")
}

The logFunc is a simple func(string). The caller processItems knows nothing about the logger’s prefix or writer; it only knows how to emit a message. The binding of prefix and writer happened once, at creation time, inside main. This is a form of encapsulation without interfaces or complex wiring.

Clean callback:

If you find yourself writing a closure that only calls a method with the same arguments, replace it with a method value. It removes unnecessary indirection and makes the intent clearer.

Method Values with Pointer Receivers

Pointer receivers allow method values to retain access to mutable state. Because the method value holds a copy of the pointer (the address), the receiver object remains alive and modifiable. This is useful for counters, state machines, or any long‑lived resource.

type Player struct {
    score int
}
func (p *Player) AddPoints(pts int) {
    p.score += pts
}
func main() {
    p := &Player{score: 0}
    add := p.AddPoints // func(int)
    add(10)
    add(5)
    fmt.Println(p.score) // 15
}

The method value add behaves exactly as if we were calling p.AddPoints(x) each time. Because p is a pointer, the method value captures the address of the Player struct, so add always mutates the same instance.

A subtlety arises when the original pointer variable is reassigned to point somewhere else. The method value still holds the old address, so later increments go to the old object:

p := &Player{score: 0}
add := p.AddPoints
p = &Player{score: 1000} // new Player
add(1)
fmt.Println(p.score) // 1000 (the new Player unchanged)
// The old Player now has score 1, but is unreachable except through 'add'.

This is consistent: the method value captured the pointer value (the address), not the variable p itself. If you need the method value to always refer to the current p, you would wrap it in a closure instead.

Method values do not track variable reassignment:

A method value captures the receiver’s identity at the point of creation. If you later reassign the variable that originally held the receiver, the method value continues to refer to the old instance. This can lead to hard‑to‑detect bugs when you expect the behavior to “follow” the variable. Use a closure if you need late binding to a changing receiver variable.

Method Values with Value Receivers

When the receiver is a value (not a pointer), the method value takes a copy of the entire receiver. All subsequent calls operate on that snapshot. This makes method values of value receivers inherently immutable from the outside — no mutation of the original will affect them, and they cannot affect the original.

type Config struct {
    Port int
}
func (c Config) Addr() string {
    return fmt.Sprintf(":%d", c.Port)
}
func main() {
    cfg := Config{Port: 8080}
    addrFn := cfg.Addr    // captures a copy of cfg
    cfg.Port = 3000
    fmt.Println(addrFn()) // ":8080"
}

The snapshotting semantics are useful when you want to freeze a configuration at a specific moment and pass that frozen view into a goroutine or a handler without worrying about later changes.

However, this also means that if the method is called many times and the receiver is large, you pay the copy cost at creation time, not at call time. That is rarely a problem for small structs, but it is worth noting for large data structures.

Method Values and the Method Set

Go’s method set rules ensure that a method value can be formed from any expression whose static type includes the method. That includes:

  • A value of type T can produce method values for all methods with value receiver T.
  • A pointer of type *T can produce method values for methods with either value receiver T or pointer receiver *T.
  • Addressable values of type T can also produce method values for pointer receiver methods (the compiler automatically takes the address).

Thus, the expression t.Mp where Mp has a pointer receiver works even if t is a value, as long as t is addressable. The method value captures &t. If t is not addressable (for example, the return value of a function), you cannot form a method value for a pointer receiver method directly; you must store the value in a variable first.

func getConfig() Config { return Config{} }
// getConfig().Addr() is fine (Addr has value receiver).
// getConfig().SetPort(3000) would be a call, also fine if SetPort has pointer receiver because call context allows addressability.
// fn := getConfig().SetPort // compile error: cannot take address of getConfig()

This is the same addressability rule that applies to method calls; method values are no exception.

Method Values vs. Closures

A method value looks similar to a closure that captures a receiver and calls the method. In fact, the compiler creates something very close to that: a closure with the receiver stored in its environment. But there are differences worth knowing:

  • Creation cost: A method value is typically as cheap as copying the receiver (for value receivers) or copying a pointer (for pointer receivers). A manually written closure may capture additional variables.
  • Readability: The expression obj.Method is concise and immediately recognizable as “the method bound to this object”. A closure func(x int) { obj.Method(x) } requires the reader to parse the body.
  • Underlying identity: Two method values created from the same receiver and method are not comparable (function values are not comparable in Go), but they have distinct allocations if the receiver is a non‑pointer value. Closures behave similarly.

When you need late binding (the receiver to be determined at call time, not at creation time), a closure is the right tool. For early binding — the receiver is known now and should be fixed — a method value is clearer.

Common Mistakes

Expecting a method value to always reflect the latest receiver state. This is the single most frequent misunderstanding. As demonstrated, a value‑receiver method value freezes the receiver; a pointer‑receiver method value freezes the pointer target’s identity but not its contents. However, if the pointer variable is reassigned, the method value still points to the old object.

Confusing method values with method expressions. A method expression T.M gives you a function func(T, ...). A method value x.M gives you func(...). Mixing them up leads to compile‑time errors about too few arguments.

Creating a method value from an unaddressable value for a pointer receiver. This is a compile‑time error:

type T struct{}
func (t *T) M() {}
_ = T{}.M // error: cannot take address of T{}

You must use a variable:

t := T{}
f := t.M // ok

Real‑World Usage Patterns

Sorting with Method Values

The sort package expects a less function of two indices. A method value can supply it directly from a slice element:

type Person struct {
    Name string
    Age  int
}
func (p Person) OlderThan(other Person) bool {
    return p.Age > other.Age
}
people := []Person{{"Alice", 30}, {"Bob", 25}}
sort.Slice(people, func(i, j int) bool {
    return people[i].OlderThan(people[j])
})
// With method value:
sort.Slice(people, func(i, j int) bool {
    return people[i].OlderThan(people[j])
})

Wait, the method value version still needs the closure because OlderThan requires two arguments. Actually, we could use people[i].OlderThan as a method value, but it still expects one argument: the other person. So the closure is unavoidable unless we use a different design. However, for methods that take no arguments (like a Key() method for grouping), method values shine:

groups := make(map[string][]Person)
for _, p := range people {
    keyFn := p.Key   // method value func() string
    groups[keyFn()] = append(groups[keyFn()], p)
}

But a simpler common case is a method that returns a single value and takes no arguments. Those method values fit exactly into functional helpers that expect func() T.

Passing Behavior to a Runner

A job runner might accept a func() and execute it. A method value can bind the method directly to its receiver, avoiding a separate wrapper struct:

type Worker struct {
    id int
}
func (w Worker) DoTask() {
    fmt.Printf("Worker %d running\n", w.id)
}
func execute(fn func()) {
    fn()
}
w := Worker{id: 42}
execute(w.DoTask) // method value

This pattern appears frequently in testing, where you want to supply a method as a test callback without defining a new type.

Event Handlers and Callbacks

GUI or network libraries often accept callback functions. A method value makes it natural to attach an object’s behavior directly:

type Button struct {
    label string
}
func (b *Button) Click() {
    fmt.Println(b.label, "clicked")
}
func onUserAction(handler func()) {
    // simulate event
    handler()
}
btn := &Button{label: "Submit"}
onUserAction(btn.Click) // method value

Summary

Method values take the receiver parameter out of the equation at creation time, turning a method into an ordinary function that can be stored, passed, and called later. They are a lightweight alternative to closures when you want to fix the receiver early. The key insight is that the receiver is evaluated and saved immediately — for value receivers, this means a snapshot; for pointer receivers, this means a captured pointer.

  • Use method values to eliminate trivial closures that only call a method with the same arguments.
  • Watch for the difference between value‑receiver snapshots and pointer‑receiver aliasing.
  • Do not expect method values to follow variable reassignment; they bind to the object identity, not the variable name.