Foundations Updated 2026-09 View as Markdown

Methods and Pointers

Pointers in Go are simple by design: no arithmetic, no manual freeing. The one decision you make repeatedly is value receiver or pointer receiver.

A pointer holds the address of a value.

go
x := 42
p := &x            // &x is "address of x"; p has type *int
fmt.Println(*p)    // 42 — *p is "the value at p"
*p = 100
fmt.Println(x)     // 100 — we changed x through p

Two operators: & takes an address, * follows one. Unlike C there is no pointer arithmetic — you cannot do p++ — and there is no manual deallocation, because the garbage collector handles it. That removes most of what makes pointers dangerous elsewhere.

Why pointers exist#

Go passes everything by value. Without a pointer, a function receives a copy:

go
func rename(u User)  { u.Name = "new" }    // modifies the copy
func rename2(u *User) { u.Name = "new" }   // modifies the original

user := User{Name: "old"}
rename(user)
fmt.Println(user.Name)     // "old"
rename2(&user)
fmt.Println(user.Name)     // "new"

The second reason is cost: copying a large struct on every call is wasted work.

nil#

The zero value of a pointer is nil. Dereferencing it panics:

go
var p *User
fmt.Println(p == nil)      // true
fmt.Println(p.Name)        // panic: nil pointer dereference

This is Go's most common runtime crash. Anywhere a function can return a pointer, check it before use:

go
user, err := findUser(id)
if err != nil {
    return err
}
// only safe past the error check
fmt.Println(user.Name)

new(T) allocates a zeroed T and returns a pointer to it, though a literal is more common:

go
p := new(User)          // *User, zero valued
p := &User{Name: "Ada"} // more idiomatic

Methods#

A method is a function with a receiver — the thing before the name:

go
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}
go
rect := Rectangle{Width: 3, Height: 4}
fmt.Println(rect.Area())     // 12
rect.Scale(2)                // Go takes &rect automatically
fmt.Println(rect.Area())     // 48

You can define methods on any type you declare in your package, not just structs:

go
type Celsius float64

func (c Celsius) String() string {
    return fmt.Sprintf("%.1f°C", float64(c))
}

fmt.Println(Celsius(21.5))   // 21.5°C — fmt uses the String method

Implementing String() string makes your type print nicely everywhere. It is the single most useful method to add to a domain type.

Value or pointer receiver#

The decision you make on every method. The rules:

Use a pointer receiver when:

  • the method modifies the receiver
  • the struct is large (copying is wasteful)
  • the type contains a sync.Mutex or anything else that must not be copied

Use a value receiver when:

  • the type is small and immutable in practice (time.Time, a Celsius)

Structs are copied on assignment#

go
a := Rectangle{Width: 1, Height: 2}
b := a              // a full copy
b.Width = 99
fmt.Println(a.Width) // 1 — unaffected

That is different from most object-oriented languages, where assignment copies a reference. It is usually what you want, and it is why you need & to share.

Note that a struct containing a slice or a map copies the header, not the underlying data — so both copies share the same backing array:

go
type Team struct{ Members []string }
a := Team{Members: []string{"ada"}}
b := a
b.Members[0] = "grace"
fmt.Println(a.Members[0])    // "grace" — shared backing array

Exercise#

go
package main

import "fmt"

type Counter struct {
	count int
}

func main() {
	// Add two methods to Counter:
	//   Increment() with a pointer receiver
	//   Value() int with a value receiver... then reconsider,
	//   and make BOTH pointer receivers for consistency.
	// Also add String() string so fmt prints "Counter(3)".
	c := Counter{}
	fmt.Println(c)
}

Common questions#

Do I need * to call a method on a pointer?#

No. Go dereferences automatically, so p.Method() works whether p is a T or a *T, and it takes the address for you when calling a pointer method on an addressable value. You only write *p when you want the value itself.

Pointer or value receiver, if I am unsure?#

Pointer. It is the more common convention, it avoids copying, and it means the method set works for interfaces without surprises. The main exception is small immutable value types where a value receiver reads more naturally.

Are Go pointers dangerous like C pointers?#

Much less so. There is no pointer arithmetic, no manual freeing, no dangling pointers after a free, and the garbage collector keeps anything reachable alive. The one real hazard left is dereferencing nil, which panics loudly rather than corrupting memory.

Get the Go agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.