Methods on Structs
In Go, a method is just a function with a special extra argument called a receiver, which binds the function to a particular type. Instead of writing Area(rect), you write rect.Area(), and that small syntactic difference is how Go attaches behavior to the structs you define. Methods, together with structs and Go’s implicit interface satisfaction, are the building blocks that replace class hierarchies and inheritance from other languages. This lesson covers how methods work under the hood, when to use a value receiver versus a pointer receiver, and the mistakes almost everyone makes the first time.
Overview: How Methods Work
A method declaration looks almost exactly like a function declaration, except it has one extra piece between the func keyword and the method name: the receiver. The receiver lists a name and a type, and that type is what the method gets attached to. Once declared, the method becomes part of that type’s method set and can be called using dot notation on any value of that type.
Under the hood, a method is really just a regular function. The Go compiler treats func (r Rectangle) Area() float64 { ... } as a function that takes a Rectangle as its first, implicit argument; the call rect.Area() compiles down to something conceptually like Rectangle.Area(rect). This is why methods can only be declared on types defined in the same package as the method itself: you cannot add a method to a type from another package, including built-in types like int or string. If you need method-like behavior on a type you don’t own, define a new named type based on it, such as type Celsius float64, and add methods to that instead.
Value Receivers vs Pointer Receivers
The receiver’s type determines what the method can do to the original value:
- A value receiver, such as
func (r Rectangle) Area() float64, receives a full copy of the struct. Any changes made inside the method are made to that copy and are lost when the method returns. - A pointer receiver, such as
func (c *Counter) Increment(), receives the address of the original value. Changes made through the pointer are visible to the caller after the method returns.
Pointer receivers are also the right choice when the struct is large, since a value receiver copies the entire struct on every call. As a rule of thumb: if a method needs to mutate the receiver, or the struct is expensive to copy, use a pointer receiver. Otherwise a value receiver is simpler and perfectly idiomatic for small, read-only style types.
Method Sets and Interfaces
Every type has a method set: the set of methods that can be called on a value of that type. This matters most when a value needs to satisfy an interface. A value of type T only has the value-receiver methods of T in its method set. A pointer of type *T has both the value-receiver and pointer-receiver methods of T. In practice this means that if a type has even one pointer-receiver method, you typically need a *T, not a plain T, to satisfy an interface that requires that method. This is a common source of confusing does not implement interface compiler errors for newcomers.
Go also helps you at the call site: if a value is addressable (a local variable, a struct field, an array or slice element), you can call a pointer-receiver method directly on it and Go automatically takes its address. This does not work on values that are not addressable, such as a value returned by a function call or, notably, a value stored directly in a map. That case is covered in Common Mistakes below.
Syntax
func (receiverName ReceiverType) MethodName(param1 Type1, param2 Type2) ReturnType {
// method body
}
| Part | Meaning |
|---|---|
receiverName |
A short local name for the receiver, conventionally one or two letters derived from the type (e.g. r for Rectangle), not this or self. |
ReceiverType |
Either T (value receiver) or *T (pointer receiver), where T is a type defined in the current package. |
MethodName |
The method’s name; combined with the receiver type, this defines the method’s identity. |
| parameters / return type | Identical to an ordinary function: any number of typed parameters and zero or more return values. |
Examples
Example 1: A Simple Value Receiver
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 4, Height: 5}
fmt.Println("Area:", rect.Area())
}
Output:
Area: 20
The Area method takes a value receiver, so r inside the method is a copy of rect. Since Area only reads the fields and never needs to modify the rectangle, a value receiver is the right, idiomatic choice here.
Example 2: Mutating State With a Pointer Receiver
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
c.Increment()
fmt.Println("Count:", c.count)
}
Output:
Count: 3
Here Increment uses a pointer receiver, so each call operates on the same underlying Counter rather than a copy. Notice that c is declared as a plain Counter value, not a pointer. Go automatically takes its address for you because c is addressable (it is a local variable), which is why c.Increment() compiles even though Increment expects a *Counter.
Example 3: A Realistic Type With Multiple Methods
package main
import (
"errors"
"fmt"
)
type Account struct {
Owner string
Balance float64
}
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func (a *Account) Withdraw(amount float64) error {
if amount > a.Balance {
return errors.New("insufficient funds")
}
a.Balance -= amount
return nil
}
func (a *Account) Summary() string {
return fmt.Sprintf("%s's balance: $%.2f", a.Owner, a.Balance)
}
func main() {
acc := Account{Owner: "Maria", Balance: 100}
acc.Deposit(50)
if err := acc.Withdraw(30); err != nil {
fmt.Println("Error:", err)
}
fmt.Println(acc.Summary())
if err := acc.Withdraw(1000); err != nil {
fmt.Println("Error:", err)
}
}
Output:
Maria's balance: $120.00
Error: insufficient funds
This example shows a more realistic pattern: a struct with several related methods, all using pointer receivers for consistency, plus explicit Go-style error handling instead of exceptions. Deposit adds 50 to the starting balance of 100, giving 150. Withdraw(30) succeeds, leaving 120, which Summary formats and prints. The final Withdraw(1000) fails because it exceeds the balance, so it returns an error instead of allowing the balance to go negative, and that error is printed instead of a panic.
How It Works Step by Step
When the compiler sees rect.Area(), it resolves the call in a few steps:
- It looks up the method set of
rect‘s type (Rectangle) for a method namedArea. - It checks whether the receiver type declared for
Areamatches, or can be reconciled with, the value’s type. If the method has a pointer receiver and the value is addressable, Go rewrites the call as(&rect).Area()automatically. - It passes
rect, or its address, as the hidden first argument and evaluates the method body exactly like an ordinary function call.
One consequence of this mechanical view is that a pointer-receiver method can be called safely on a nil pointer, as long as the method body does not dereference a nil receiver. This is different from most object-oriented languages, where calling a method on a null reference immediately crashes.
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func (n *Node) Describe() string {
if n == nil {
return "empty node"
}
return fmt.Sprintf("node with value %d", n.Value)
}
func main() {
var n *Node
fmt.Println(n.Describe())
n2 := &Node{Value: 42}
fmt.Println(n2.Describe())
}
Output:
empty node
node with value 42
n is a nil *Node, but calling n.Describe() is completely legal: the method receives n as a plain pointer value and checks it for nil before touching any field. Only dereferencing a nil pointer, such as reading n.Value without the guard, would panic. This pattern is common in linked data structures like trees and linked lists, where an empty subtree is represented as a nil pointer.
Common Mistakes
Mistake 1: Using a Value Receiver When You Need to Mutate
This is the single most common method bug in Go. The method compiles fine and looks correct, but it silently does nothing because it only ever modifies a copy.
package main
import "fmt"
type Counter struct {
count int
}
func (c Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println("Count:", c.count)
}
Output:
Count: 0
Because Increment has a value receiver, c inside the method is a fresh copy of the Counter each time. Incrementing that copy has no effect on the original. The fix is to switch to a pointer receiver:
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println("Count:", c.count)
}
Output:
Count: 2
Mistake 2: Calling a Pointer-Receiver Method on a Non-Addressable Map Value
Map values in Go are not addressable, so you cannot call a pointer-receiver method directly on myMap[key]. This is a compile-time error, not a runtime one, but it surprises many people coming from languages where every object reference behaves the same way.
package main
type Point struct {
X, Y int
}
func (p *Point) Scale(factor int) {
p.X *= factor
p.Y *= factor
}
func main() {
points := map[string]Point{"origin": {0, 0}, "a": {1, 2}}
points["a"].Scale(3) // compile error: cannot call pointer method on points["a"]
}
The fix is to either store pointers in the map so each entry is addressable, or to read the value out, modify the copy, and write it back explicitly. Storing pointers is usually simplest:
package main
import "fmt"
type Point struct {
X, Y int
}
func (p *Point) Scale(factor int) {
p.X *= factor
p.Y *= factor
}
func main() {
points := map[string]*Point{"origin": {0, 0}, "a": {1, 2}}
points["a"].Scale(3)
fmt.Println(*points["a"])
}
Output:
{3 6}
Best Practices
- Use a pointer receiver whenever the method needs to modify the receiver’s fields.
- Use a pointer receiver for large structs to avoid copying their contents on every call.
- Once a type has one pointer-receiver method, make all of its methods pointer receivers for consistency, even the ones that only read data.
- Use value receivers for small, simple, immutable-style types, such as a coordinate pair or a duration wrapper.
- If a pointer-receiver method might reasonably be called on a
nilpointer, for example in a tree or linked-list node, check fornilat the top of the method. - Remember that only addressable values get their address taken automatically; values from map indexing or function returns are not addressable, so store pointers instead if you need to call pointer methods on them.
- Give the receiver a short, consistent name based on the type’s initial, such as
rforRectangle, neverthisorself; that is not idiomatic Go. - Keep methods focused on one responsibility; a struct that accumulates dozens of unrelated methods is usually a sign it should be split into smaller types.
Practice Exercises
- Define a
Circlestruct with aRadius float64field. Write value-receiver methodsArea()andCircumference()usingmath.Pi. For a circle with radius 5, print both values (expect roughly78.53981633974483and31.41592653589793). - Define a
Stackstruct wrapping a[]int. Write pointer-receiver methodsPush(v int)andPop() (int, bool), where the boolean reports whether the stack was non-empty. Push 10, 20, and 30, then pop three times and print each result. - Define a
TreeNodestruct with anintvalue andLeft/Right*TreeNodefields. Write a pointer-receiver methodSum() intthat returns 0 for anilreceiver and otherwise returns the node’s value plus the sum of both children. Build a small three-node tree and print the total.
Summary
- A method is a function with a receiver argument that binds it to a type, enabling
value.Method()syntax. - Value receivers operate on a copy; pointer receivers operate on the original and can mutate it.
- Methods can only be declared on types defined in the same package as the method.
- A type’s method set determines which interfaces it satisfies; pointer-receiver methods are only in the method set of the pointer type.
- Go automatically takes the address of an addressable value to call a pointer-receiver method, but non-addressable values, like map entries, require extra care.
- Pointer-receiver methods can be called safely on a
nilreceiver as long as the method guards against dereferencing it. - Prefer pointer receivers for mutation or large structs, and keep receiver types consistent across a type’s method set.
