DEV Community

Cover image for Method Values vs Method Expressions in Go: A Distinction Worth Knowing
Gabriel Anhaia
Gabriel Anhaia

Posted on

Method Values vs Method Expressions in Go: A Distinction Worth Knowing


You are wiring an HTTP router. You have a Server struct with a
handler method, and you want to register it. So you write
mux.HandleFunc("/users", srv.ListUsers) and it works. You never
stop to ask what srv.ListUsers actually is as an expression.

It is a method value. Go took the method, bound it to srv, and
handed you back a plain func(http.ResponseWriter, *http.Request).
That binding is the thing doing the quiet work in half the handler
wiring you write.

There is a second form the Go spec gives you, and most Go developers
never type it on purpose: the method expression, Server.ListUsers,
which produces a function that takes the receiver as its first
argument. Same method, different shape, different use.

These two live in the Go spec
and they are worth telling apart, because the compiler will happily
let you reach for the wrong one.

The two forms, side by side

Start with a small type and one method.

type Greeter struct {
    prefix string
}

func (g Greeter) Say(name string) string {
    return g.prefix + " " + name
}
Enter fullscreen mode Exit fullscreen mode

A method value binds the receiver now:

g := Greeter{prefix: "hello"}
say := g.Say          // method value, receiver g is captured
fmt.Println(say("ana")) // "hello ana"
Enter fullscreen mode Exit fullscreen mode

say has type func(string) string. The receiver is gone from the
signature because Go already stored g inside the closure. Call it
later, from anywhere, and it still remembers g.

A method expression leaves the receiver open:

say := Greeter.Say    // method expression, no receiver yet
fmt.Println(say(g, "ana")) // "hello ana"
Enter fullscreen mode Exit fullscreen mode

say here has type func(Greeter, string) string. The receiver
became the first parameter. You supply it at the call site, every
time.

Same method name. g.Say reads the value g; Greeter.Say reads
the type Greeter. That single distinction is the whole topic.

Method values are how callbacks stay clean

The reason srv.ListUsers works as a router handler is that a method
value is already the right function type. No wrapper closure, no
func(w, r) { srv.ListUsers(w, r) } boilerplate.

type Server struct {
    db *sql.DB
}

func (s *Server) ListUsers(w http.ResponseWriter,
    r *http.Request) {
    // ... s.db is available, receiver is bound
}

func (s *Server) routes() *http.ServeMux {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users", s.ListUsers)
    mux.HandleFunc("POST /users", s.CreateUser)
    return mux
}
Enter fullscreen mode Exit fullscreen mode

Each s.ListUsers is a method value. It carries s with it, so the
handler has s.db when the request lands, without the router knowing
anything about your Server. The router only sees a
http.HandlerFunc.

The same shape shows up anywhere the stdlib wants a func:

// graceful shutdown hook
srv := &http.Server{Addr: ":8080", Handler: s.routes()}
// register the bound Close as a cleanup callback
context.AfterFunc(ctx, func() { srv.Shutdown(ctx) })

// a time.AfterFunc that pokes a bound method
t := time.AfterFunc(5*time.Second, s.flushMetrics)
defer t.Stop()
Enter fullscreen mode Exit fullscreen mode

s.flushMetrics is a bound method value with signature func(),
which is exactly what time.AfterFunc asks for. The binding is
invisible and that is the point.

The pointer-receiver gotcha behind bound values

There is one trap worth naming. When you take a method value off a
pointer receiver, Go binds the pointer, so later changes are seen.
When the method has a value receiver, Go copies the receiver at the
moment you take the value.

type Counter struct{ n int }

func (c Counter) Read() int { return c.n } // value receiver

func main() {
    c := Counter{n: 1}
    read := c.Read   // copies c NOW, with n == 1
    c.n = 99
    fmt.Println(read()) // 1, not 99
}
Enter fullscreen mode Exit fullscreen mode

read closed over a snapshot taken at binding time. If Read had a
pointer receiver (func (c *Counter) Read()), the method value would
bind &c, and the call would print 99. This is the same value-vs-
pointer receiver rule you already know, it just fires at the moment
the method value is formed rather than at the call. Worth a second
look when a callback reads stale data.

Method expressions turn methods into data

Method expressions earn their keep when you want to treat "which
method" as a value you pass around, and you want to hand the receiver
in later per call. The receiver is a parameter, so one function value
serves many receivers.

A table of operations is the clearest case:

type Account struct{ balance int }

func (a *Account) Deposit(amount int)  { a.balance += amount }
func (a *Account) Withdraw(amount int) { a.balance -= amount }

// op is func(*Account, int) — receiver is the first arg
var ops = map[string]func(*Account, int){
    "deposit":  (*Account).Deposit,
    "withdraw": (*Account).Withdraw,
}

func apply(a *Account, cmd string, amount int) {
    if op, ok := ops[cmd]; ok {
        op(a, amount) // receiver supplied at call time
    }
}
Enter fullscreen mode Exit fullscreen mode

(*Account).Deposit is a method expression on the pointer receiver.
Its type is func(*Account, int). The map stores methods as values
without binding them to any particular account, and apply picks the
account per call. You could not build this table with method values,
because a method value needs a concrete receiver at the moment you
write it.

The parentheses matter: (*Account).Deposit selects the method set
of *Account. Writing Account.Deposit would ask for the value
receiver's method set, which does not contain the pointer methods, so
the compiler rejects it.

Where the stdlib already uses this thinking

An API accepts a method expression when its callback signature lines
up with the method's — receiver first, then the rest. slices.SortFunc
wants a func(a, b E) int, so a func (p Person) Compare(o Person) int
method drops in as Person.Compare with no wrapper. The everyday case
still writes the closure by hand:

type Person struct{ Name string }

// method expression flavor: the comparator IS the method,
// receiver-free, taking the two operands as parameters
people := []Person{{Name: "Bo"}, {Name: "Al"}}
sort.Slice(people, func(i, j int) bool {
    return people[i].Name < people[j].Name
})
Enter fullscreen mode Exit fullscreen mode

Here you write the closure by hand, but the mental model is the same
as a method expression: a comparison expressed as a free function
over its operands rather than a method bound to a receiver. Give that
comparison a name as a method, and Person.Compare slots straight
into slices.SortFunc (Go 1.21+) in place of the closure.

Choosing between them

The decision is almost always about when the receiver is known.

  • You have the receiver now, and you want a plain callback the caller invokes with no knowledge of your type: use a method value (s.ListUsers). This is the common case in handler wiring, timers, and cleanup hooks.
  • You want to store or pass "which method" and supply different receivers later: use a method expression ((*Account).Deposit). This shows up in dispatch tables, generic helpers, and test matrices that run the same operation across many instances.

If you catch yourself writing a wrapper closure that only forwards
its arguments to a method, one of these two forms almost certainly
deletes the wrapper. func(w, r) { s.h(w, r) } is just s.h. And a
map of func(x) { obj.M(x) } closures over a fixed set of methods is
often a map of method expressions with the receiver passed in.

The one-line summary

instance.Method is the callback you register. Type.Method is the
method you store as data. Reach for the shape that matches when the
receiver becomes known, and the wiring reads cleaner.

Method values and expressions are the kind of small Go feature that
sits under a lot of idiomatic code without a name attached. The
Complete Guide to Go Programming
digs into the receiver rules, the
binding semantics, and the method-set details that decide which form
the compiler accepts. Hexagonal Architecture in Go is where this
lands in practice, keeping handler wiring and dispatch at the right
boundary instead of leaking receivers across your ports and adapters.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Top comments (0)