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:

  1. It looks up the method set of rect‘s type (Rectangle) for a method named Area.
  2. It checks whether the receiver type declared for Area matches, 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.
  3. 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 nil pointer, for example in a tree or linked-list node, check for nil at 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 r for Rectangle, never this or self; 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

  1. Define a Circle struct with a Radius float64 field. Write value-receiver methods Area() and Circumference() using math.Pi. For a circle with radius 5, print both values (expect roughly 78.53981633974483 and 31.41592653589793).
  2. Define a Stack struct wrapping a []int. Write pointer-receiver methods Push(v int) and Pop() (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.
  3. Define a TreeNode struct with an int value and Left/Right *TreeNode fields. Write a pointer-receiver method Sum() int that returns 0 for a nil receiver 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 nil receiver 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.