Methods and Receivers

Updated

September 13, 2026

Methods and Receivers

A method is a function with one extra parameter written in front: the receiver. The boring default is a value receiver when the method only reads, and a pointer receiver when it writes. Pick one style per type and keep it.

Mental model

func (t Ticket) Label() string    // value receiver: works on a copy of t
func (t *Ticket) Seat(n int)      // pointer receiver: t points at the original

The receiver is still pass-by-value. A value receiver copies the struct. A pointer receiver copies the pointer (the address), so the method can change fields.

Go will take the address of an addressable variable for you: if bump has a *Table receiver, t.bump() means (&t).bump(). That convenience does not apply when you put the value in an interface. Interfaces use method sets:

  • Type T has the methods with receiver T.
  • Type *T has the methods with receiver T and the methods with receiver *T.

So a value of type Drawer does not satisfy an interface that needs Close() if Close is on *Drawer.

Worked examples

Case 1: A value receiver, called two ways

Save as ticket_label.go. Label only reads. A copy is fine. Calling it on a pointer still works: Go copies the pointed-to struct into the value receiver.

// ticket_label.go
package main

import "fmt"

type Ticket struct {
    ID    int
    Table int
}

func (t Ticket) Label() string {
    return fmt.Sprintf("ticket %d → table %d", t.ID, t.Table)
}

func main() {
    t := Ticket{ID: 7, Table: 12}
    fmt.Println(t.Label())
    p := &t
    fmt.Println(p.Label())
}

Run:

go run ticket_label.go

Output:

ticket 7 → table 12
ticket 7 → table 12

There is no this. The receiver is an argument with a slightly special place in the syntax.

Case 2: Mutation needs a pointer

Save as seats.go. The value-receiver version increments a copy. The pointer-receiver version increments the table you care about. t.bump() is allowed because t is a variable (addressable).

// seats.go
package main

import "fmt"

type Table struct {
    Number int
    Seats  int
}

func (t Table) bumpValue() {
    t.Seats++
}

func (t *Table) bump() {
    t.Seats++
}

func main() {
    t := Table{Number: 4, Seats: 2}
    t.bumpValue()
    fmt.Println("after value:", t.Seats)
    t.bump()
    fmt.Println("after pointer:", t.Seats)
}

Run:

go run seats.go

Output:

after value: 2
after pointer: 3

If a method must change the receiver, or must share one struct with other methods that change it, use *T. Mixing a pointer Close with a value Label on the same type is allowed; mixing them at random is how method-set errors show up on Friday.

Case 3: Method sets and interfaces

Save as drawer.go. Closer needs Close(). Close has a pointer receiver, so you pass *Drawer.

// drawer.go
package main

import "fmt"

type Drawer struct {
    Name string
    Open bool
}

func (d *Drawer) Close() {
    d.Open = false
}

type Closer interface {
    Close()
}

func shut(c Closer) {
    c.Close()
}

func main() {
    d := Drawer{Name: "till", Open: true}
    shut(&d)
    fmt.Println(d.Open)
}

Run:

go run drawer.go

Output:

false

&d has type *Drawer. *Drawer includes Close. The field flips on the original d.

Case 4: Defensive methods on nil receivers

In many object-oriented languages, invoking a method on a nil or null reference immediately causes a crash. In Go, calling a method on a nil pointer receiver is valid and executes the function body. The receiver is simply passed as a nil argument. The method can check t == nil and return a sensible default.

Save as nil_receiver.go:

// nil_receiver.go
package main

import "fmt"

type Table struct {
    Number int
}

func (t *Table) Description() string {
    if t == nil {
        return "no table assigned"
    }
    return fmt.Sprintf("table %d", t.Number)
}

func main() {
    var t1 *Table
    t2 := &Table{Number: 12}

    fmt.Println(t1.Description())
    fmt.Println(t2.Description())
}

Run:

go run nil_receiver.go

Output:

no table assigned
table 12

This pattern is widely used in standard library types (like *bytes.Buffer.String() or error types) to avoid panics on uninitialized pointers.

Case 5: Method values and method expressions

A method can be extracted as a first-class function value:

  1. Method value: t.Summary binds the method to the specific instance t. It produces a function with signature func() string.
  2. Method expression: Ticket.Summary yields an unbound function with signature func(Ticket) string, where the receiver is passed explicitly as the first argument.

Save as method_values.go:

// method_values.go
package main

import "fmt"

type Ticket struct {
    ID    int
    Table int
}

func (t Ticket) Summary() string {
    return fmt.Sprintf("ticket %d (table %d)", t.ID, t.Table)
}

func main() {
    t := Ticket{ID: 41, Table: 8}

    // Method value: bound to instance t
    summaryFunc := t.Summary
    fmt.Println("method value:", summaryFunc())

    // Method expression: unbound, takes instance as first argument
    exprFunc := Ticket.Summary
    fmt.Println("method expression:", exprFunc(t))
}

Run:

go run method_values.go

Output:

method value: ticket 41 (table 8)
method expression: ticket 41 (table 8)

Method values are useful for passing callbacks (e.g. http.HandlerFunc(srv.handleOrder)).

The trap

The same shut with a value does not compile. t.Close() would have been rewritten to (&t).Close() if t were a variable in the caller. Once the value is in the interface, that rewrite is gone.

Save as drawer_value.go:

// drawer_value.go
package main

import "fmt"

type Drawer struct {
    Name string
    Open bool
}

func (d *Drawer) Close() {
    d.Open = false
}

type Closer interface {
    Close()
}

func shut(c Closer) {
    c.Close()
}

func main() {
    d := Drawer{Name: "till", Open: true}
    shut(d)
    fmt.Println(d.Open)
}

Run:

go run drawer_value.go

Output:

# command-line-arguments
./drawer_value.go:25:7: cannot use d (variable of struct type Drawer) as Closer value in argument to shut: Drawer does not implement Closer (method Close has pointer receiver)

If you are inside a module, the first line is the module path instead of command-line-arguments. The error text is the point.

The fix is the previous program: pass &d, or give Close a value receiver if it does not mutate (it does, so do not). When a type has any pointer-receiver methods, treat *T as the type you store in interfaces.

The boring rule

  • Value receiver: small struct, method only reads.
  • Pointer receiver: method writes fields, or the struct is the identity you want to share.
  • Do not mix receiver kinds on one type without a reason. If one method needs *T, most of the others can too.
  • Pointer receivers can handle nil gracefully: check if t == nil before accessing fields.
  • Use method values (instance.Method) when passing a callback to event loops or HTTP handlers.
  • For interfaces, remember the method set. *T is the safe choice when any method has a pointer receiver.
  • A method is still a function. Ticket.Label(t) works; t.Label() is the same call with nicer syntax.

Try this

  1. In ticket_label.go, change Label to a pointer receiver. Confirm both t.Label() and p.Label() still run.
  2. In nil_receiver.go, remove the if t == nil check and run t1.Description(). Observe the nil pointer dereference panic.
  3. In method_values.go, pass summaryFunc into a helper function func printReport(f func() string) and verify it executes without needing t.
  4. In seats.go, try Table{Number: 1, Seats: 2}.bump() — a method call on a temporary. Read the compiler error (the value is not addressable).
  5. In drawer.go, add func (d Drawer) NameTag() string that returns d.Name. Call it on d and on &d.