Methods and Receivers
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 originalThe 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
Thas the methods with receiverT. - Type
*Thas the methods with receiverTand 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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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:
- Method value:
t.Summarybinds the method to the specific instancet. It produces a function with signaturefunc() string. - Method expression:
Ticket.Summaryyields an unbound function with signaturefunc(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.goOutput:
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.goOutput:
# 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
nilgracefully: checkif t == nilbefore accessing fields. - Use method values (
instance.Method) when passing a callback to event loops or HTTP handlers. - For interfaces, remember the method set.
*Tis 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
- In
ticket_label.go, changeLabelto a pointer receiver. Confirm botht.Label()andp.Label()still run. - In
nil_receiver.go, remove theif t == nilcheck and runt1.Description(). Observe the nil pointer dereference panic. - In
method_values.go, passsummaryFuncinto a helper functionfunc printReport(f func() string)and verify it executes without needingt. - In
seats.go, tryTable{Number: 1, Seats: 2}.bump()— a method call on a temporary. Read the compiler error (the value is not addressable). - In
drawer.go, addfunc (d Drawer) NameTag() stringthat returnsd.Name. Call it ondand on&d.