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.
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 pTwo 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:
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:
var p *User
fmt.Println(p == nil) // true
fmt.Println(p.Name) // panic: nil pointer dereferenceThis is Go's most common runtime crash. Anywhere a function can return a pointer, check it before use:
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:
p := new(User) // *User, zero valued
p := &User{Name: "Ada"} // more idiomaticMethods#
A method is a function with a receiver — the thing before the name:
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
}rect := Rectangle{Width: 3, Height: 4}
fmt.Println(rect.Area()) // 12
rect.Scale(2) // Go takes &rect automatically
fmt.Println(rect.Area()) // 48You can define methods on any type you declare in your package, not just structs:
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 methodImplementing 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.Mutexor anything else that must not be copied
Use a value receiver when:
- the type is small and immutable in practice (
time.Time, aCelsius)
Structs are copied on assignment#
a := Rectangle{Width: 1, Height: 2}
b := a // a full copy
b.Width = 99
fmt.Println(a.Width) // 1 — unaffectedThat 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:
type Team struct{ Members []string }
a := Team{Members: []string{"ada"}}
b := a
b.Members[0] = "grace"
fmt.Println(a.Members[0]) // "grace" — shared backing arrayExercise#
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.
AGENTS.md now — no email needed.