← thecodex.expert · The Codex Family of Knowledge
Go

Structs and Methods

Go has no classes — a struct groups data, and a method is just a function with an extra receiver argument bound to that struct’s type.

Go 1.26 No classes Last verified:
Where this bites you

You copy a struct before handing it off, trusting the copy to be independent — that's the whole point of a value type. You change the copy's name, leave the original alone, everything looks fine. Then you change one of the copy's slice fields, and the original changes too, even though you never touched it directly. The struct was copied. The slice inside it wasn't.

Gostructcopy.go
type Team struct {
    Name    string
    Members []string
}

original := Team{Name: "Blue", Members: []string{"Ann", "Bo"}}
dup := original
dup.Name = "Red"              // fine — Name is a plain string, copied for real
dup.Members[0] = "HACKED"     // NOT fine — Members is a slice header, not the data
fmt.Println(original)         // {Blue [HACKED Bo]} — original.Members changed too

The fix doesn't involve receivers at all — it's at the very bottom of this page, once struct copying and method receivers have both been covered.

Canonical Definition

A struct is a composite type that groups together zero or more named fields of any type. Go has no class keyword; instead, a method is declared as an ordinary function with an extra receiver parameter before its name, which binds that function to a specific named type. The receiver can be a value (func (p Point) Method()) or a pointer (func (p *Point) Method()), and the choice changes whether the method can mutate the original data.

Declaring and using structs

A struct groups named fields under one type. Struct values can be created with field names (recommended, order-independent) or positionally (order-dependent and brittle if the struct changes), and fields are accessed with dot notation.

Gostructs.go
type Point struct {
    X, Y int
}

p1 := Point{X: 3, Y: 4}   // named fields — preferred
p2 := Point{3, 4}          // positional — works, but brittle

p1.X = 10                  // dot notation to access/set fields
fmt.Println(p1.X, p1.Y)    // 10 4

Methods: functions with a receiver

A method looks like a regular function, but names a receiver type in parentheses before the function name. That receiver is how Go attaches behavior to a type without a class hierarchy — any named type, not just structs, can have methods.

Gomethods.go
type Rectangle struct {
    Width, Height float64
}

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

rect := Rectangle{Width: 3, Height: 4}
fmt.Println(rect.Area())   // 12

Value receivers vs. pointer receivers

This is the detail that trips up newcomers most: a method with a value receiver operates on a copy of the struct, so changes inside the method never affect the original. A method with a pointer receiver (*Rectangle) operates on the original data, so mutations are visible to the caller. Go automatically takes the address when you call a pointer-receiver method on an addressable value, so the call syntax looks identical either way — the difference is entirely in the method's declaration, not the call site. The idiomatic rule: if any method on a type needs a pointer receiver (to mutate, or to avoid copying a large struct), make all of that type's methods use pointer receivers, for consistency.

Goreceivers.go
func (r Rectangle) Scale(factor float64) {    // value receiver: no effect outside
    r.Width *= factor
}

func (r *Rectangle) ScalePtr(factor float64) { // pointer receiver: mutates original
    r.Width *= factor
}

rect := Rectangle{Width: 3, Height: 4}
rect.Scale(2)
fmt.Println(rect.Width)     // still 3 — Scale worked on a copy

rect.ScalePtr(2)
fmt.Println(rect.Width)     // 6 — ScalePtr mutated the original

Which brings us back to the copied-struct bug at the top of this page. A plain assignment (dup := original) copies a struct field-by-field — but when a field is itself a slice, map, or pointer, what gets copied is the header or address, not the data it refers to. Both Team values end up pointing at the same backing array. The fix is a method that builds a genuinely independent copy, field by field, allocating a new backing array for any reference-like field:

Gostructcopy_fix.go
func (t Team) Clone() Team {
    members := make([]string, len(t.Members))
    copy(members, t.Members)   // copies the elements, not just the header
    return Team{Name: t.Name, Members: members}
}

dup := original.Clone()
dup.Members[0] = "HACKED"
fmt.Println(original)   // {Blue [Ann Bo]} — untouched this time

Sources

1
The Go Authors. The Go Programming Language Specification, "Method declarations", and "A Tour of Go" — Methods section, go.dev/ref/spec & go.dev/tour.