skip to content

What is a method value in Go, and what does `f := runner.Warmup` capture?

level: juniorimportance: must knowfreq 62%

answer

  1. a method written without its parentheses
  2. the receiver rides inside the function value
  3. signature minus the receiver parameter
  4. evaluated once, where you write it
  5. value receiver copies, pointer receiver saves address

basics

~20 s

A method value is the function value you get by writing runner.Warmup with no call parentheses. Go evaluates and saves the receiver at that moment, so calling f() later runs Warmup on that saved receiver and takes no receiver argument.

solid answer

~40 s

Writing `runner.Warmup` without parentheses produces a method value: a function whose type is the method's signature with the receiver removed. If the method is declared `func (r *Runner) Warmup(n int) error`, then `runner.Warmup` has type `func(int) error`. The receiver expression is evaluated once, right where you write it, and stored alongside the function; every later call reuses that saved receiver. That is what lets you hand a method straight to anything that wants a plain function value: `time.AfterFunc(d, runner.Warmup)`, a `map[string]func()` registry of named workloads, or `mux.HandleFunc("/health", app.health)`. The consequence worth stating in an interview is that the binding is eager, not lazy. With a value receiver the saved receiver is a copy taken at that instant; with a pointer receiver Go saves the address, so calls read live state.

code

go · 15 lines
go
type Runner struct {
	Name string
}

func (r *Runner) Warmup(n int) error {
	fmt.Println(r.Name, n)
	return nil
}

func demo() {
	runner := &Runner{Name: "mix-a"}

	var f func(int) error = runner.Warmup // method value, receiver already bound
	_ = f(3)                              // prints: mix-a 3
}

go deeper

for a junior

Be ready to say what runner.Warmup without parentheses evaluates to, give its type, and show one place you would pass it, such as a timer callback or a map of named functions.

for a middle

Explain the binding moment: the receiver expression is evaluated once, right there, and a value receiver saves a copy while a pointer receiver saves an address. Mention that the pointer form needs an addressable operand.

for a senior

An interviewer expects you to connect eager binding to real bugs — a table of handlers built at startup that silently freezes state — and to say which receiver form you would declare so callers cannot make that mistake.

for a principal

Frame it as an API-shape decision: whether your package hands out method values at all, or exposes a constructor returning a function, determines how much of your struct's mutability leaks into other teams' call graphs.

## The construct In Go a selector like `runner.Warmup` can be used two ways. With parentheses, `runner.Warmup()`, it is a call. Without them, `runner.Warmup` is an expression whose value is a **function**, and Go calls that a *method value*. The type of a method value is the method's signature **minus the receiver**. Given ```go func (r *Runner) Warmup(n int) error ``` the expression `runner.Warmup` has type `func(int) error`. There is no receiver parameter left for the caller to supply, because the receiver has already been supplied — that is the whole point of the construct. ## When the receiver is bound The receiver expression is evaluated **once, at the point where the method value is created**, and the result is saved with the function. It is not looked up again when the function is finally called. This matters even when you never store the method value in a variable: passing `runner.Warmup` directly as an argument binds the receiver at that argument position. What gets saved depends on how the method declares its receiver: - **Value receiver**, `func (r Runner) Warmup()`: Go saves a **copy** of `runner` as it looked at that instant. Fields written afterwards are written to a different value, and the saved copy never sees them. - **Pointer receiver**, `func (r *Runner) Warmup()`: `runner.Warmup` is shorthand for `(&runner).Warmup`, so the saved receiver is the **address**. The pointer is copied, the pointed-to struct is not, so calls made later read whatever the struct holds then. Because the pointer form needs `&runner`, the operand has to be **addressable**. A local variable, a field of an addressable struct, a slice element or an already-pointer expression are all fine. A map element or the result of a function call is not addressable, so `workloads["burst"].Warmup` will not compile for a pointer-receiver method; copy the element into a local variable first and take the method value from that. ## What you use it for Method values are Go's answer to "pass this object's behaviour somewhere that only understands functions": ```go timer := time.AfterFunc(2*time.Second, runner.Warmup) mix := map[string]func(){ "warmup": runner.Warmup, "burst": runner.Burst, } ``` A registry of named workload functions built this way is idiomatic: the map's value type is a plain `func()`, and each entry already knows which `Runner` it belongs to. When you are teaching the construct to a team coming from another language, the sentence that lands is "the receiver rides along inside the function value". ## Traps worth naming 1. **Eager binding.** `f := runner.Warmup` on a value-receiver method freezes the receiver. If the struct's fields change later, `f` is working from history. 2. **Addressability.** A pointer-receiver method value requires an addressable operand, so map elements and call results are rejected at compile time. 3. **A nil pointer and a value receiver.** If `p` is a nil `*Runner` and `Warmup` has a **value** receiver, `p.Warmup` means `(*p).Warmup`, and the dereference happens when the method value is created — the panic fires at the binding site, not at the call. With a pointer receiver, the nil pointer is simply saved and the call panics only if the body dereferences it. 4. **`f` is not `f()`.** Writing `go runner.Warmup` is a compile error (a `go` statement needs a call); `go runner.Warmup()` is what you meant. Conversely `time.AfterFunc(d, runner.Warmup())` calls the method immediately and passes its result. ## The other form A method value is not the same thing as a *method expression*. `Runner.Warmup` — the **type** name rather than a variable on the left — binds nothing and yields a function that takes the receiver as an ordinary first parameter. The method value is the bound form; the method expression is the unbound form.

  • Does the method value have to be stored in a variable before the receiver is bound?
    No. Binding happens wherever the expression `runner.Warmup` is evaluated, including as an argument. `time.AfterFunc(d, runner.Warmup)` binds the receiver at that call site, then the timer fires much later with the receiver that was saved then.
  • Can you take a method value of a pointer-receiver method from a map element?
    No. `workloads["burst"].Warmup` does not compile when `Warmup` has a pointer receiver, because Go must save `&workloads["burst"]` and a map element is not addressable. Copy the element into a local variable and take the method value from that variable, remembering that you are now bound to the copy.
  • What is the type of `runner.Warmup` if the method is `func (r *Runner) Warmup(n int) error`?
    `func(int) error`. The receiver is not a parameter of the method value — it has already been supplied — so only the declared parameters and results remain. That is exactly why the value slots into an API expecting a plain function type.

saying these in an interview costs you the question

  • Says the receiver is looked up when the function is finally called
  • Thinks runner.Warmup and runner.Warmup() are the same expression
  • Claims the method value still takes the receiver as its first parameter
  • Assumes a value-receiver method value tracks later changes to the variable
  • Believes any expression can supply the receiver, ignoring addressability