11.1 Defining Methods: func (r Receiver) Name()
Alright, let’s get our hands dirty with methods. Forget the dry theory for a second. At its core, a method in Go is just a function with a special guest star in its parameter list: the receiver. It’s how we attach functionality to our types. The syntax is the first thing that trips people up, so let’s break it down.
You define a method like this:
func (r Receiver) Name(parameters) returnType {
// your brilliant code here
}
That (r Receiver) bit before the function name is the receiver. It’s the anchor that ties this function to a specific type. When you call myObject.Name(), it’s essentially passing myObject as that r parameter under the hood. It’s syntactic sugar, but it’s the good kind that makes your code readable and organized.
Now, the big question: what do you put for Receiver? This is where the pointer vs. value distinction rears its head, and it’s the single most important decision you’ll make for most methods. Get it wrong, and you’ll have a bad time.
The Value Receiver: func (v MyType) Method()
When you use a value receiver, you’re working on a copy of the original value. It’s like getting a photocopy of a document; you can scribble all over the copy without affecting the original.
type Counter struct {
count int
}
// Value receiver method
func (c Counter) Increment() {
c.count++ // This only increments the copy!
fmt.Printf("Inside Increment (value): c's address: %p, value: %d\n", &c, c.count)
}
func main() {
myCounter := Counter{count: 5}
fmt.Printf("Before Increment: address: %p, value: %d\n", &myCounter, myCounter.count)
myCounter.Increment() // This gets a copy of myCounter
fmt.Printf("After Increment: value: %d\n", myCounter.count) // Spoiler: still 5
}
Run this. See that the addresses are different? The Increment method received a full copy of the myCounter struct. Changing the copy is useless. This is almost always a bug. The only time you’d use a value receiver is if your method is a pure function that doesn’t modify the underlying struct, like a String() or Distance() method, and the struct is very small (like, an int small). For any method that needs to mutate state, this is the wrong tool for the job.
The Pointer Receiver: func (p *MyType) Method()
This is what you’ll use 95% of the time. A pointer receiver doesn’t get a copy of the value; it gets a copy of the address of the value. It’s like being given the exact map location of the original document. Any changes you make are made on the original.
type Counter struct {
count int
}
// Pointer receiver method - This is what we want.
func (c *Counter) ActuallyIncrement() {
c.count++ // This works!
fmt.Printf("Inside ActuallyIncrement (ptr): c's address: %p, value: %d\n", c, c.count)
}
func main() {
myCounter := Counter{count: 5}
fmt.Printf("Before ActuallyIncrement: address: %p, value: %d\n", &myCounter, myCounter.count)
myCounter.ActuallyIncrement() // Go is smart enough to pass the address for you here.
fmt.Printf("After ActuallyIncrement: value: %d\n", myCounter.count) // Now it's 6!
}
Notice the magic? We called myCounter.ActuallyIncrement(), not (&myCounter).ActuallyIncrement(). This is Go’s helpful “dereferencing” sugar. If a method has a pointer receiver, Go will automatically take the address of the value for you. This is fantastic because it means your code’s usage site remains clean and consistent. You don’t have to remember if a method needs & or not. The rule is simple: if you need to modify the receiver, use a pointer receiver. Always.
Method Sets: The Rulebook
This is where it gets a bit theoretical, but stick with me—it explains why the above sugar works and defines what you can and can’t do. The method set of a type is the set of all methods that can be called on a value of that type.
- The method set of
T(a value type) consists of all methods declared with value receivers. - The method set of
*T(a pointer type) consists of all methods declared with both value AND pointer receivers.
Let that sink in. It’s a one-way street. A value T can only use methods with value receivers. A pointer *T can use all methods, value or pointer. This is why we could call ActuallyIncrement (pointer receiver) on myCounter (a value). The method set of *Counter includes all methods, so Go happily provided the automatic dereferencing.
This has huge implications for interfaces. If you define an interface based on methods with pointer receivers, only pointers to your type will satisfy it. This is a very common “gotcha”.
type Incrementer interface {
ActuallyIncrement()
}
// This works:
var inc1 Incrementer = &Counter{}
// This DOES NOT COMPILE:
// var inc2 Incrementer = Counter{}
// Counter does not implement Incrementer (ActuallyIncrement method has pointer receiver)
The error message is confusing until you learn the method set rule. Then it makes perfect sense. A value Counter doesn’t have the ActuallyIncrement method in its method set; only *Counter does. So the lesson is: be consistent. If you’re defining an interface for a type, use pointer receivers for all its methods to avoid this confusion.
The Nil Receiver Question
Here’s a fun edge case. What happens if you call a method on a nil pointer?
func (c *Counter) WhatHappensNow() {
if c == nil {
fmt.Println("Receiver is nil!")
return
}
fmt.Println("Receiver value:", c.count)
}
func main() {
var nilCounter *Counter
nilCounter.WhatHappensNow() // This is actually valid Go.
}
Yep, it runs. Methods are just functions, and the receiver is just a parameter. You can call a function with a nil parameter; you just have to handle it inside the method. This can be useful for representing empty states, but you must check for nil to avoid a panic if you try to dereference c.count.