Method Values and Method Expressions — Tasks¶
Exercise structure¶
- 🟢 Easy — for beginners
- 🟡 Medium — middle level
- 🔴 Hard — senior level
- 🟣 Expert — professional level
A solution for each exercise is provided at the end.
Easy 🟢¶
Task 1 — Bind a method to a variable¶
Write a Greeter struct with a Hi() method. Create a method value fn bound to a Greeter instance and call it.
type Greeter struct{ name string }
func (g Greeter) Hi() string { return "Hi, " + g.name }
// Bind to fn and call.
Task 2 — Method expression call¶
For the same Greeter, take the method expression Greeter.Hi and call it with an explicit receiver.
Task 3 — Type of method value¶
Given func (t T) M(x int) int, write down the types of t.M, T.M, and (*T).M.
Task 4 — Currying via method value¶
Define Adder{base int} with Add(x int) int. Make addFive by binding Adder{5}.Add. Verify that addFive(10) == 15.
Task 5 — Pointer receiver method value¶
Define Counter{n int} with Inc() (pointer receiver). Bind it to inc and call inc() three times. Print c.n.
Medium 🟡¶
Task 6 — http.HandleFunc with method value¶
Build a Server struct with two HTTP handlers Users and Orders. Register them with http.HandleFunc using method values.
Task 7 — Dispatch table with method expressions¶
Define Calc with Add, Sub, Mul, Div methods. Build map[string]func(Calc, int, int) int as a dispatch table. Implement Apply(op string, a, b int) int.
Task 8 — sort.Slice with bound method¶
Write a People slice of Person{Name, Age}. Provide method values lessByName and lessByAge and use them with sort.Slice.
Task 9 — Pre-Go 1.22 loop gotcha¶
Write code that demonstrates the receiver-capture issue when binding method values inside a loop, then fix it for both Go 1.22+ and earlier.
Task 10 — Convert method expression into bound function¶
Given T.M, write a helper that returns a closure equivalent to t.M:
Task 11 — Detect heap escape¶
Write a small program that creates a method value, store it in a slice, and verify with go build -gcflags='-m' that the receiver escapes.
Task 12 — (*T).M indirection¶
For a value-receiver method func (t T) M() int, demonstrate that calling (*T).M(&t) does not mutate t even when M is later modified to write to a copy of the receiver.
Hard 🔴¶
Task 13 — Plugin registry¶
Build a Plugin type with methods Hello, Status, Reload. Build a registry map[string]func(Plugin). Add a function Invoke(p Plugin, op string) that dispatches via method expression.
Task 14 — Multi-receiver dispatch¶
Define types English, French each with method Greet() string. Build a single dispatch map keyed by language code that returns the appropriate greeting using method expressions:
(Hint: each entry in the map can have a different signature only if you design carefully — you may need to wrap.)
Task 15 — sort.Slice with multi-key comparison¶
Sort a slice of Person by Age ascending, then by Name ascending. Use method values for the comparator chain.
Task 16 — Method value benchmark¶
Write a benchmark that compares calling a method directly vs through a method value 1,000,000 times. Run with go test -bench=. -benchmem and record allocation differences.
Task 17 — Goroutine-safe method-value broadcast¶
Build an EventBus where you can register handler method values via Bus.Subscribe(handler.OnEvent). The bus calls all subscribers concurrently when an event fires.
Task 18 — Interface method value¶
Take an io.Writer interface value, bind its Write method, and call it through the bound function. Verify that re-assigning the original interface variable does not affect the bound method.
Expert 🟣¶
Task 19 — Reflection: method value via reflect¶
Given an arbitrary value v, look up its method named "Run" using reflect.Value.Method and invoke it. Compare with reflect.Type.Method which exposes the method expression form.
Task 20 — Generic dispatch table¶
Build a generic Dispatcher[T any] that holds a map of method expressions of type func(T, ...any) any. Demonstrate it with two different generic types.
Task 21 — State machine via method expression table¶
Implement a state machine where each state is a method expression. The machine holds a current method value and Step(input) calls it, replacing itself based on the result.
Task 22 — Avoid the heap-escape trap¶
Refactor an http.Server registration that is allocating per request because of repeated method-value bindings. Compare allocations with -benchmem.
Solutions¶
Solution 1¶
type Greeter struct{ name string }
func (g Greeter) Hi() string { return "Hi, " + g.name }
g := Greeter{name: "Ada"}
fn := g.Hi
fmt.Println(fn()) // Hi, Ada
Solution 2¶
expr := Greeter.Hi
fmt.Println(expr(Greeter{name: "Ada"})) // Hi, Ada
// Type of expr: func(Greeter) string
Solution 3¶
Solution 4¶
type Adder struct{ base int }
func (a Adder) Add(x int) int { return a.base + x }
addFive := Adder{base: 5}.Add
fmt.Println(addFive(10)) // 15
Solution 5¶
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
c := &Counter{}
inc := c.Inc // captures &(*c)
inc(); inc(); inc()
fmt.Println(c.n) // 3
Solution 6¶
type Server struct{ db *sql.DB }
func (s *Server) Users(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "users")
}
func (s *Server) Orders(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "orders")
}
s := &Server{db: db}
mux := http.NewServeMux()
mux.HandleFunc("/users", s.Users)
mux.HandleFunc("/orders", s.Orders)
Solution 7¶
type Calc struct{}
func (Calc) Add(a, b int) int { return a + b }
func (Calc) Sub(a, b int) int { return a - b }
func (Calc) Mul(a, b int) int { return a * b }
func (Calc) Div(a, b int) int { return a / b }
var ops = map[string]func(Calc, int, int) int{
"+": Calc.Add,
"-": Calc.Sub,
"*": Calc.Mul,
"/": Calc.Div,
}
func Apply(op string, a, b int) int {
return ops[op](Calc{}, a, b)
}
Solution 8¶
type Person struct {
Name string
Age int
}
type People []Person
func (p People) lessByName(i, j int) bool { return p[i].Name < p[j].Name }
func (p People) lessByAge(i, j int) bool { return p[i].Age < p[j].Age }
people := People{
{"Ada", 30},
{"Bob", 25},
{"Carol", 28},
}
sort.Slice(people, people.lessByAge) // by age
sort.Slice(people, people.lessByName) // by name
Solution 9¶
// Pre-Go 1.22 BUG:
services := []*Service{{name: "a"}, {name: "b"}, {name: "c"}}
var fns []func()
for _, s := range services {
fns = append(fns, s.Run) // OK in 1.22+, but in 1.21 captures shared s
}
// Pre-1.22 fix:
for _, s := range services {
s := s // shadow
fns = append(fns, s.Run)
}
// Go 1.22+ — no fix needed: s is per-iteration.
Solution 10¶
func Bind[T, A1, R any](m func(T, A1) R, t T) func(A1) R {
return func(a A1) R { return m(t, a) }
}
// Usage:
addFive := Bind(Adder.Add, Adder{base: 5})
fmt.Println(addFive(10))
Solution 11¶
type Big struct{ buf [1 << 16]byte }
func (b Big) Run() {}
func main() {
b := Big{}
var fns []func()
fns = append(fns, b.Run) // b escapes to heap
fns[0]()
}
// $ go build -gcflags='-m' .
// ./main.go:7:6: moved to heap: b
// ./main.go:9:21: b.Run escapes to heap
Solution 12¶
type T struct{ n int }
func (t T) M() int { return t.n }
t := T{n: 1}
me := (*T).M // type: func(*T) int
fmt.Println(me(&t)) // 1
fmt.Println(t.n) // 1 — unchanged; copy was passed
Solution 13¶
type Plugin struct{ id string }
func (p Plugin) Hello() { fmt.Println("hello from", p.id) }
func (p Plugin) Status() { fmt.Println("status of", p.id) }
func (p Plugin) Reload() { fmt.Println("reloading", p.id) }
var registry = map[string]func(Plugin){
"hello": Plugin.Hello,
"status": Plugin.Status,
"reload": Plugin.Reload,
}
func Invoke(p Plugin, op string) {
if fn, ok := registry[op]; ok {
fn(p)
return
}
fmt.Println("unknown op:", op)
}
Solution 14¶
type English struct{}
func (English) Greet() string { return "Hello" }
type French struct{}
func (French) Greet() string { return "Bonjour" }
// Different receiver types -> wrap into a uniform signature:
var greet = map[string]func() string{
"en": func() string { return English{}.Greet() },
"fr": func() string { return French{}.Greet() },
}
fmt.Println(greet["en"]())
fmt.Println(greet["fr"]())
// Alternative: define an interface and use method expressions on the interface.
Solution 15¶
type Person struct {
Name string
Age int
}
type People []Person
func (p People) lessByAge(i, j int) bool { return p[i].Age < p[j].Age }
func (p People) lessByName(i, j int) bool { return p[i].Name < p[j].Name }
func multiSort(p People, less ...func(int, int) bool) {
sort.SliceStable(p, func(i, j int) bool {
for _, l := range less {
if l(i, j) { return true }
if l(j, i) { return false }
}
return false
})
}
people := People{...}
multiSort(people, people.lessByAge, people.lessByName)
Solution 16¶
type T struct{ n int }
func (t T) Inc() int { return t.n + 1 }
var sink int
func BenchmarkDirect(b *testing.B) {
t := T{n: 1}
for i := 0; i < b.N; i++ { sink = t.Inc() }
}
func BenchmarkMethodValue(b *testing.B) {
t := T{n: 1}
fn := t.Inc
for i := 0; i < b.N; i++ { sink = fn() }
}
// $ go test -bench=. -benchmem
// BenchmarkDirect 1000000000 0.30 ns/op 0 B/op 0 allocs/op
// BenchmarkMethodValue 1000000000 0.30 ns/op 0 B/op 0 allocs/op
//
// (Within the loop the closure does not escape; binding outside the loop
// is the cheap path. Bind inside the loop and you'll see allocations.)
Solution 17¶
type EventBus struct {
mu sync.Mutex
subs []func(Event)
}
func (b *EventBus) Subscribe(fn func(Event)) {
b.mu.Lock(); defer b.mu.Unlock()
b.subs = append(b.subs, fn)
}
func (b *EventBus) Publish(e Event) {
b.mu.Lock()
subs := append([]func(Event){}, b.subs...)
b.mu.Unlock()
var wg sync.WaitGroup
for _, fn := range subs {
wg.Add(1)
go func(f func(Event)) { defer wg.Done(); f(e) }(fn)
}
wg.Wait()
}
// Usage:
type Listener struct{ id string }
func (l Listener) OnEvent(e Event) { fmt.Println(l.id, e) }
bus := &EventBus{}
bus.Subscribe(Listener{"a"}.OnEvent)
bus.Subscribe(Listener{"b"}.OnEvent)
bus.Publish(Event{...})
Solution 18¶
var w io.Writer = os.Stdout
write := w.Write // captures (type, *os.File) interface value
w = nil // does not affect captured method value
write([]byte("hello\n")) // OK; still writes to Stdout
Solution 19¶
type Service struct{ id string }
func (s Service) Run() { fmt.Println("run", s.id) }
s := Service{id: "x"}
// Method value via reflect
v := reflect.ValueOf(s)
mv := v.MethodByName("Run")
mv.Call(nil) // prints: run x
// Method expression via reflect
t := reflect.TypeOf(s)
me, _ := t.MethodByName("Run")
me.Func.Call([]reflect.Value{v}) // pass receiver explicitly
Solution 20¶
type Dispatcher[T any] struct {
ops map[string]func(T)
}
func NewDispatcher[T any]() *Dispatcher[T] {
return &Dispatcher[T]{ops: make(map[string]func(T))}
}
func (d *Dispatcher[T]) Register(name string, fn func(T)) {
d.ops[name] = fn
}
func (d *Dispatcher[T]) Invoke(t T, name string) {
if fn, ok := d.ops[name]; ok { fn(t) }
}
// Usage 1
type A struct{}
func (A) Foo() { fmt.Println("foo") }
dA := NewDispatcher[A]()
dA.Register("foo", A.Foo)
dA.Invoke(A{}, "foo")
// Usage 2
type B struct{ id string }
func (b B) Bar() { fmt.Println("bar", b.id) }
dB := NewDispatcher[B]()
dB.Register("bar", B.Bar)
dB.Invoke(B{id: "1"}, "bar")
Solution 21¶
type Machine struct {
state func(string) func(string) any
}
func startState(in string) func(string) any { ... }
func runningState(in string) func(string) any { ... }
// Step holds current state and transitions
type SM struct {
cur func(string) any
}
func (m *SM) Step(in string) {
next := m.cur(in)
// expecting next to be itself a method-value-like closure (state func)
if fn, ok := next.(func(string) any); ok {
m.cur = fn
}
}
// Concrete pattern: bind methods of a State struct as method values and store
// them in a struct field representing the current state.
type State struct{}
func (s State) Idle(in string) any { return State.Active }
func (s State) Active(in string) any { return State.Closed }
func (s State) Closed(in string) any { return State.Closed }
Solution 22¶
// BAD: per-request method-value binding (allocates each request).
func registerBad(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/x", func(w http.ResponseWriter, r *http.Request) {
s := &Server{db: db} // allocates each request
s.Handle(w, r)
})
}
// GOOD: bind once.
func registerGood(mux *http.ServeMux, s *Server) {
mux.HandleFunc("/x", s.Handle) // single bound method value
}
// Benchmark with go test -benchmem to see allocations vanish in the
// "good" version.
Cheat Sheet¶
METHOD VALUE
t.M func(args) receiver bound (snapshot at binding time)
METHOD EXPRESSION
T.M func(T, args) value-receiver method
(*T).M func(*T, args) pointer-receiver method (or value, dereffed)
WHEN TO USE
Method value callbacks, http.Handler, sort.Slice, channel sends
Method expression dispatch tables, plugin registries, generic adapters
PITFALLS
- Map elements not addressable -> no method value with pointer receiver
- Receiver captured at binding moment, not call moment
- Pre-Go 1.22 loops share the loop variable -> shadow `s := s`
- Method values allocate when escaping the closure scope
- Function values not == comparable