🔧 Analogy
A method is a function that already knows which thing it acts on. Instead of area(rect), you write rect.Area() — the rectangle is handed in silently as the receiver. Same machine as a function, but bolted onto a type so the data and its behavior travel together. The receiver isn’t magic; it’s just the first argument, written before the name instead of inside the parentheses.
A method is a function with a receiver
The only syntactic difference between a function and a method is the receiver — the parameter in parentheses before the name. That parameter binds the method to a type, so it becomes callable with dot syntax:
type User struct{ Name string }
// (u User) is the receiver — User.Greet attaches to the type
func (u User) Greet() string {
return "Hi, " + u.Name
}
u.Greet() is really shorthand: the compiler passes u in as the receiver argument. There is no per-object function pointer and no hidden vtable on a struct — a method is one function compiled once, and the receiver is passed like any other parameter. (Dynamic dispatch only appears when you call through an interface, not when you call a method on a concrete value.)
A receiver must be a named type defined in the same package. You can’t add methods to int, to []string, or to a type from another package directly — but you can wrap it in a local named type and attach methods to that.
Methods on any named type
Methods aren’t limited to structs. They attach to any named type in your package — even one built on a primitive, a slice, or a string. This is how you give plain data a vocabulary:
package main
import (
"fmt"
"strings"
)
// A named type over a primitive.
type Celsius float64
func (c Celsius) Fahrenheit() float64 { return float64(c)*9/5 + 32 }
func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(c)) }
// A named type over a slice — its method can range over itself.
type IntSlice []int
func (s IntSlice) Sum() int {
total := 0
for _, n := range s {
total += n
}
return total
}
// A named type over a string.
type Loud string
func (l Loud) Shout() string { return strings.ToUpper(string(l)) + "!" }
func main() {
boiling := Celsius(100)
fmt.Printf("%s = %.1f°F\\n", boiling, boiling.Fahrenheit()) // 100.0°C = 212.0°F
nums := IntSlice{1, 2, 3, 4}
fmt.Println("sum:", nums.Sum()) // sum: 10
fmt.Println(Loud("hi there").Shout()) // HI THERE!
}
This pattern is everywhere in the standard library: time.Duration is a named int64 with String() and arithmetic helpers, and sort.Interface is satisfied by named slice types. Giving the underlying value a name and methods turns a bag of bits into a small domain type.
Value vs pointer receivers
This is the decision you make on every method. A value receiver gets a copy of the receiver, so it can read but its writes never reach the original. A pointer receiver gets the address, so it mutates in place — and it avoids copying a large struct on every call.
graph TD CALL["c.Method()"] --> VR["value receiver (c T)"] CALL --> PR["pointer receiver (c *T)"] VR --> COPY["gets a COPY — mutations are lost, struct copied each call"] PR --> ORIG["gets the ADDRESS — mutations stick, no copy"]
package main
import "fmt"
type Counter struct {
count int
}
// Value receiver: operates on a COPY. The caller's Counter is untouched.
func (c Counter) IncValue() {
c.count++ // writes to the copy, then the copy is discarded
}
// Pointer receiver: operates on the ORIGINAL via its address.
func (c *Counter) IncPointer() {
c.count++
}
// Value receiver is fine for a read-only method.
func (c Counter) String() string {
return fmt.Sprintf("count=%d", c.count)
}
func main() {
c := Counter{}
c.IncValue()
c.IncValue()
fmt.Println("after 2x IncValue: ", c.String()) // count=0 — copies thrown away
c.IncPointer() // Go rewrites this to (&c).IncPointer()
c.IncPointer()
c.IncPointer()
fmt.Println("after 3x IncPointer:", c.String()) // count=3 — real mutation
// A pointer variable works the same way; Go dereferences for the value method.
p := &Counter{count: 10}
p.IncPointer()
fmt.Println("through a pointer: ", p.String()) // count=11
}
Automatic addressing
You may have noticed c.IncPointer() works on a plain Counter variable even though the method needs *Counter. Go inserts the & for you when the value is addressable — a variable, a struct field, an array element. The reverse also happens: calling a value-receiver method through a pointer auto-dereferences (p.String() becomes (*p).String()). This convenience hides the receiver distinction in everyday code, which is exactly why it surprises people the first time an interface refuses a value (see below). A value that isn’t addressable — a map element, the result of a function call, a literal — can’t have its address taken, so a pointer method on it won’t compile.
Choosing — and being consistent
| Situation | Receiver | Why |
|---|---|---|
| Method mutates the receiver | pointer | writes must reach the original |
| Large struct, called often | pointer | avoid copying it every call |
Type contains a sync.Mutex, slice header you grow, etc. | pointer | copying would split state / duplicate the lock |
Small, value-like type (Celsius, a Point) | value | cheap to copy, reads like a number |
| Some methods already need a pointer | pointer for all | one consistent method set per type |
The overriding rule: pick one receiver kind per type and stay consistent. Mixing them makes the method set of T differ awkwardly from *T and invites the interface trap below. When in doubt, especially for structs, default to pointer receivers.
Method sets and interface satisfaction
Each type has a method set — the methods you may call on a value of that type for interface purposes. This set is what decides whether a type satisfies an interface:
- The method set of
Tcontains only its value-receiver methods. - The method set of
*Tcontains both value- and pointer-receiver methods.
So if M has a pointer receiver, only *T satisfies an interface requiring M — a plain T value does not. Automatic addressing does not rescue you here: storing a value in an interface makes a copy that isn’t addressable, so the compiler can’t synthesize the pointer.
graph TD T["method set of T"] --> VM["value-receiver methods only"] PT["method set of *T"] --> VM2["value-receiver methods"] PT --> PM["+ pointer-receiver methods"] VM --> IFACE["satisfies interface needing those methods"] PM --> IFACE2["only *T satisfies interfaces needing pointer methods"]
type Stringer interface{ String() string }
type Big struct{ /* ... */ }
func (b *Big) String() string { return "big" } // POINTER receiver
var s Stringer = &Big{} // OK — *Big is in the method set
// var s Stringer = Big{} // compile error: Big does not implement Stringer
// // (String has pointer receiver)
This is the single most common “why won’t this compile?” with interfaces. The fix is almost always to pass *T and to keep receivers consistent. See interfaces for the dispatch side of this rule, and pointers for addressability.
Method values and method expressions
A method can be pulled out as a first-class function value in two distinct ways:
- A method value
v.Mis bound to a specific receiver. Taking it captures the receiver right then; the result is an ordinaryfunc(...)you can store or pass as a callback. - A method expression
T.Mis unbound. The receiver becomes the first explicit parameter, so its type isfunc(T, ...).
package main
import "fmt"
type Greeter struct{ Name string }
func (g Greeter) Greet(greeting string) string {
return greeting + ", " + g.Name
}
func main() {
g := Greeter{Name: "Ada"}
// METHOD VALUE: bound to g. Its type is func(string) string.
// It captures the receiver now, so later changes to g are not seen.
bound := g.Greet
g.Name = "Grace" // changed AFTER the method value was taken
fmt.Println(bound("Hello")) // Hello, Ada — the bound copy still says Ada
// A method value is an ordinary func — pass it as a callback.
apply := func(fn func(string) string) string { return fn("Hi") }
fmt.Println(apply(g.Greet)) // Hi, Grace — g.Greet taken fresh here
// METHOD EXPRESSION: unbound. Its type is func(Greeter, string) string;
// the receiver becomes the first explicit argument.
expr := Greeter.Greet
fmt.Println(expr(Greeter{Name: "Linus"}, "Hey")) // Hey, Linus
}
The capture detail matters: a method value with a value receiver snapshots the receiver, so later mutations are invisible to it (above, bound still greets “Ada”). With a pointer receiver, the method value captures the pointer, so it sees later mutations. Method values are the idiomatic way to hand a bound behavior to http.HandleFunc, sort.Slice, or any callback-taking API.
Nil receivers
The receiver is just an argument, so a method can be called on a nil pointer — it only panics if the body actually dereferences nil. Methods that check for nil first turn the nil pointer into a useful base case (the empty list, the empty tree):
package main
import "fmt"
// A linked list whose methods are safe to call on a nil *List.
type List struct {
val int
next *List
}
// nil receiver: Len treats a nil *List as the empty list, never dereferencing nil.
func (l *List) Len() int {
if l == nil {
return 0
}
return 1 + l.next.Len() // l.next may be nil — the recursion handles it
}
// Sum likewise tolerates a nil receiver.
func (l *List) Sum() int {
if l == nil {
return 0
}
return l.val + l.next.Sum()
}
func main() {
var empty *List // nil pointer
fmt.Println("len(nil):", empty.Len()) // 0 — method runs on a nil receiver
fmt.Println("sum(nil):", empty.Sum()) // 0
list := &List{val: 1, next: &List{val: 2, next: &List{val: 3}}}
fmt.Println("len:", list.Len()) // 3
fmt.Println("sum:", list.Sum()) // 6
}
When to reach for a method (vs a plain function)
- Use a method when the behavior is intrinsic to the type and you want it to satisfy an interface, travel with the value, and read as
x.Do(). Methods are how a type advertises what it is. - Use a plain function when the operation spans several types equally, or doesn’t belong to any one type’s identity. Don’t force a method just to use dot syntax.
- Methods enable interfaces. A function can’t satisfy an interface; only a type’s method set can. If you need polymorphism, the behavior must be a method.
🐹 Mixing receiver kinds bites at interface time
Code like c.IncPointer() works on a normal variable because Go auto-takes its address. But the moment you store a value T in an interface that needs a pointer-receiver method, satisfaction fails — the copy held by the interface isn’t addressable, so Go can’t synthesize the &. The standard advice: be consistent. If any method needs a pointer receiver, give them all pointer receivers and pass *T around. The error message — “X does not implement Y (method M has pointer receiver)” — is the tell.
See also
- Interfaces — method sets decide who satisfies, and dynamic dispatch through the (type, value) pair.
- Pointers — addressability, and why a pointer receiver can mutate.
- Structs — the most common method receivers.
- Embedding — how a type can borrow another type’s methods via promotion.
Next: how method sets let unrelated types share behavior — interfaces.
Related topics
Implicit satisfaction and structural typing, the (type,value) pair and dynamic dispatch, method sets, any and type switches, composition, stdlib interfaces, design, and the nil trap.
compositeStructsGrouping fields into one type — literals, the zero value, nesting and embedding, struct tags, comparability, and the empty struct.
basicsPointers& and *, value vs reference semantics, nil, when a function needs a pointer to mutate, and why Go has no pointer arithmetic.
Check your understanding
Score: 0 / 51. When should the receiver be a pointer rather than a value?
A value receiver gets a copy, so writes don't reach the original and a big struct is copied each call. Use a pointer receiver to mutate state or to skip the copy. Keep the receiver kind consistent across a type's methods.
2. A type T has a method `func (t *T) M()`. Which value's method set includes M?
Pointer-receiver methods belong to the method set of *T only. So a *T can satisfy an interface needing M, but a plain T value cannot — the usual cause of "does not implement" errors.
3. Can you attach a method to `type Celsius float64`?
Methods attach to named types, including ones built on primitives or slices. `func (c Celsius) Fahrenheit() float64` is valid. You cannot add methods to types from other packages, though — define a local named type first.
4. What is the difference between a method value `v.M` and a method expression `T.M`?
A method value captures the receiver now, producing a closure like func(string) string. A method expression leaves the receiver out, producing func(T, string) string where you pass the receiver explicitly.
5. Can a method run when its pointer receiver is nil?
The receiver is just an argument; passing nil is fine. The method only panics if it actually dereferences the nil pointer. Nil-tolerant methods power empty-list and tree base cases (e.g. (*List).Len returning 0 for nil).
Comments
Sign in with GitHub to join the discussion.